coding-tool-x 3.6.6 → 3.6.8
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 +7 -0
- package/dist/web/assets/{Analytics-CcGKjDkP.js → Analytics-C57ZRDq5.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-DPOa1w5D.js → ConfigTemplates-B3qqqL05.js} +1 -1
- package/dist/web/assets/Home-CG5XrTNc.js +1 -0
- package/dist/web/assets/Home-CIua-b-o.css +1 -0
- package/dist/web/assets/{PluginManager-BVbHUdey.js → PluginManager-BhX0LUzr.js} +1 -1
- package/dist/web/assets/{ProjectList-BQrz2FKo.js → ProjectList-BQ01OZn9.js} +1 -1
- package/dist/web/assets/{SessionList-CRGer64T.js → SessionList-DPv2IOXG.js} +1 -1
- package/dist/web/assets/{SkillManager-BhJKtWYM.js → SkillManager-D5CfQm2O.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-pc6tpF3m.js → WorkspaceManager-Ce8ggteK.js} +1 -1
- package/dist/web/assets/index-CpnFNWqC.js +2 -0
- package/dist/web/assets/index-DJ9VFidd.css +1 -0
- package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +1 -1
- package/src/config/channel-preset-websites.js +2 -1
- package/src/config/paths.js +8 -0
- package/src/server/api/agents.js +16 -54
- package/src/server/api/claude-hooks.js +1 -1
- package/src/server/api/claude-statistics.js +57 -0
- package/src/server/api/commands.js +16 -54
- package/src/server/api/dashboard.js +2 -2
- package/src/server/api/gemini-channels.js +2 -0
- package/src/server/api/hooks.js +4 -1
- package/src/server/api/plugins.js +40 -65
- package/src/server/api/skills.js +21 -77
- package/src/server/api/validation-errors.js +34 -0
- package/src/server/gemini-proxy-server.js +205 -21
- package/src/server/index.js +1 -0
- package/src/server/services/agents-service.js +166 -75
- package/src/server/services/base/base-channel-service.js +15 -0
- package/src/server/services/base/proxy-utils.js +6 -3
- package/src/server/services/base/response-usage-parser.js +44 -0
- package/src/server/services/channel-balance.js +86 -1
- package/src/server/services/claude-statistics-service.js +139 -0
- package/src/server/services/commands-service.js +203 -121
- package/src/server/services/config-artifact-paths.js +116 -0
- package/src/server/services/config-export-service.js +7 -6
- package/src/server/services/config-registry-service.js +2 -2
- package/src/server/services/config-sync-manager.js +45 -11
- package/src/server/services/config-templates-service.js +21 -19
- package/src/server/services/format-converter.js +42 -4
- package/src/server/services/gemini-channels.js +30 -1
- package/src/server/services/gemini-sessions.js +347 -61
- package/src/server/services/mcp-client.js +236 -49
- package/src/server/services/mcp-service.js +51 -64
- package/src/server/services/notification-hooks.js +659 -88
- package/src/server/services/opencode-channels.js +15 -0
- package/src/server/services/plugins-service.js +771 -63
- package/src/server/services/repo-scanner-base.js +6 -29
- package/src/server/services/skill-service.js +187 -63
- package/dist/web/assets/Home-BP1LxGtp.js +0 -1
- package/dist/web/assets/Home-BeGkC4bb.css +0 -1
- package/dist/web/assets/index-0isNzXNY.css +0 -1
- package/dist/web/assets/index-BsdIJwLf.js +0 -2
|
@@ -13,38 +13,14 @@ const http = require('http');
|
|
|
13
13
|
const { createWriteStream } = require('fs');
|
|
14
14
|
const AdmZip = require('adm-zip');
|
|
15
15
|
const { PATHS, getRepoScannerReposPath, getRepoScannerCachePath } = require('../../config/paths');
|
|
16
|
+
const {
|
|
17
|
+
normalizeSafeRelativePath,
|
|
18
|
+
resolveInsideRoot
|
|
19
|
+
} = require('./config-artifact-paths');
|
|
16
20
|
|
|
17
21
|
// 缓存有效期(5分钟)
|
|
18
22
|
const CACHE_TTL = 5 * 60 * 1000;
|
|
19
23
|
|
|
20
|
-
function normalizeSafeRelativePath(input, errorLabel) {
|
|
21
|
-
const raw = String(input || '').replace(/\\/g, '/').trim();
|
|
22
|
-
if (!raw || raw.includes('\0')) {
|
|
23
|
-
throw new Error(`Invalid ${errorLabel}`);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const normalized = path.posix.normalize(raw).replace(/^(\.\/)+/, '');
|
|
27
|
-
if (!normalized ||
|
|
28
|
-
normalized === '.' ||
|
|
29
|
-
normalized === '..' ||
|
|
30
|
-
normalized.startsWith('../') ||
|
|
31
|
-
normalized.includes('/../') ||
|
|
32
|
-
path.posix.isAbsolute(normalized)) {
|
|
33
|
-
throw new Error(`Invalid ${errorLabel}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
return normalized;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function resolveInsideRoot(rootDir, relativePath, errorLabel) {
|
|
40
|
-
const resolvedRoot = path.resolve(rootDir);
|
|
41
|
-
const resolvedPath = path.resolve(resolvedRoot, relativePath);
|
|
42
|
-
if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
43
|
-
throw new Error(`${errorLabel} escapes target directory`);
|
|
44
|
-
}
|
|
45
|
-
return resolvedPath;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
24
|
/**
|
|
49
25
|
* 仓库配置结构
|
|
50
26
|
* @typedef {Object} RepoConfig
|
|
@@ -449,7 +425,8 @@ class RepoScannerBase {
|
|
|
449
425
|
* 卸载项目
|
|
450
426
|
*/
|
|
451
427
|
uninstall(targetName) {
|
|
452
|
-
const
|
|
428
|
+
const safeTargetName = normalizeSafeRelativePath(targetName, 'target name');
|
|
429
|
+
const dest = resolveInsideRoot(this.installDir, safeTargetName, 'Target path');
|
|
453
430
|
|
|
454
431
|
if (fs.existsSync(dest)) {
|
|
455
432
|
if (fs.statSync(dest).isDirectory()) {
|
|
@@ -19,6 +19,11 @@ const {
|
|
|
19
19
|
} = require('./format-converter');
|
|
20
20
|
const { maskToken } = require('./oauth-utils');
|
|
21
21
|
const { NATIVE_PATHS, HOME_DIR, PATHS } = require('../../config/paths');
|
|
22
|
+
const {
|
|
23
|
+
normalizeSafeRelativePath,
|
|
24
|
+
pathHasProtectedSegment,
|
|
25
|
+
resolveInsideRoot
|
|
26
|
+
} = require('./config-artifact-paths');
|
|
22
27
|
|
|
23
28
|
const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
|
|
24
29
|
const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
|
|
@@ -157,6 +162,13 @@ function isRootSkillFile(filePath = '') {
|
|
|
157
162
|
return filePath === 'SKILL.md' || filePath.endsWith('/SKILL.md');
|
|
158
163
|
}
|
|
159
164
|
|
|
165
|
+
function normalizeSkillRelativePath(input, label = 'skill directory', options = {}) {
|
|
166
|
+
return normalizeSafeRelativePath(input, label, {
|
|
167
|
+
allowHiddenSegments: true,
|
|
168
|
+
...options
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
160
172
|
const DEFAULT_REPOS_BY_PLATFORM = {
|
|
161
173
|
claude: [],
|
|
162
174
|
codex: [],
|
|
@@ -254,6 +266,7 @@ class SkillService {
|
|
|
254
266
|
: [];
|
|
255
267
|
|
|
256
268
|
this.mergeLocalSkills(preparedSkills);
|
|
269
|
+
this.mergeInstalledSkills(preparedSkills);
|
|
257
270
|
this.deduplicateSkills(preparedSkills);
|
|
258
271
|
preparedSkills.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
|
|
259
272
|
this.updateInstallStatus(preparedSkills);
|
|
@@ -783,8 +796,11 @@ class SkillService {
|
|
|
783
796
|
async fetchLocalRepoSkills(repo) {
|
|
784
797
|
const skills = [];
|
|
785
798
|
const repoRoot = repo.localPath;
|
|
786
|
-
const
|
|
787
|
-
|
|
799
|
+
const safeDirectory = normalizeSkillRelativePath(repo.directory || '', 'skill repository directory', {
|
|
800
|
+
allowEmpty: true
|
|
801
|
+
});
|
|
802
|
+
const scanRoot = safeDirectory
|
|
803
|
+
? resolveInsideRoot(repoRoot, safeDirectory, 'Skill repository directory', { allowHiddenSegments: true })
|
|
788
804
|
: repoRoot;
|
|
789
805
|
|
|
790
806
|
if (!fs.existsSync(scanRoot)) {
|
|
@@ -901,7 +917,8 @@ class SkillService {
|
|
|
901
917
|
return this.fetchGitLabFileContent(repo, file.path);
|
|
902
918
|
}
|
|
903
919
|
if (repo.provider === 'local') {
|
|
904
|
-
const
|
|
920
|
+
const safeFilePath = normalizeSkillRelativePath(file.path, 'skill repository file path');
|
|
921
|
+
const localFilePath = resolveInsideRoot(repo.localPath, safeFilePath, 'Skill repository file path', { allowHiddenSegments: true });
|
|
905
922
|
return fs.readFileSync(localFilePath, 'utf-8');
|
|
906
923
|
}
|
|
907
924
|
return this.fetchGitHubBlobContent(file.sha, repo);
|
|
@@ -1310,7 +1327,7 @@ class SkillService {
|
|
|
1310
1327
|
*/
|
|
1311
1328
|
parseSkillMd(content) {
|
|
1312
1329
|
// 使用格式转换器统一解析
|
|
1313
|
-
const parsed = parseSkillContent(content);
|
|
1330
|
+
const parsed = parseSkillContent(content, { platform: this.platform });
|
|
1314
1331
|
|
|
1315
1332
|
return {
|
|
1316
1333
|
name: parsed.name || null,
|
|
@@ -1328,13 +1345,40 @@ class SkillService {
|
|
|
1328
1345
|
return String(directory).replace(/\\/g, '/').split('/').pop();
|
|
1329
1346
|
}
|
|
1330
1347
|
|
|
1348
|
+
normalizeSkillDirectory(directory, label = 'skill directory') {
|
|
1349
|
+
return normalizeSkillRelativePath(directory, label);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
resolveInstallPath(directory, label = 'skill directory') {
|
|
1353
|
+
const safeDirectory = this.normalizeSkillDirectory(directory, label);
|
|
1354
|
+
return {
|
|
1355
|
+
safeDirectory,
|
|
1356
|
+
path: resolveInsideRoot(this.installDir, safeDirectory, label, { allowHiddenSegments: true })
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
resolveStoragePath(directory, label = 'skill directory') {
|
|
1361
|
+
const safeDirectory = this.normalizeSkillDirectory(directory, label);
|
|
1362
|
+
return {
|
|
1363
|
+
safeDirectory,
|
|
1364
|
+
path: resolveInsideRoot(this.storageDir, safeDirectory, label, { allowHiddenSegments: true })
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
isProtectedSkillDirectory(directory) {
|
|
1369
|
+
return this.platform === 'codex' && pathHasProtectedSegment(directory, ['.system']);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1331
1372
|
/**
|
|
1332
1373
|
* 检查技能是否已安装
|
|
1333
1374
|
*/
|
|
1334
1375
|
isInstalled(directory) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1376
|
+
try {
|
|
1377
|
+
const { path: skillPath } = this.resolveInstallPath(directory);
|
|
1378
|
+
return fs.existsSync(path.join(skillPath, 'SKILL.md'));
|
|
1379
|
+
} catch {
|
|
1380
|
+
return false;
|
|
1381
|
+
}
|
|
1338
1382
|
}
|
|
1339
1383
|
|
|
1340
1384
|
/**
|
|
@@ -1347,10 +1391,22 @@ class SkillService {
|
|
|
1347
1391
|
this.scanLocalDir(this.storageDir, this.storageDir, skills);
|
|
1348
1392
|
}
|
|
1349
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* 合并平台原生已安装技能(扫描 installDir)。
|
|
1396
|
+
*/
|
|
1397
|
+
mergeInstalledSkills(skills) {
|
|
1398
|
+
if (!fs.existsSync(this.installDir)) return;
|
|
1399
|
+
this.scanLocalDir(this.installDir, this.installDir, skills, {
|
|
1400
|
+
includeHiddenDirs: this.platform === 'codex',
|
|
1401
|
+
forceInstalled: true,
|
|
1402
|
+
source: 'native-installed'
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1350
1406
|
/**
|
|
1351
1407
|
* 递归扫描本地目录
|
|
1352
1408
|
*/
|
|
1353
|
-
scanLocalDir(currentDir, baseDir, skills) {
|
|
1409
|
+
scanLocalDir(currentDir, baseDir, skills, options = {}) {
|
|
1354
1410
|
const skillMdPath = path.join(currentDir, 'SKILL.md');
|
|
1355
1411
|
|
|
1356
1412
|
if (fs.existsSync(skillMdPath)) {
|
|
@@ -1364,31 +1420,58 @@ class SkillService {
|
|
|
1364
1420
|
return normalizeRepoPath(s.directory).toLowerCase() === normalizedDirectory;
|
|
1365
1421
|
});
|
|
1366
1422
|
|
|
1423
|
+
const protectedSkill = this.isProtectedSkillDirectory(directory);
|
|
1424
|
+
const skillSource = protectedSkill ? 'system-installed' : (options.source || 'local');
|
|
1425
|
+
|
|
1367
1426
|
// 判断是否已安装到平台目录
|
|
1368
|
-
|
|
1427
|
+
let isInstalled = options.forceInstalled === true;
|
|
1428
|
+
if (!isInstalled) {
|
|
1429
|
+
try {
|
|
1430
|
+
isInstalled = fs.existsSync(path.join(this.resolveInstallPath(directory).path, 'SKILL.md'));
|
|
1431
|
+
} catch {
|
|
1432
|
+
isInstalled = false;
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1369
1435
|
|
|
1370
1436
|
if (existing) {
|
|
1371
1437
|
existing.installed = isInstalled;
|
|
1372
|
-
|
|
1438
|
+
if (protectedSkill) {
|
|
1439
|
+
existing.source = 'system-installed';
|
|
1440
|
+
existing.protected = true;
|
|
1441
|
+
existing.isLocal = false;
|
|
1442
|
+
existing.key = `system:${this.platform}:${directory}`;
|
|
1443
|
+
} else if (options.source === 'native-installed') {
|
|
1444
|
+
existing.source = existing.source || 'native-installed';
|
|
1445
|
+
} else {
|
|
1446
|
+
existing.isLocal = true;
|
|
1447
|
+
}
|
|
1373
1448
|
} else {
|
|
1374
1449
|
// 添加 cc-tool 托管的技能
|
|
1375
1450
|
try {
|
|
1376
1451
|
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
1377
1452
|
const metadata = this.parseSkillMd(content);
|
|
1378
1453
|
|
|
1379
|
-
|
|
1454
|
+
const skill = {
|
|
1380
1455
|
key: `local:${directory}`,
|
|
1381
1456
|
name: metadata.name || directory,
|
|
1382
1457
|
description: metadata.description || '',
|
|
1383
1458
|
directory,
|
|
1384
1459
|
installed: isInstalled,
|
|
1385
|
-
isLocal:
|
|
1460
|
+
isLocal: skillSource === 'local',
|
|
1461
|
+
source: skillSource,
|
|
1462
|
+
protected: protectedSkill,
|
|
1386
1463
|
readmeUrl: null,
|
|
1387
1464
|
repoOwner: null,
|
|
1388
1465
|
repoName: null,
|
|
1389
1466
|
repoBranch: null,
|
|
1390
1467
|
license: metadata.license
|
|
1391
|
-
}
|
|
1468
|
+
};
|
|
1469
|
+
if (protectedSkill) {
|
|
1470
|
+
skill.key = `system:${this.platform}:${directory}`;
|
|
1471
|
+
} else if (options.source === 'native-installed') {
|
|
1472
|
+
skill.key = `native:${this.platform}:${directory}`;
|
|
1473
|
+
}
|
|
1474
|
+
skills.push(skill);
|
|
1392
1475
|
} catch (err) {
|
|
1393
1476
|
console.warn(`[SkillService] Parse local skill ${directory} error:`, err.message);
|
|
1394
1477
|
}
|
|
@@ -1401,8 +1484,8 @@ class SkillService {
|
|
|
1401
1484
|
try {
|
|
1402
1485
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1403
1486
|
for (const entry of entries) {
|
|
1404
|
-
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
1405
|
-
this.scanLocalDir(path.join(currentDir, entry.name), baseDir, skills);
|
|
1487
|
+
if (entry.isDirectory() && (options.includeHiddenDirs || !entry.name.startsWith('.'))) {
|
|
1488
|
+
this.scanLocalDir(path.join(currentDir, entry.name), baseDir, skills, options);
|
|
1406
1489
|
}
|
|
1407
1490
|
}
|
|
1408
1491
|
} catch (err) {
|
|
@@ -1445,7 +1528,10 @@ class SkillService {
|
|
|
1445
1528
|
* @param {string} [fullDirectory] - 仓库中的完整路径(可选,默认与 directory 相同)
|
|
1446
1529
|
*/
|
|
1447
1530
|
async installSkill(directory, repo, fullDirectory = null) {
|
|
1448
|
-
const dest =
|
|
1531
|
+
const { safeDirectory, path: dest } = this.resolveInstallPath(directory);
|
|
1532
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1533
|
+
throw new Error('不能安装到 Codex 系统技能目录');
|
|
1534
|
+
}
|
|
1449
1535
|
const normalizedRepo = this.normalizeRepoConfig(repo);
|
|
1450
1536
|
|
|
1451
1537
|
// 已安装则跳过
|
|
@@ -1454,11 +1540,14 @@ class SkillService {
|
|
|
1454
1540
|
}
|
|
1455
1541
|
|
|
1456
1542
|
// 使用 fullDirectory(仓库中的完整路径)或 directory(向后兼容)
|
|
1457
|
-
const sourcePath = fullDirectory ||
|
|
1543
|
+
const sourcePath = fullDirectory || safeDirectory;
|
|
1544
|
+
const safeSourcePath = sourcePath
|
|
1545
|
+
? normalizeSkillRelativePath(sourcePath, 'skill source directory')
|
|
1546
|
+
: '';
|
|
1458
1547
|
|
|
1459
1548
|
if (normalizedRepo.provider === 'local') {
|
|
1460
|
-
const sourceDir =
|
|
1461
|
-
?
|
|
1549
|
+
const sourceDir = safeSourcePath
|
|
1550
|
+
? resolveInsideRoot(normalizedRepo.localPath, safeSourcePath, 'Skill source directory', { allowHiddenSegments: true })
|
|
1462
1551
|
: normalizedRepo.localPath;
|
|
1463
1552
|
|
|
1464
1553
|
if (!fs.existsSync(sourceDir)) {
|
|
@@ -1513,7 +1602,9 @@ class SkillService {
|
|
|
1513
1602
|
}
|
|
1514
1603
|
|
|
1515
1604
|
const repoDir = path.join(tempDir, extractedDirs[0]);
|
|
1516
|
-
const sourceDir =
|
|
1605
|
+
const sourceDir = safeSourcePath
|
|
1606
|
+
? resolveInsideRoot(repoDir, safeSourcePath, 'Skill source directory', { allowHiddenSegments: true })
|
|
1607
|
+
: repoDir;
|
|
1517
1608
|
|
|
1518
1609
|
if (!fs.existsSync(sourceDir)) {
|
|
1519
1610
|
throw new Error(`Skill directory not found: ${sourcePath}`);
|
|
@@ -1609,7 +1700,7 @@ class SkillService {
|
|
|
1609
1700
|
* 创建自定义技能
|
|
1610
1701
|
*/
|
|
1611
1702
|
createCustomSkill({ name, directory, description, content }) {
|
|
1612
|
-
const dest =
|
|
1703
|
+
const { safeDirectory, path: dest } = this.resolveStoragePath(directory);
|
|
1613
1704
|
|
|
1614
1705
|
// 检查是否已存在
|
|
1615
1706
|
if (fs.existsSync(dest)) {
|
|
@@ -1636,7 +1727,7 @@ ${content}
|
|
|
1636
1727
|
|
|
1637
1728
|
this.clearCache({ removeFile: true });
|
|
1638
1729
|
|
|
1639
|
-
return { success: true, message: '技能创建成功', directory };
|
|
1730
|
+
return { success: true, message: '技能创建成功', directory: safeDirectory };
|
|
1640
1731
|
}
|
|
1641
1732
|
|
|
1642
1733
|
/**
|
|
@@ -1646,7 +1737,7 @@ ${content}
|
|
|
1646
1737
|
* @returns {Object} 创建结果
|
|
1647
1738
|
*/
|
|
1648
1739
|
createSkillWithFiles({ directory, files }) {
|
|
1649
|
-
const dest =
|
|
1740
|
+
const { safeDirectory, path: dest } = this.resolveStoragePath(directory);
|
|
1650
1741
|
|
|
1651
1742
|
// 检查是否已存在
|
|
1652
1743
|
if (fs.existsSync(dest)) {
|
|
@@ -1654,8 +1745,12 @@ ${content}
|
|
|
1654
1745
|
}
|
|
1655
1746
|
|
|
1656
1747
|
// 验证必须包含 SKILL.md
|
|
1657
|
-
const
|
|
1658
|
-
|
|
1748
|
+
const safeFiles = files.map(file => ({
|
|
1749
|
+
...file,
|
|
1750
|
+
safePath: normalizeSkillRelativePath(file.path, 'skill file path')
|
|
1751
|
+
}));
|
|
1752
|
+
const hasSkillMd = safeFiles.some(f =>
|
|
1753
|
+
f.safePath === 'SKILL.md' || f.safePath.endsWith('/SKILL.md')
|
|
1659
1754
|
);
|
|
1660
1755
|
if (!hasSkillMd) {
|
|
1661
1756
|
throw new Error('技能必须包含 SKILL.md 文件');
|
|
@@ -1665,8 +1760,8 @@ ${content}
|
|
|
1665
1760
|
fs.mkdirSync(dest, { recursive: true });
|
|
1666
1761
|
|
|
1667
1762
|
// 写入所有文件
|
|
1668
|
-
for (const file of
|
|
1669
|
-
const filePath =
|
|
1763
|
+
for (const file of safeFiles) {
|
|
1764
|
+
const filePath = resolveInsideRoot(dest, file.safePath, 'skill file path', { allowHiddenSegments: true });
|
|
1670
1765
|
const fileDir = path.dirname(filePath);
|
|
1671
1766
|
|
|
1672
1767
|
// 确保父目录存在
|
|
@@ -1688,7 +1783,7 @@ ${content}
|
|
|
1688
1783
|
return {
|
|
1689
1784
|
success: true,
|
|
1690
1785
|
message: '技能创建成功',
|
|
1691
|
-
directory,
|
|
1786
|
+
directory: safeDirectory,
|
|
1692
1787
|
fileCount: files.length
|
|
1693
1788
|
};
|
|
1694
1789
|
}
|
|
@@ -1699,10 +1794,10 @@ ${content}
|
|
|
1699
1794
|
* @returns {Array<{path: string, size: number, isDirectory: boolean}>}
|
|
1700
1795
|
*/
|
|
1701
1796
|
getSkillFiles(directory) {
|
|
1702
|
-
const skillPath =
|
|
1797
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
1703
1798
|
|
|
1704
1799
|
if (!fs.existsSync(skillPath)) {
|
|
1705
|
-
throw new Error(`技能 "${
|
|
1800
|
+
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
1706
1801
|
}
|
|
1707
1802
|
|
|
1708
1803
|
const files = [];
|
|
@@ -1745,10 +1840,12 @@ ${content}
|
|
|
1745
1840
|
* @returns {Object} 文件内容
|
|
1746
1841
|
*/
|
|
1747
1842
|
getSkillFileContent(directory, filePath) {
|
|
1748
|
-
const
|
|
1843
|
+
const { path: skillPath } = this.resolveInstallPath(directory);
|
|
1844
|
+
const safeFilePath = normalizeSkillRelativePath(filePath, 'skill file path');
|
|
1845
|
+
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
1749
1846
|
|
|
1750
1847
|
if (!fs.existsSync(fullPath)) {
|
|
1751
|
-
throw new Error(`文件 "${
|
|
1848
|
+
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
1752
1849
|
}
|
|
1753
1850
|
|
|
1754
1851
|
const stats = fs.statSync(fullPath);
|
|
@@ -1784,15 +1881,19 @@ ${content}
|
|
|
1784
1881
|
* @param {Array<{path: string, content: string, isBase64?: boolean}>} files - 文件数组
|
|
1785
1882
|
*/
|
|
1786
1883
|
addSkillFiles(directory, files) {
|
|
1787
|
-
const skillPath =
|
|
1884
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
1885
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1886
|
+
throw new Error('不能修改 Codex 系统技能');
|
|
1887
|
+
}
|
|
1788
1888
|
|
|
1789
1889
|
if (!fs.existsSync(skillPath)) {
|
|
1790
|
-
throw new Error(`技能 "${
|
|
1890
|
+
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
1791
1891
|
}
|
|
1792
1892
|
|
|
1793
1893
|
const added = [];
|
|
1794
1894
|
for (const file of files) {
|
|
1795
|
-
const
|
|
1895
|
+
const safeFilePath = normalizeSkillRelativePath(file.path, 'skill file path');
|
|
1896
|
+
const filePath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
1796
1897
|
const fileDir = path.dirname(filePath);
|
|
1797
1898
|
|
|
1798
1899
|
// 确保父目录存在
|
|
@@ -1806,7 +1907,7 @@ ${content}
|
|
|
1806
1907
|
} else {
|
|
1807
1908
|
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
1808
1909
|
}
|
|
1809
|
-
added.push(
|
|
1910
|
+
added.push(safeFilePath);
|
|
1810
1911
|
}
|
|
1811
1912
|
|
|
1812
1913
|
this.clearCache({ removeFile: true });
|
|
@@ -1820,10 +1921,13 @@ ${content}
|
|
|
1820
1921
|
* @param {string} filePath - 文件相对路径
|
|
1821
1922
|
*/
|
|
1822
1923
|
deleteSkillFile(directory, filePath) {
|
|
1823
|
-
const skillPath =
|
|
1924
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
1925
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1926
|
+
throw new Error('不能修改 Codex 系统技能');
|
|
1927
|
+
}
|
|
1824
1928
|
|
|
1825
1929
|
if (!fs.existsSync(skillPath)) {
|
|
1826
|
-
throw new Error(`技能 "${
|
|
1930
|
+
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
1827
1931
|
}
|
|
1828
1932
|
|
|
1829
1933
|
// 不允许删除 SKILL.md
|
|
@@ -1831,10 +1935,14 @@ ${content}
|
|
|
1831
1935
|
throw new Error('不能删除 SKILL.md 文件');
|
|
1832
1936
|
}
|
|
1833
1937
|
|
|
1834
|
-
const
|
|
1938
|
+
const safeFilePath = normalizeSkillRelativePath(filePath, 'skill file path');
|
|
1939
|
+
if (safeFilePath === 'SKILL.md') {
|
|
1940
|
+
throw new Error('不能删除 SKILL.md 文件');
|
|
1941
|
+
}
|
|
1942
|
+
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
1835
1943
|
|
|
1836
1944
|
if (!fs.existsSync(fullPath)) {
|
|
1837
|
-
throw new Error(`文件 "${
|
|
1945
|
+
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
1838
1946
|
}
|
|
1839
1947
|
|
|
1840
1948
|
const stats = fs.statSync(fullPath);
|
|
@@ -1846,7 +1954,7 @@ ${content}
|
|
|
1846
1954
|
|
|
1847
1955
|
this.clearCache({ removeFile: true });
|
|
1848
1956
|
|
|
1849
|
-
return { success: true, deleted:
|
|
1957
|
+
return { success: true, deleted: safeFilePath };
|
|
1850
1958
|
}
|
|
1851
1959
|
|
|
1852
1960
|
/**
|
|
@@ -1857,16 +1965,20 @@ ${content}
|
|
|
1857
1965
|
* @param {boolean} isBase64 - 是否为 base64 编码
|
|
1858
1966
|
*/
|
|
1859
1967
|
updateSkillFile(directory, filePath, content, isBase64 = false) {
|
|
1860
|
-
const skillPath =
|
|
1968
|
+
const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
|
|
1969
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
1970
|
+
throw new Error('不能修改 Codex 系统技能');
|
|
1971
|
+
}
|
|
1861
1972
|
|
|
1862
1973
|
if (!fs.existsSync(skillPath)) {
|
|
1863
|
-
throw new Error(`技能 "${
|
|
1974
|
+
throw new Error(`技能 "${safeDirectory}" 不存在`);
|
|
1864
1975
|
}
|
|
1865
1976
|
|
|
1866
|
-
const
|
|
1977
|
+
const safeFilePath = normalizeSkillRelativePath(filePath, 'skill file path');
|
|
1978
|
+
const fullPath = resolveInsideRoot(skillPath, safeFilePath, 'skill file path', { allowHiddenSegments: true });
|
|
1867
1979
|
|
|
1868
1980
|
if (!fs.existsSync(fullPath)) {
|
|
1869
|
-
throw new Error(`文件 "${
|
|
1981
|
+
throw new Error(`文件 "${safeFilePath}" 不存在`);
|
|
1870
1982
|
}
|
|
1871
1983
|
|
|
1872
1984
|
if (isBase64) {
|
|
@@ -1877,7 +1989,7 @@ ${content}
|
|
|
1877
1989
|
|
|
1878
1990
|
this.clearCache({ removeFile: true });
|
|
1879
1991
|
|
|
1880
|
-
return { success: true, updated:
|
|
1992
|
+
return { success: true, updated: safeFilePath };
|
|
1881
1993
|
}
|
|
1882
1994
|
|
|
1883
1995
|
|
|
@@ -1885,11 +1997,14 @@ ${content}
|
|
|
1885
1997
|
* 安装 cc-tool 本地托管的技能(从 storageDir cp 到 installDir)
|
|
1886
1998
|
*/
|
|
1887
1999
|
installLocalSkill(directory) {
|
|
1888
|
-
const src =
|
|
1889
|
-
const dest =
|
|
2000
|
+
const { safeDirectory, path: src } = this.resolveStoragePath(directory);
|
|
2001
|
+
const { path: dest } = this.resolveInstallPath(safeDirectory);
|
|
2002
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2003
|
+
throw new Error('不能安装到 Codex 系统技能目录');
|
|
2004
|
+
}
|
|
1890
2005
|
|
|
1891
2006
|
if (!fs.existsSync(src)) {
|
|
1892
|
-
throw new Error(`本地技能 "${
|
|
2007
|
+
throw new Error(`本地技能 "${safeDirectory}" 不存在`);
|
|
1893
2008
|
}
|
|
1894
2009
|
|
|
1895
2010
|
if (fs.existsSync(dest)) {
|
|
@@ -1906,7 +2021,10 @@ ${content}
|
|
|
1906
2021
|
* 卸载技能
|
|
1907
2022
|
*/
|
|
1908
2023
|
uninstallSkill(directory) {
|
|
1909
|
-
const dest =
|
|
2024
|
+
const { safeDirectory, path: dest } = this.resolveInstallPath(directory);
|
|
2025
|
+
if (this.isProtectedSkillDirectory(safeDirectory)) {
|
|
2026
|
+
throw new Error('不能卸载 Codex 系统技能');
|
|
2027
|
+
}
|
|
1910
2028
|
|
|
1911
2029
|
if (fs.existsSync(dest)) {
|
|
1912
2030
|
fs.rmSync(dest, { recursive: true, force: true });
|
|
@@ -1921,8 +2039,9 @@ ${content}
|
|
|
1921
2039
|
* 获取技能详情(完整内容)
|
|
1922
2040
|
*/
|
|
1923
2041
|
async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '') {
|
|
2042
|
+
const safeDirectory = this.normalizeSkillDirectory(directory);
|
|
1924
2043
|
// 先检查本地是否安装
|
|
1925
|
-
const localPath = path.join(this.
|
|
2044
|
+
const localPath = path.join(this.resolveInstallPath(safeDirectory).path, 'SKILL.md');
|
|
1926
2045
|
|
|
1927
2046
|
if (fs.existsSync(localPath)) {
|
|
1928
2047
|
const content = fs.readFileSync(localPath, 'utf-8');
|
|
@@ -1933,13 +2052,14 @@ ${content}
|
|
|
1933
2052
|
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
1934
2053
|
|
|
1935
2054
|
return {
|
|
1936
|
-
directory,
|
|
1937
|
-
name: metadata.name ||
|
|
2055
|
+
directory: safeDirectory,
|
|
2056
|
+
name: metadata.name || safeDirectory,
|
|
1938
2057
|
description: metadata.description || '',
|
|
1939
2058
|
content: body,
|
|
1940
2059
|
fullContent: content,
|
|
1941
2060
|
installed: true,
|
|
1942
|
-
source: 'local'
|
|
2061
|
+
source: this.isProtectedSkillDirectory(safeDirectory) ? 'system-installed' : 'local',
|
|
2062
|
+
protected: this.isProtectedSkillDirectory(safeDirectory)
|
|
1943
2063
|
};
|
|
1944
2064
|
}
|
|
1945
2065
|
|
|
@@ -1949,8 +2069,8 @@ ${content}
|
|
|
1949
2069
|
const body = bodyMatch ? bodyMatch[1].trim() : content;
|
|
1950
2070
|
|
|
1951
2071
|
return {
|
|
1952
|
-
directory,
|
|
1953
|
-
name: metadata.name ||
|
|
2072
|
+
directory: safeDirectory,
|
|
2073
|
+
name: metadata.name || safeDirectory,
|
|
1954
2074
|
description: metadata.description || '',
|
|
1955
2075
|
content: body,
|
|
1956
2076
|
fullContent: content,
|
|
@@ -1973,7 +2093,7 @@ ${content}
|
|
|
1973
2093
|
const tryLoadRemoteDetailFromRepo = async (repo, extraCandidateDirs = []) => {
|
|
1974
2094
|
try {
|
|
1975
2095
|
if (repo.provider === 'local') {
|
|
1976
|
-
const normalizedDirectory = normalizeRepoPath(
|
|
2096
|
+
const normalizedDirectory = normalizeRepoPath(safeDirectory);
|
|
1977
2097
|
const candidateDirs = new Set([
|
|
1978
2098
|
normalizedDirectory,
|
|
1979
2099
|
normalizeRepoPath(fullDirectoryHint || ''),
|
|
@@ -1996,7 +2116,7 @@ ${content}
|
|
|
1996
2116
|
: await this.fetchGitHubRepoTree(repo);
|
|
1997
2117
|
if (!treeItems?.length) return null;
|
|
1998
2118
|
|
|
1999
|
-
|
|
2119
|
+
const normalizedDirectory = normalizeRepoPath(safeDirectory);
|
|
2000
2120
|
const candidateDirs = new Set();
|
|
2001
2121
|
candidateDirs.add(normalizedDirectory);
|
|
2002
2122
|
|
|
@@ -2037,7 +2157,7 @@ ${content}
|
|
|
2037
2157
|
const normalizedRepoHint = this.normalizeRepoConfig(repoHint);
|
|
2038
2158
|
const detail = await tryLoadRemoteDetailFromRepo(normalizedRepoHint, [
|
|
2039
2159
|
fullDirectoryHint || '',
|
|
2040
|
-
|
|
2160
|
+
repoHint.directory ? `${repoHint.directory}/${safeDirectory}` : '',
|
|
2041
2161
|
repoHint.fullDirectory || ''
|
|
2042
2162
|
]);
|
|
2043
2163
|
if (detail) return detail;
|
|
@@ -2048,7 +2168,7 @@ ${content}
|
|
|
2048
2168
|
|
|
2049
2169
|
// 先尝试使用缓存中的 repo 信息(最快)
|
|
2050
2170
|
const cachedSkill = this.skillsCache?.find(s =>
|
|
2051
|
-
normalizeRepoPath(s.directory) === normalizeRepoPath(
|
|
2171
|
+
normalizeRepoPath(s.directory) === normalizeRepoPath(safeDirectory)
|
|
2052
2172
|
);
|
|
2053
2173
|
if (cachedSkill && (cachedSkill.repoOwner || cachedSkill.repoProjectPath || cachedSkill.repoLocalPath)) {
|
|
2054
2174
|
const cachedRepo = this.normalizeRepoConfig({
|
|
@@ -2065,7 +2185,7 @@ ${content}
|
|
|
2065
2185
|
const detail = await tryLoadRemoteDetailFromRepo(cachedRepo, [
|
|
2066
2186
|
fullDirectoryHint || '',
|
|
2067
2187
|
cachedSkill.fullDirectory || '',
|
|
2068
|
-
cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${
|
|
2188
|
+
cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${safeDirectory}` : ''
|
|
2069
2189
|
]);
|
|
2070
2190
|
if (detail) return detail;
|
|
2071
2191
|
}
|
|
@@ -2075,7 +2195,7 @@ ${content}
|
|
|
2075
2195
|
for (const repo of repos) {
|
|
2076
2196
|
const detail = await tryLoadRemoteDetailFromRepo(
|
|
2077
2197
|
repo,
|
|
2078
|
-
[repo.directory ? `${repo.directory}/${
|
|
2198
|
+
[repo.directory ? `${repo.directory}/${safeDirectory}` : '']
|
|
2079
2199
|
);
|
|
2080
2200
|
if (detail) return detail;
|
|
2081
2201
|
}
|
|
@@ -2088,7 +2208,11 @@ ${content}
|
|
|
2088
2208
|
*/
|
|
2089
2209
|
getInstalledSkills() {
|
|
2090
2210
|
const skills = [];
|
|
2091
|
-
this.scanLocalDir(this.installDir, this.installDir, skills
|
|
2211
|
+
this.scanLocalDir(this.installDir, this.installDir, skills, {
|
|
2212
|
+
includeHiddenDirs: this.platform === 'codex',
|
|
2213
|
+
forceInstalled: true,
|
|
2214
|
+
source: 'native-installed'
|
|
2215
|
+
});
|
|
2092
2216
|
return skills;
|
|
2093
2217
|
}
|
|
2094
2218
|
}
|