coding-tool-x 3.6.7 → 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.
Files changed (54) hide show
  1. package/dist/web/assets/{Analytics-CKycBI0G.js → Analytics-C57ZRDq5.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-B3qqqL05.js} +1 -1
  3. package/dist/web/assets/Home-CG5XrTNc.js +1 -0
  4. package/dist/web/assets/Home-CIua-b-o.css +1 -0
  5. package/dist/web/assets/{PluginManager-CQJl_uso.js → PluginManager-BhX0LUzr.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-BQ01OZn9.js} +1 -1
  7. package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-DPv2IOXG.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-D5CfQm2O.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-Ce8ggteK.js} +1 -1
  10. package/dist/web/assets/index-CpnFNWqC.js +2 -0
  11. package/dist/web/assets/index-DJ9VFidd.css +1 -0
  12. package/dist/web/assets/{naive-ui-DM897mIP.js → naive-ui-kcWDeJwl.js} +1 -1
  13. package/dist/web/index.html +3 -3
  14. package/package.json +1 -1
  15. package/src/commands/stats.js +1 -1
  16. package/src/config/channel-preset-websites.js +2 -1
  17. package/src/config/paths.js +8 -0
  18. package/src/server/api/agents.js +16 -54
  19. package/src/server/api/claude-hooks.js +1 -1
  20. package/src/server/api/claude-statistics.js +57 -0
  21. package/src/server/api/commands.js +16 -54
  22. package/src/server/api/dashboard.js +2 -2
  23. package/src/server/api/gemini-channels.js +2 -0
  24. package/src/server/api/hooks.js +4 -1
  25. package/src/server/api/plugins.js +40 -65
  26. package/src/server/api/skills.js +21 -77
  27. package/src/server/api/validation-errors.js +34 -0
  28. package/src/server/gemini-proxy-server.js +205 -21
  29. package/src/server/index.js +1 -0
  30. package/src/server/services/agents-service.js +166 -75
  31. package/src/server/services/base/base-channel-service.js +15 -0
  32. package/src/server/services/base/response-usage-parser.js +44 -0
  33. package/src/server/services/channel-balance.js +86 -1
  34. package/src/server/services/claude-statistics-service.js +139 -0
  35. package/src/server/services/commands-service.js +203 -121
  36. package/src/server/services/config-artifact-paths.js +116 -0
  37. package/src/server/services/config-export-service.js +7 -6
  38. package/src/server/services/config-registry-service.js +2 -2
  39. package/src/server/services/config-sync-manager.js +45 -11
  40. package/src/server/services/config-templates-service.js +21 -19
  41. package/src/server/services/format-converter.js +42 -4
  42. package/src/server/services/gemini-channels.js +30 -1
  43. package/src/server/services/gemini-sessions.js +347 -61
  44. package/src/server/services/mcp-client.js +236 -49
  45. package/src/server/services/mcp-service.js +51 -64
  46. package/src/server/services/notification-hooks.js +659 -88
  47. package/src/server/services/opencode-channels.js +15 -0
  48. package/src/server/services/plugins-service.js +771 -63
  49. package/src/server/services/repo-scanner-base.js +6 -29
  50. package/src/server/services/skill-service.js +187 -63
  51. package/dist/web/assets/Home-BeGkC4bb.css +0 -1
  52. package/dist/web/assets/Home-Bk6sP5Ed.js +0 -1
  53. package/dist/web/assets/index-0isNzXNY.css +0 -1
  54. package/dist/web/assets/index-fWgvlZQx.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 dest = path.join(this.installDir, targetName);
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 scanRoot = repo.directory
787
- ? path.join(repoRoot, repo.directory)
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 localFilePath = path.join(repo.localPath, file.path);
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
- const skillPath = path.join(this.installDir, directory);
1336
- const skillMdPath = path.join(skillPath, 'SKILL.md');
1337
- return fs.existsSync(skillMdPath);
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
- const isInstalled = fs.existsSync(path.join(this.installDir, directory, 'SKILL.md'));
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
- existing.isLocal = true;
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
- skills.push({
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: true,
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 = path.join(this.installDir, directory);
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 || directory;
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 = sourcePath
1461
- ? path.join(normalizedRepo.localPath, sourcePath)
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 = path.join(repoDir, sourcePath);
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 = path.join(this.storageDir, directory);
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 = path.join(this.storageDir, directory);
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 hasSkillMd = files.some(f =>
1658
- f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md')
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 files) {
1669
- const filePath = path.join(dest, file.path);
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 = path.join(this.installDir, directory);
1797
+ const { safeDirectory, path: skillPath } = this.resolveInstallPath(directory);
1703
1798
 
1704
1799
  if (!fs.existsSync(skillPath)) {
1705
- throw new Error(`技能 "${directory}" 不存在`);
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 fullPath = path.join(this.installDir, directory, filePath);
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(`文件 "${filePath}" 不存在`);
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 = path.join(this.installDir, directory);
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(`技能 "${directory}" 不存在`);
1890
+ throw new Error(`技能 "${safeDirectory}" 不存在`);
1791
1891
  }
1792
1892
 
1793
1893
  const added = [];
1794
1894
  for (const file of files) {
1795
- const filePath = path.join(skillPath, file.path);
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(file.path);
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 = path.join(this.installDir, directory);
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(`技能 "${directory}" 不存在`);
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 fullPath = path.join(skillPath, filePath);
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(`文件 "${filePath}" 不存在`);
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: filePath };
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 = path.join(this.installDir, directory);
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(`技能 "${directory}" 不存在`);
1974
+ throw new Error(`技能 "${safeDirectory}" 不存在`);
1864
1975
  }
1865
1976
 
1866
- const fullPath = path.join(skillPath, filePath);
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(`文件 "${filePath}" 不存在`);
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: filePath };
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 = path.join(this.storageDir, directory);
1889
- const dest = path.join(this.installDir, directory);
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(`本地技能 "${directory}" 不存在`);
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 = path.join(this.installDir, directory);
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.installDir, directory, 'SKILL.md');
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 || directory,
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 || directory,
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(directory);
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
- const normalizedDirectory = normalizeRepoPath(directory);
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
- repoHint.directory ? `${repoHint.directory}/${directory}` : '',
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(directory)
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}/${directory}` : ''
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}/${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
  }