coding-tool-x 3.6.7 → 3.6.9

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-BuBlsbcI.js} +1 -1
  2. package/dist/web/assets/{ConfigTemplates-CACLQb8e.js → ConfigTemplates-DQczxuOX.js} +1 -1
  3. package/dist/web/assets/Home-BvAzzWnB.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-C8gAlaps.js} +1 -1
  6. package/dist/web/assets/{ProjectList-DSkvQFEP.js → ProjectList-nqgi6_cR.js} +1 -1
  7. package/dist/web/assets/{SessionList-Czjs1GN1.js → SessionList-D8AxMYy6.js} +1 -1
  8. package/dist/web/assets/{SkillManager-Cg1xZ5Cv.js → SkillManager-BDbNbNkK.js} +1 -1
  9. package/dist/web/assets/{WorkspaceManager-COjYeKvN.js → WorkspaceManager-DGGaGW2c.js} +1 -1
  10. package/dist/web/assets/index-Dsh4Q5Cg.css +1 -0
  11. package/dist/web/assets/index-y-eEayxR.js +2 -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 +46 -69
  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 +1137 -86
  49. package/src/server/services/repo-scanner-base.js +6 -29
  50. package/src/server/services/skill-service.js +198 -65
  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,10 @@ ${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 localSkillDir = this.resolveInstallPath(safeDirectory).path;
2045
+ const localPath = path.join(localSkillDir, 'SKILL.md');
1926
2046
 
1927
2047
  if (fs.existsSync(localPath)) {
1928
2048
  const content = fs.readFileSync(localPath, 'utf-8');
@@ -1933,13 +2053,17 @@ ${content}
1933
2053
  const body = bodyMatch ? bodyMatch[1].trim() : content;
1934
2054
 
1935
2055
  return {
1936
- directory,
1937
- name: metadata.name || directory,
2056
+ directory: safeDirectory,
2057
+ name: metadata.name || safeDirectory,
1938
2058
  description: metadata.description || '',
1939
2059
  content: body,
1940
2060
  fullContent: content,
1941
2061
  installed: true,
1942
- source: 'local'
2062
+ path: localSkillDir,
2063
+ fullPath: localPath,
2064
+ installPath: localSkillDir,
2065
+ source: this.isProtectedSkillDirectory(safeDirectory) ? 'system-installed' : 'local',
2066
+ protected: this.isProtectedSkillDirectory(safeDirectory)
1943
2067
  };
1944
2068
  }
1945
2069
 
@@ -1949,8 +2073,8 @@ ${content}
1949
2073
  const body = bodyMatch ? bodyMatch[1].trim() : content;
1950
2074
 
1951
2075
  return {
1952
- directory,
1953
- name: metadata.name || directory,
2076
+ directory: safeDirectory,
2077
+ name: metadata.name || safeDirectory,
1954
2078
  description: metadata.description || '',
1955
2079
  content: body,
1956
2080
  fullContent: content,
@@ -1966,14 +2090,15 @@ ${content}
1966
2090
  repoProjectPath: repo.projectPath || null,
1967
2091
  repoLocalPath: repo.localPath || null,
1968
2092
  repoId: repo.id,
1969
- repoUrl: repo.repoUrl || buildRepoUrl(repo)
2093
+ repoUrl: repo.repoUrl || buildRepoUrl(repo),
2094
+ path: fullDirectory
1970
2095
  };
1971
2096
  };
1972
2097
 
1973
2098
  const tryLoadRemoteDetailFromRepo = async (repo, extraCandidateDirs = []) => {
1974
2099
  try {
1975
2100
  if (repo.provider === 'local') {
1976
- const normalizedDirectory = normalizeRepoPath(directory);
2101
+ const normalizedDirectory = normalizeRepoPath(safeDirectory);
1977
2102
  const candidateDirs = new Set([
1978
2103
  normalizedDirectory,
1979
2104
  normalizeRepoPath(fullDirectoryHint || ''),
@@ -1986,7 +2111,11 @@ ${content}
1986
2111
  : path.join(repo.localPath, 'SKILL.md');
1987
2112
  if (!fs.existsSync(skillMdPath)) continue;
1988
2113
  const content = fs.readFileSync(skillMdPath, 'utf-8');
1989
- return parseRemoteSkillContent(content, repo, candidateDir);
2114
+ return {
2115
+ ...parseRemoteSkillContent(content, repo, candidateDir),
2116
+ path: path.dirname(skillMdPath),
2117
+ fullPath: skillMdPath
2118
+ };
1990
2119
  }
1991
2120
  return null;
1992
2121
  }
@@ -1996,7 +2125,7 @@ ${content}
1996
2125
  : await this.fetchGitHubRepoTree(repo);
1997
2126
  if (!treeItems?.length) return null;
1998
2127
 
1999
- const normalizedDirectory = normalizeRepoPath(directory);
2128
+ const normalizedDirectory = normalizeRepoPath(safeDirectory);
2000
2129
  const candidateDirs = new Set();
2001
2130
  candidateDirs.add(normalizedDirectory);
2002
2131
 
@@ -2037,7 +2166,7 @@ ${content}
2037
2166
  const normalizedRepoHint = this.normalizeRepoConfig(repoHint);
2038
2167
  const detail = await tryLoadRemoteDetailFromRepo(normalizedRepoHint, [
2039
2168
  fullDirectoryHint || '',
2040
- repoHint.directory ? `${repoHint.directory}/${directory}` : '',
2169
+ repoHint.directory ? `${repoHint.directory}/${safeDirectory}` : '',
2041
2170
  repoHint.fullDirectory || ''
2042
2171
  ]);
2043
2172
  if (detail) return detail;
@@ -2048,7 +2177,7 @@ ${content}
2048
2177
 
2049
2178
  // 先尝试使用缓存中的 repo 信息(最快)
2050
2179
  const cachedSkill = this.skillsCache?.find(s =>
2051
- normalizeRepoPath(s.directory) === normalizeRepoPath(directory)
2180
+ normalizeRepoPath(s.directory) === normalizeRepoPath(safeDirectory)
2052
2181
  );
2053
2182
  if (cachedSkill && (cachedSkill.repoOwner || cachedSkill.repoProjectPath || cachedSkill.repoLocalPath)) {
2054
2183
  const cachedRepo = this.normalizeRepoConfig({
@@ -2065,7 +2194,7 @@ ${content}
2065
2194
  const detail = await tryLoadRemoteDetailFromRepo(cachedRepo, [
2066
2195
  fullDirectoryHint || '',
2067
2196
  cachedSkill.fullDirectory || '',
2068
- cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${directory}` : ''
2197
+ cachedSkill.repoDirectory ? `${cachedSkill.repoDirectory}/${safeDirectory}` : ''
2069
2198
  ]);
2070
2199
  if (detail) return detail;
2071
2200
  }
@@ -2075,7 +2204,7 @@ ${content}
2075
2204
  for (const repo of repos) {
2076
2205
  const detail = await tryLoadRemoteDetailFromRepo(
2077
2206
  repo,
2078
- [repo.directory ? `${repo.directory}/${directory}` : '']
2207
+ [repo.directory ? `${repo.directory}/${safeDirectory}` : '']
2079
2208
  );
2080
2209
  if (detail) return detail;
2081
2210
  }
@@ -2088,7 +2217,11 @@ ${content}
2088
2217
  */
2089
2218
  getInstalledSkills() {
2090
2219
  const skills = [];
2091
- this.scanLocalDir(this.installDir, this.installDir, skills);
2220
+ this.scanLocalDir(this.installDir, this.installDir, skills, {
2221
+ includeHiddenDirs: this.platform === 'codex',
2222
+ forceInstalled: true,
2223
+ source: 'native-installed'
2224
+ });
2092
2225
  return skills;
2093
2226
  }
2094
2227
  }