coding-tool-x 3.5.7 → 3.5.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 (60) hide show
  1. package/dist/web/assets/{Analytics-C6DEmD3D.js → Analytics-C5W3axXs.js} +2 -2
  2. package/dist/web/assets/Analytics-vQS5IWvs.css +1 -0
  3. package/dist/web/assets/{ConfigTemplates-Cf_iTpC4.js → ConfigTemplates-DzyVFDx9.js} +1 -1
  4. package/dist/web/assets/{Home-BtBmYLJ1.js → Home-C9TQNB6f.js} +1 -1
  5. package/dist/web/assets/Home-qzk118Of.css +1 -0
  6. package/dist/web/assets/{PluginManager-DEk8vSw5.js → PluginManager-9B_brLWT.js} +1 -1
  7. package/dist/web/assets/ProjectList-Bjt6mrsV.js +1 -0
  8. package/dist/web/assets/ProjectList-GCC2QOmq.css +1 -0
  9. package/dist/web/assets/SessionList-BsHPgmUR.css +1 -0
  10. package/dist/web/assets/SessionList-DcBH13uA.js +1 -0
  11. package/dist/web/assets/{SkillManager-DcZOiiSf.js → SkillManager-vST8DRRg.js} +1 -1
  12. package/dist/web/assets/{WorkspaceManager-BHqI8aGV.js → WorkspaceManager-ov1KgRXR.js} +1 -1
  13. package/dist/web/assets/icons-CEq2hYB-.js +1 -0
  14. package/dist/web/assets/index-Dih_bOsv.css +1 -0
  15. package/dist/web/assets/index-Duc7QP4e.js +2 -0
  16. package/dist/web/assets/{naive-ui-BaTCPPL5.js → naive-ui-Cg4_ZeoT.js} +1 -1
  17. package/dist/web/assets/{vendors-Fza9uSYn.js → vendors-Bsp-dq2d.js} +1 -1
  18. package/dist/web/assets/vue-vendor-BxIT0uQq.js +45 -0
  19. package/dist/web/index.html +7 -7
  20. package/package.json +1 -1
  21. package/src/commands/export-config.js +6 -6
  22. package/src/config/default.js +2 -6
  23. package/src/config/loader.js +2 -2
  24. package/src/config/paths.js +160 -33
  25. package/src/server/api/agents.js +52 -2
  26. package/src/server/api/codex-sessions.js +4 -2
  27. package/src/server/api/commands.js +38 -2
  28. package/src/server/api/opencode-sessions.js +4 -2
  29. package/src/server/api/plugins.js +104 -1
  30. package/src/server/api/sessions.js +9 -7
  31. package/src/server/services/agents-service.js +269 -62
  32. package/src/server/services/commands-service.js +281 -81
  33. package/src/server/services/config-export-service.js +7 -7
  34. package/src/server/services/config-registry-service.js +4 -5
  35. package/src/server/services/config-sync-manager.js +61 -41
  36. package/src/server/services/config-sync-service.js +3 -3
  37. package/src/server/services/gemini-channels.js +5 -5
  38. package/src/server/services/gemini-config.js +3 -4
  39. package/src/server/services/gemini-sessions.js +23 -20
  40. package/src/server/services/gemini-settings-manager.js +2 -3
  41. package/src/server/services/mcp-service.js +9 -14
  42. package/src/server/services/native-oauth-adapters.js +3 -3
  43. package/src/server/services/notification-hooks.js +3 -3
  44. package/src/server/services/opencode-sessions.js +16 -6
  45. package/src/server/services/opencode-settings-manager.js +3 -3
  46. package/src/server/services/plugins-service.js +499 -23
  47. package/src/server/services/prompts-service.js +5 -9
  48. package/src/server/services/session-launch-command.js +1 -24
  49. package/src/server/services/sessions.js +91 -40
  50. package/src/server/services/skill-service.js +155 -18
  51. package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
  52. package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
  53. package/dist/web/assets/ProjectList-BMVhA_Kh.js +0 -1
  54. package/dist/web/assets/ProjectList-DL4JK6ci.css +0 -1
  55. package/dist/web/assets/SessionList-B5ioAXxg.js +0 -1
  56. package/dist/web/assets/SessionList-B8dXVXfi.css +0 -1
  57. package/dist/web/assets/icons-CQuif85v.js +0 -1
  58. package/dist/web/assets/index-CtByKdkA.js +0 -2
  59. package/dist/web/assets/index-VGAxnLqi.css +0 -1
  60. package/dist/web/assets/vue-vendor-aWwwFAao.js +0 -45
@@ -1,28 +1,11 @@
1
- const { isWindowsLikePlatform } = require('../../utils/home-dir');
2
-
3
1
  function escapeForDoubleQuotes(value) {
4
2
  return String(value).replace(/"/g, '\\"');
5
3
  }
6
4
 
7
- function escapeForPowerShellSingleQuotes(value) {
8
- return String(value).replace(/'/g, "''");
9
- }
10
-
11
5
  function buildDisplayCommand(executable, args = []) {
12
6
  return [String(executable || ''), ...args.map(arg => String(arg))].filter(Boolean).join(' ').trim();
13
7
  }
14
8
 
15
- function buildWindowsCopyCommand(cwd, executable, args = []) {
16
- const quotedCwd = `'${escapeForPowerShellSingleQuotes(cwd)}'`;
17
- const quotedExecutable = `'${escapeForPowerShellSingleQuotes(executable)}'`;
18
- const quotedArgs = args.map(arg => `'${escapeForPowerShellSingleQuotes(arg)}'`).join(' ');
19
- const invokeCommand = quotedArgs
20
- ? `& ${quotedExecutable} ${quotedArgs}`
21
- : `& ${quotedExecutable}`;
22
-
23
- return `powershell -NoProfile -ExecutionPolicy Bypass -Command "& { Set-Location -LiteralPath ${quotedCwd}; ${invokeCommand} }"`;
24
- }
25
-
26
9
  function buildPosixCopyCommand(cwd, command) {
27
10
  const quotedCwd = `"${escapeForDoubleQuotes(cwd)}"`;
28
11
  return `cd ${quotedCwd} && ${command}`;
@@ -41,10 +24,6 @@ function buildCopyCommand({
41
24
  return resolvedCommand;
42
25
  }
43
26
 
44
- if (isWindowsLikePlatform(runtimePlatform, runtimeEnv)) {
45
- return buildWindowsCopyCommand(cwd, executable, args);
46
- }
47
-
48
27
  return buildPosixCopyCommand(cwd, resolvedCommand);
49
28
  }
50
29
 
@@ -74,8 +53,6 @@ module.exports = {
74
53
  _test: {
75
54
  buildDisplayCommand,
76
55
  buildCopyCommand,
77
- buildWindowsCopyCommand,
78
- buildPosixCopyCommand,
79
- escapeForPowerShellSingleQuotes
56
+ buildPosixCopyCommand
80
57
  }
81
58
  };
@@ -14,12 +14,35 @@ const { globalCache, CacheKeys } = require('./enhanced-cache');
14
14
  const { PATHS, NATIVE_PATHS } = require('../../config/paths');
15
15
 
16
16
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
17
- const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
- const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
17
+ const CODEX_PROJECTS_DIR = NATIVE_PATHS.codex.projects;
18
+ const GEMINI_PROJECTS_DIR = NATIVE_PATHS.gemini.projects;
19
19
  const PROJECT_PATH_CACHE_TTL_MS = 5 * 60 * 1000;
20
20
  const MAX_PROJECT_PATH_CACHE_ENTRIES = 500;
21
21
  let projectPathResolutionCache = new Map();
22
22
 
23
+ function getProjectsStatsCacheKey(config) {
24
+ return `${CacheKeys.PROJECTS}${config?.projectsDir || '__default__'}`;
25
+ }
26
+
27
+ function getProjectSessionsCacheKey(projectName) {
28
+ return `${CacheKeys.SESSIONS}${projectName}`;
29
+ }
30
+
31
+ function invalidateProjectStatsCache(config) {
32
+ invalidateProjectsCache(config);
33
+ globalCache.delete(getProjectsStatsCacheKey(config));
34
+ }
35
+
36
+ function invalidateProjectSessionCache(projectName) {
37
+ if (!projectName) return;
38
+ globalCache.delete(getProjectSessionsCacheKey(projectName));
39
+ }
40
+
41
+ function invalidateProjectCaches(config, projectName) {
42
+ invalidateProjectStatsCache(config);
43
+ invalidateProjectSessionCache(projectName);
44
+ }
45
+
23
46
  // Base directory for cc-tool data
24
47
  function getCcToolDir() {
25
48
  return PATHS.base;
@@ -313,6 +336,41 @@ function validateProjectPath(candidatePath) {
313
336
  return null;
314
337
  }
315
338
 
339
+ function scanSessionsDir(sessionsDir) {
340
+ if (!sessionsDir || !fs.existsSync(sessionsDir)) {
341
+ return [];
342
+ }
343
+
344
+ return fs.readdirSync(sessionsDir)
345
+ .filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-'))
346
+ .map((file) => {
347
+ const filePath = path.join(sessionsDir, file);
348
+ const stats = fs.statSync(filePath);
349
+ return {
350
+ sessionId: file.replace('.jsonl', ''),
351
+ filePath,
352
+ size: stats.size,
353
+ mtime: stats.mtime
354
+ };
355
+ });
356
+ }
357
+
358
+ function getProjectSessionsDir(config, projectName) {
359
+ return path.join(config.projectsDir, projectName);
360
+ }
361
+
362
+ function getProjectSessionFiles(config, projectName) {
363
+ return scanSessionsDir(getProjectSessionsDir(config, projectName)).sort((a, b) => b.mtime - a.mtime);
364
+ }
365
+
366
+ function resolveSessionFile(config, projectName, sessionId) {
367
+ const sessionFile = path.join(getProjectSessionsDir(config, projectName), `${sessionId}.jsonl`);
368
+ if (fs.existsSync(sessionFile)) {
369
+ return sessionFile;
370
+ }
371
+ return '';
372
+ }
373
+
316
374
  function tryResolvePathFromSessions(encodedName) {
317
375
  try {
318
376
  const projectDir = path.join(CLAUDE_PROJECTS_DIR, encodedName);
@@ -365,7 +423,7 @@ function extractCwdFromSessionHeader(sessionFile) {
365
423
  async function getProjectsWithStats(config, options = {}) {
366
424
  if (!options.force) {
367
425
  // Check enhanced cache first
368
- const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
426
+ const cacheKey = getProjectsStatsCacheKey(config);
369
427
  const enhancedCached = globalCache.get(cacheKey);
370
428
  if (enhancedCached) {
371
429
  return enhancedCached;
@@ -386,7 +444,7 @@ async function getProjectsWithStats(config, options = {}) {
386
444
  return [];
387
445
  }
388
446
  setCachedProjects(config, data);
389
- globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
447
+ globalCache.set(getProjectsStatsCacheKey(config), data, 300000);
390
448
  return data;
391
449
  } catch (err) {
392
450
  console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
@@ -418,14 +476,10 @@ async function buildProjectsWithStats(config) {
418
476
  let lastUsed = null;
419
477
 
420
478
  try {
421
- const files = await fs.promises.readdir(projectPath);
422
- const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
423
-
424
479
  const sessionChecks = await Promise.all(
425
- jsonlFiles.map(async (fileName) => {
426
- const filePath = path.join(projectPath, fileName);
427
- const stats = await fs.promises.stat(filePath);
428
- const hasMessages = await hasActualMessages(filePath, stats);
480
+ getProjectSessionFiles(config, projectName).map(async (session) => {
481
+ const stats = await fs.promises.stat(session.filePath);
482
+ const hasMessages = await hasActualMessages(session.filePath, stats);
429
483
  return hasMessages ? stats.mtime.getTime() : null;
430
484
  })
431
485
  );
@@ -469,10 +523,8 @@ function getProjectAndSessionCounts(config) {
469
523
  return;
470
524
  }
471
525
  projectCount += 1;
472
- const projectPath = path.join(projectsDir, entry.name);
473
526
  try {
474
- const files = fs.readdirSync(projectPath);
475
- sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
527
+ sessionCount += getProjectSessionFiles(config, entry.name).length;
476
528
  } catch (err) {
477
529
  // 忽略单个项目的读取错误
478
530
  }
@@ -575,14 +627,14 @@ function scanSessionFileForMessagesAsync(filePath) {
575
627
  // Get sessions for a project - async version
576
628
  async function getSessionsForProject(config, projectName) {
577
629
  // Check cache first
578
- const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
630
+ const cacheKey = getProjectSessionsCacheKey(projectName);
579
631
  const cached = globalCache.get(cacheKey);
580
632
  if (cached) {
581
633
  return cached;
582
634
  }
583
635
 
584
636
  const projectConfig = { ...config, currentProject: projectName };
585
- const sessions = getAllSessions(projectConfig);
637
+ const sessions = getProjectSessionFiles(projectConfig, projectName);
586
638
  const forkRelations = getForkRelations();
587
639
  const savedOrder = getSessionOrder(projectName);
588
640
 
@@ -627,8 +679,10 @@ async function getSessionsForProject(config, projectName) {
627
679
  }
628
680
 
629
681
  // Add remaining sessions (new ones not in saved order)
630
- ordered.push(...sessionMap.values());
631
- orderedSessions = ordered;
682
+ const newSessions = [...sessionMap.values()].sort((a, b) => {
683
+ return new Date(b.mtime).getTime() - new Date(a.mtime).getTime();
684
+ });
685
+ orderedSessions = [...newSessions, ...ordered];
632
686
  }
633
687
 
634
688
  const result = {
@@ -643,24 +697,22 @@ async function getSessionsForProject(config, projectName) {
643
697
 
644
698
  // Delete a session
645
699
  function deleteSession(config, projectName, sessionId) {
646
- const projectDir = path.join(config.projectsDir, projectName);
647
- const sessionFile = path.join(projectDir, sessionId + '.jsonl');
700
+ const sessionFile = resolveSessionFile(config, projectName, sessionId);
648
701
 
649
- if (!fs.existsSync(sessionFile)) {
702
+ if (!sessionFile || !fs.existsSync(sessionFile)) {
650
703
  throw new Error('Session not found');
651
704
  }
652
705
 
653
706
  fs.unlinkSync(sessionFile);
654
- invalidateProjectsCache(config);
707
+ invalidateProjectCaches(config, projectName);
655
708
  return { success: true };
656
709
  }
657
710
 
658
711
  // Fork a session
659
712
  function forkSession(config, projectName, sessionId) {
660
- const projectDir = path.join(config.projectsDir, projectName);
661
- const sessionFile = path.join(projectDir, sessionId + '.jsonl');
713
+ const sessionFile = resolveSessionFile(config, projectName, sessionId);
662
714
 
663
- if (!fs.existsSync(sessionFile)) {
715
+ if (!sessionFile || !fs.existsSync(sessionFile)) {
664
716
  throw new Error('Session not found');
665
717
  }
666
718
 
@@ -669,7 +721,7 @@ function forkSession(config, projectName, sessionId) {
669
721
 
670
722
  // Generate new session ID (UUID v4)
671
723
  const newSessionId = crypto.randomUUID();
672
- const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
724
+ const newSessionFile = path.join(path.dirname(sessionFile), newSessionId + '.jsonl');
673
725
 
674
726
  // Write to new file
675
727
  fs.writeFileSync(newSessionFile, content, 'utf8');
@@ -678,7 +730,13 @@ function forkSession(config, projectName, sessionId) {
678
730
  const forkRelations = getForkRelations();
679
731
  forkRelations[newSessionId] = sessionId;
680
732
  saveForkRelations(forkRelations);
681
- invalidateProjectsCache(config);
733
+
734
+ const savedOrder = getSessionOrder(projectName);
735
+ if (savedOrder.length > 0) {
736
+ saveSessionOrder(projectName, [newSessionId, ...savedOrder.filter(id => id !== newSessionId)]);
737
+ }
738
+
739
+ invalidateProjectCaches(config, projectName);
682
740
 
683
741
  return { newSessionId, forkedFrom: sessionId };
684
742
  }
@@ -720,6 +778,7 @@ function saveSessionOrder(projectName, order) {
720
778
  // Update order for this project
721
779
  allOrders[projectName] = order;
722
780
  fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
781
+ invalidateProjectSessionCache(projectName);
723
782
  }
724
783
 
725
784
  // Delete a project (remove the entire project directory)
@@ -739,7 +798,7 @@ function deleteProject(config, projectName) {
739
798
  saveProjectOrder(config, newOrder);
740
799
  }
741
800
 
742
- invalidateProjectsCache(config);
801
+ invalidateProjectCaches(config, projectName);
743
802
  clearProjectPathResolutionCache(projectName);
744
803
  return {
745
804
  success: true,
@@ -749,20 +808,12 @@ function deleteProject(config, projectName) {
749
808
 
750
809
  // Search sessions for keyword
751
810
  function searchSessions(config, projectName, keyword, contextLength = 15) {
752
- const projectDir = path.join(config.projectsDir, projectName);
753
-
754
- if (!fs.existsSync(projectDir)) {
755
- return [];
756
- }
757
-
758
811
  const results = [];
759
- const files = fs.readdirSync(projectDir);
760
- const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
761
812
  const aliases = loadAliases();
762
813
 
763
- for (const file of jsonlFiles) {
764
- const sessionId = file.replace('.jsonl', '');
765
- const filePath = path.join(projectDir, file);
814
+ for (const session of getProjectSessionFiles(config, projectName)) {
815
+ const sessionId = session.sessionId;
816
+ const filePath = session.filePath;
766
817
 
767
818
  // Skip sessions with no actual messages
768
819
  if (!hasActualMessages(filePath)) {
@@ -836,7 +887,7 @@ async function getRecentSessions(config, limit = 5) {
836
887
  // Collect all sessions from all projects
837
888
  projects.forEach(projectName => {
838
889
  const projectConfig = { ...config, currentProject: projectName };
839
- const sessions = getAllSessions(projectConfig);
890
+ const sessions = getProjectSessionFiles(projectConfig, projectName);
840
891
  const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
841
892
 
842
893
  sessions.forEach(session => {
@@ -18,7 +18,7 @@ const {
18
18
  parseSkillContent,
19
19
  } = require('./format-converter');
20
20
  const { maskToken } = require('./oauth-utils');
21
- const { NATIVE_PATHS, HOME_DIR, PATHS } = require('../../config/paths');
21
+ const { NATIVE_PATHS, HOME_DIR, PATHS, getNativePlatformSkillsDir } = require('../../config/paths');
22
22
 
23
23
  const SUPPORTED_PLATFORMS = ['claude', 'codex', 'gemini', 'opencode'];
24
24
  const SUPPORTED_REPO_PROVIDERS = ['github', 'gitlab', 'local'];
@@ -166,25 +166,25 @@ const DEFAULT_REPOS_BY_PLATFORM = {
166
166
 
167
167
  const PLATFORM_CONFIG = {
168
168
  claude: {
169
- installDir: path.join(HOME_DIR, '.claude', 'skills'),
169
+ installDir: getNativePlatformSkillsDir('claude'),
170
170
  storageDir: PATHS.localSkills.claude,
171
171
  reposFile: PATHS.skillRepos.claude,
172
172
  cacheFile: PATHS.skillCaches.claude
173
173
  },
174
174
  codex: {
175
- installDir: path.join(HOME_DIR, '.codex', 'skills'),
175
+ installDir: getNativePlatformSkillsDir('codex'),
176
176
  storageDir: PATHS.localSkills.codex,
177
177
  reposFile: PATHS.skillRepos.codex,
178
178
  cacheFile: PATHS.skillCaches.codex
179
179
  },
180
180
  gemini: {
181
- installDir: path.join(HOME_DIR, '.gemini', 'skills'),
181
+ installDir: getNativePlatformSkillsDir('gemini'),
182
182
  storageDir: PATHS.localSkills.gemini,
183
183
  reposFile: PATHS.skillRepos.gemini,
184
184
  cacheFile: PATHS.skillCaches.gemini
185
185
  },
186
186
  opencode: {
187
- installDir: path.join(NATIVE_PATHS.opencode.config, 'skills'),
187
+ installDir: getNativePlatformSkillsDir('opencode'),
188
188
  storageDir: PATHS.localSkills.opencode,
189
189
  reposFile: PATHS.skillRepos.opencode,
190
190
  cacheFile: PATHS.skillCaches.opencode
@@ -248,11 +248,12 @@ class SkillService {
248
248
  }
249
249
  }
250
250
 
251
- prepareSkills(skills = []) {
251
+ prepareSkills(skills = [], options = {}) {
252
252
  const preparedSkills = Array.isArray(skills)
253
253
  ? skills.map(skill => ({ ...skill }))
254
254
  : [];
255
255
 
256
+ this.mergeInstalledSkills(preparedSkills, options);
256
257
  this.mergeLocalSkills(preparedSkills);
257
258
  this.deduplicateSkills(preparedSkills);
258
259
  preparedSkills.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
@@ -537,6 +538,8 @@ class SkillService {
537
538
  * 获取所有技能列表(带缓存)
538
539
  */
539
540
  async listSkills(forceRefresh = false) {
541
+ const prepareOptions = { syncManagedLocalSkills: forceRefresh };
542
+
540
543
  // 强制刷新时仅清空内存缓存,保留磁盘缓存作为回退来源
541
544
  if (forceRefresh) {
542
545
  this.clearCache();
@@ -547,11 +550,11 @@ class SkillService {
547
550
  // 检查内存缓存
548
551
  if (!forceRefresh && Array.isArray(this.skillsCache) && this.skillsCache.length > 0) {
549
552
  if (Array.isArray(fileCache) && fileCache.length > this.skillsCache.length) {
550
- this.skillsCache = this.prepareSkills(fileCache);
553
+ this.skillsCache = this.prepareSkills(fileCache, prepareOptions);
551
554
  this.cacheTime = Date.now();
552
555
  return this.skillsCache;
553
556
  }
554
- this.skillsCache = this.prepareSkills(this.skillsCache);
557
+ this.skillsCache = this.prepareSkills(this.skillsCache, prepareOptions);
555
558
  this.cacheTime = Date.now();
556
559
  return this.skillsCache;
557
560
  }
@@ -559,7 +562,7 @@ class SkillService {
559
562
  // 检查文件缓存
560
563
  if (!forceRefresh) {
561
564
  if (fileCache && fileCache.length > 0) {
562
- this.skillsCache = this.prepareSkills(fileCache);
565
+ this.skillsCache = this.prepareSkills(fileCache, prepareOptions);
563
566
  this.cacheTime = Date.now();
564
567
  return this.skillsCache;
565
568
  }
@@ -600,10 +603,10 @@ class SkillService {
600
603
  }
601
604
  }
602
605
 
603
- const preparedSkills = this.prepareSkills(skills);
606
+ const preparedSkills = this.prepareSkills(skills, prepareOptions);
604
607
 
605
608
  const hasUsableFileCache = Array.isArray(fileCache) && fileCache.length > 0;
606
- const preparedFileCache = hasUsableFileCache ? this.prepareSkills(fileCache) : null;
609
+ const preparedFileCache = hasUsableFileCache ? this.prepareSkills(fileCache, prepareOptions) : null;
607
610
  const shouldUseStaleFileCache = hasUsableFileCache && (
608
611
  (enabledRemoteRepos.length > 0 && remoteFailureCount === enabledRemoteRepos.length) ||
609
612
  (remoteFailureCount > 0 && preparedFileCache.length > preparedSkills.length)
@@ -1347,6 +1350,75 @@ class SkillService {
1347
1350
  this.scanLocalDir(this.storageDir, this.storageDir, skills);
1348
1351
  }
1349
1352
 
1353
+ /**
1354
+ * 合并平台原生安装目录中的技能,补齐“已安装但未被仓库/本地托管覆盖”的条目
1355
+ */
1356
+ mergeInstalledSkills(skills, options = {}) {
1357
+ if (!fs.existsSync(this.installDir)) return;
1358
+
1359
+ const syncManagedLocalSkills = options.syncManagedLocalSkills === true;
1360
+ const installedSkills = [];
1361
+ this.scanInstalledDir(this.installDir, this.installDir, installedSkills);
1362
+
1363
+ for (const installedSkill of installedSkills) {
1364
+ const normalizedDirectory = normalizeRepoPath(installedSkill.directory).toLowerCase();
1365
+ const existing = skills.find(skill =>
1366
+ normalizeRepoPath(skill.directory).toLowerCase() === normalizedDirectory
1367
+ );
1368
+
1369
+ if (existing) {
1370
+ const managedSkillMdPath = path.join(this.storageDir, installedSkill.directory, 'SKILL.md');
1371
+ if (syncManagedLocalSkills && fs.existsSync(managedSkillMdPath)) {
1372
+ const installedSkillPath = path.join(this.installDir, installedSkill.directory);
1373
+ this.ensureManagedSkillCopy(installedSkill.directory, installedSkillPath, { overwrite: true });
1374
+ }
1375
+ existing.installed = true;
1376
+ if (!existing.name && installedSkill.name) {
1377
+ existing.name = installedSkill.name;
1378
+ }
1379
+ if (!existing.description && installedSkill.description) {
1380
+ existing.description = installedSkill.description;
1381
+ }
1382
+ if (!existing.license && installedSkill.license) {
1383
+ existing.license = installedSkill.license;
1384
+ }
1385
+ continue;
1386
+ }
1387
+
1388
+ const installedSkillPath = path.join(this.installDir, installedSkill.directory);
1389
+ this.ensureManagedSkillCopy(installedSkill.directory, installedSkillPath, {
1390
+ overwrite: syncManagedLocalSkills
1391
+ });
1392
+ skills.push(installedSkill);
1393
+ }
1394
+ }
1395
+
1396
+ ensureManagedSkillCopy(directory, sourceDir, options = {}) {
1397
+ const normalizedDirectory = normalizeRepoPath(directory);
1398
+ const overwrite = options.overwrite === true;
1399
+ if (!normalizedDirectory || !sourceDir || !fs.existsSync(sourceDir)) {
1400
+ return false;
1401
+ }
1402
+
1403
+ const sourceSkillMdPath = path.join(sourceDir, 'SKILL.md');
1404
+ if (!fs.existsSync(sourceSkillMdPath)) {
1405
+ return false;
1406
+ }
1407
+
1408
+ const managedDir = path.join(this.storageDir, normalizedDirectory);
1409
+ const managedSkillMdPath = path.join(managedDir, 'SKILL.md');
1410
+ if (fs.existsSync(managedSkillMdPath)) {
1411
+ if (!overwrite) {
1412
+ return false;
1413
+ }
1414
+ fs.rmSync(managedDir, { recursive: true, force: true });
1415
+ }
1416
+
1417
+ fs.mkdirSync(managedDir, { recursive: true });
1418
+ this.copyDirRecursive(sourceDir, managedDir);
1419
+ return true;
1420
+ }
1421
+
1350
1422
  /**
1351
1423
  * 递归扫描本地目录
1352
1424
  */
@@ -1410,6 +1482,54 @@ class SkillService {
1410
1482
  }
1411
1483
  }
1412
1484
 
1485
+ /**
1486
+ * 递归扫描平台原生安装目录
1487
+ */
1488
+ scanInstalledDir(currentDir, baseDir, skills) {
1489
+ const skillMdPath = path.join(currentDir, 'SKILL.md');
1490
+
1491
+ if (fs.existsSync(skillMdPath)) {
1492
+ const directory = currentDir === baseDir
1493
+ ? path.basename(currentDir)
1494
+ : path.relative(baseDir, currentDir);
1495
+
1496
+ try {
1497
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
1498
+ const metadata = this.parseSkillMd(content);
1499
+
1500
+ skills.push({
1501
+ key: `installed:${directory}`,
1502
+ name: metadata.name || directory,
1503
+ description: metadata.description || '',
1504
+ directory,
1505
+ installed: true,
1506
+ isLocal: false,
1507
+ readmeUrl: null,
1508
+ repoOwner: null,
1509
+ repoName: null,
1510
+ repoBranch: null,
1511
+ license: metadata.license,
1512
+ source: 'native-installed'
1513
+ });
1514
+ } catch (err) {
1515
+ console.warn(`[SkillService] Parse installed skill ${directory} error:`, err.message);
1516
+ }
1517
+
1518
+ return;
1519
+ }
1520
+
1521
+ try {
1522
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
1523
+ for (const entry of entries) {
1524
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
1525
+ this.scanInstalledDir(path.join(currentDir, entry.name), baseDir, skills);
1526
+ }
1527
+ }
1528
+ } catch (err) {
1529
+ // 忽略读取错误
1530
+ }
1531
+ }
1532
+
1413
1533
  /**
1414
1534
  * 去重技能列表
1415
1535
  */
@@ -1921,11 +2041,8 @@ ${content}
1921
2041
  * 获取技能详情(完整内容)
1922
2042
  */
1923
2043
  async getSkillDetail(directory, repoHint = null, fullDirectoryHint = '') {
1924
- // 先检查本地是否安装
1925
- const localPath = path.join(this.installDir, directory, 'SKILL.md');
1926
-
1927
- if (fs.existsSync(localPath)) {
1928
- const content = fs.readFileSync(localPath, 'utf-8');
2044
+ const parseLocalSkillDetail = (skillMdPath, { installed, isLocal }) => {
2045
+ const content = fs.readFileSync(skillMdPath, 'utf-8');
1929
2046
  const metadata = this.parseSkillMd(content);
1930
2047
 
1931
2048
  // 提取正文内容(去除 frontmatter)
@@ -1938,9 +2055,29 @@ ${content}
1938
2055
  description: metadata.description || '',
1939
2056
  content: body,
1940
2057
  fullContent: content,
1941
- installed: true,
2058
+ installed,
2059
+ isLocal,
1942
2060
  source: 'local'
1943
2061
  };
2062
+ };
2063
+
2064
+ // 先检查本地是否安装
2065
+ const localPath = path.join(this.installDir, directory, 'SKILL.md');
2066
+
2067
+ if (fs.existsSync(localPath)) {
2068
+ const managedSkillMdPath = path.join(this.storageDir, directory, 'SKILL.md');
2069
+ return parseLocalSkillDetail(localPath, {
2070
+ installed: true,
2071
+ isLocal: fs.existsSync(managedSkillMdPath)
2072
+ });
2073
+ }
2074
+
2075
+ const managedLocalPath = path.join(this.storageDir, directory, 'SKILL.md');
2076
+ if (fs.existsSync(managedLocalPath)) {
2077
+ return parseLocalSkillDetail(managedLocalPath, {
2078
+ installed: false,
2079
+ isLocal: true
2080
+ });
1944
2081
  }
1945
2082
 
1946
2083
  const parseRemoteSkillContent = (content, repo, fullDirectory = '') => {
@@ -2088,7 +2225,7 @@ ${content}
2088
2225
  */
2089
2226
  getInstalledSkills() {
2090
2227
  const skills = [];
2091
- this.scanLocalDir(this.installDir, this.installDir, skills);
2228
+ this.scanInstalledDir(this.installDir, this.installDir, skills);
2092
2229
  return skills;
2093
2230
  }
2094
2231
  }
@@ -1 +0,0 @@
1
- .analytics-page[data-v-34f16206]{height:100%;background:var(--bg-primary);overflow-y:auto;padding:16px 20px;box-sizing:border-box;display:flex;flex-direction:column;gap:14px}.analytics-header[data-v-34f16206]{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;flex-wrap:wrap;gap:10px}.page-title[data-v-34f16206]{margin:0;font-size:18px;font-weight:700;color:var(--text-primary)}.toolbar[data-v-34f16206]{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.range-buttons[data-v-34f16206]{display:flex;align-items:center;gap:4px}.range-btn[data-v-34f16206]{padding:4px 10px;font-size:12px;border:1px solid var(--border-color, #e0e0e6);background:transparent;color:var(--text-secondary);border-radius:4px;cursor:pointer;transition:all .15s}.range-btn[data-v-34f16206]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.range-btn.active[data-v-34f16206]{background:var(--primary-color, #18a058);border-color:var(--primary-color, #18a058);color:#fff}.custom-range-wrapper[data-v-34f16206]{margin-left:4px}.loading-overlay[data-v-34f16206]{display:flex;justify-content:center;padding:20px 0}.summary-cards[data-v-34f16206]{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;flex-shrink:0}.summary-card[data-v-34f16206]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px}.summary-label[data-v-34f16206]{font-size:11px;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.05em}.summary-value[data-v-34f16206]{font-size:22px;font-weight:700;color:var(--text-primary);line-height:1.2}.chart-section[data-v-34f16206]{background:var(--bg-secondary);border:1px solid var(--border-color, #e0e0e6);border-radius:8px;padding:14px 16px;flex-shrink:0}.chart-section.fullscreen[data-v-34f16206]{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;border-radius:0;padding:20px;background:var(--bg-secondary);overflow:auto}.cumulative-section[data-v-34f16206]{margin-bottom:8px}.chart-section-header[data-v-34f16206]{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.chart-title[data-v-34f16206]{font-size:13px;font-weight:600;color:var(--text-primary)}.chart-controls[data-v-34f16206]{display:flex;align-items:center;gap:8px}.chart-type-toggle[data-v-34f16206]{display:flex;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;overflow:hidden}.toggle-btn[data-v-34f16206]{padding:3px 10px;font-size:12px;border:none;background:transparent;color:var(--text-secondary);cursor:pointer;transition:all .15s}.toggle-btn.active[data-v-34f16206]{background:var(--primary-color, #18a058);color:#fff}.icon-btn[data-v-34f16206]{display:flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid var(--border-color, #e0e0e6);border-radius:4px;background:transparent;cursor:pointer;color:var(--text-secondary);transition:all .15s}.icon-btn[data-v-34f16206]:hover{border-color:var(--primary-color, #18a058);color:var(--primary-color, #18a058)}.main-chart[data-v-34f16206]{height:560px;width:100%}.chart-section.fullscreen .main-chart[data-v-34f16206]{height:calc(100vh - 100px)}.cumulative-chart[data-v-34f16206]{height:200px;width:100%}.empty-state[data-v-34f16206]{height:120px;display:flex;align-items:center;justify-content:center;color:var(--text-secondary);font-size:13px}@media (max-width: 768px){.analytics-page[data-v-34f16206]{padding:10px 12px}.summary-cards[data-v-34f16206]{grid-template-columns:repeat(2,1fr)}.analytics-header[data-v-34f16206]{flex-direction:column;align-items:flex-start}.toolbar[data-v-34f16206]{width:100%}}@media (max-width: 480px){.summary-cards[data-v-34f16206]{grid-template-columns:1fr 1fr}.summary-value[data-v-34f16206]{font-size:18px}}