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.
- package/dist/web/assets/{Analytics-C6DEmD3D.js → Analytics-C5W3axXs.js} +2 -2
- package/dist/web/assets/Analytics-vQS5IWvs.css +1 -0
- package/dist/web/assets/{ConfigTemplates-Cf_iTpC4.js → ConfigTemplates-DzyVFDx9.js} +1 -1
- package/dist/web/assets/{Home-BtBmYLJ1.js → Home-C9TQNB6f.js} +1 -1
- package/dist/web/assets/Home-qzk118Of.css +1 -0
- package/dist/web/assets/{PluginManager-DEk8vSw5.js → PluginManager-9B_brLWT.js} +1 -1
- package/dist/web/assets/ProjectList-Bjt6mrsV.js +1 -0
- package/dist/web/assets/ProjectList-GCC2QOmq.css +1 -0
- package/dist/web/assets/SessionList-BsHPgmUR.css +1 -0
- package/dist/web/assets/SessionList-DcBH13uA.js +1 -0
- package/dist/web/assets/{SkillManager-DcZOiiSf.js → SkillManager-vST8DRRg.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-BHqI8aGV.js → WorkspaceManager-ov1KgRXR.js} +1 -1
- package/dist/web/assets/icons-CEq2hYB-.js +1 -0
- package/dist/web/assets/index-Dih_bOsv.css +1 -0
- package/dist/web/assets/index-Duc7QP4e.js +2 -0
- package/dist/web/assets/{naive-ui-BaTCPPL5.js → naive-ui-Cg4_ZeoT.js} +1 -1
- package/dist/web/assets/{vendors-Fza9uSYn.js → vendors-Bsp-dq2d.js} +1 -1
- package/dist/web/assets/vue-vendor-BxIT0uQq.js +45 -0
- package/dist/web/index.html +7 -7
- package/package.json +1 -1
- package/src/commands/export-config.js +6 -6
- package/src/config/default.js +2 -6
- package/src/config/loader.js +2 -2
- package/src/config/paths.js +160 -33
- package/src/server/api/agents.js +52 -2
- package/src/server/api/codex-sessions.js +4 -2
- package/src/server/api/commands.js +38 -2
- package/src/server/api/opencode-sessions.js +4 -2
- package/src/server/api/plugins.js +104 -1
- package/src/server/api/sessions.js +9 -7
- package/src/server/services/agents-service.js +269 -62
- package/src/server/services/commands-service.js +281 -81
- package/src/server/services/config-export-service.js +7 -7
- package/src/server/services/config-registry-service.js +4 -5
- package/src/server/services/config-sync-manager.js +61 -41
- package/src/server/services/config-sync-service.js +3 -3
- package/src/server/services/gemini-channels.js +5 -5
- package/src/server/services/gemini-config.js +3 -4
- package/src/server/services/gemini-sessions.js +23 -20
- package/src/server/services/gemini-settings-manager.js +2 -3
- package/src/server/services/mcp-service.js +9 -14
- package/src/server/services/native-oauth-adapters.js +3 -3
- package/src/server/services/notification-hooks.js +3 -3
- package/src/server/services/opencode-sessions.js +16 -6
- package/src/server/services/opencode-settings-manager.js +3 -3
- package/src/server/services/plugins-service.js +499 -23
- package/src/server/services/prompts-service.js +5 -9
- package/src/server/services/session-launch-command.js +1 -24
- package/src/server/services/sessions.js +91 -40
- package/src/server/services/skill-service.js +155 -18
- package/dist/web/assets/Analytics-RNn1BUbG.css +0 -1
- package/dist/web/assets/Home-BQxQ1LhR.css +0 -1
- package/dist/web/assets/ProjectList-BMVhA_Kh.js +0 -1
- package/dist/web/assets/ProjectList-DL4JK6ci.css +0 -1
- package/dist/web/assets/SessionList-B5ioAXxg.js +0 -1
- package/dist/web/assets/SessionList-B8dXVXfi.css +0 -1
- package/dist/web/assets/icons-CQuif85v.js +0 -1
- package/dist/web/assets/index-CtByKdkA.js +0 -2
- package/dist/web/assets/index-VGAxnLqi.css +0 -1
- 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
|
-
|
|
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 =
|
|
18
|
-
const GEMINI_PROJECTS_DIR =
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
426
|
-
const
|
|
427
|
-
const
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
631
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
764
|
-
const sessionId =
|
|
765
|
-
const filePath =
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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}}
|