@adversity/coding-tool-x 2.4.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/web/assets/icons-BkBtk3H1.js +1 -0
- package/dist/web/assets/index-Cgq2DyzS.css +41 -0
- package/dist/web/assets/index-DOsR4qc6.js +14 -0
- package/dist/web/assets/naive-ui-D-gb0WfN.js +1 -0
- package/dist/web/assets/vendors-Bd5vxA1-.js +7 -0
- package/dist/web/assets/vue-vendor-hRp8vsrL.js +44 -0
- package/dist/web/index.html +6 -2
- package/package.json +1 -1
- package/src/commands/export-config.js +205 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/projects.js +2 -2
- package/src/server/api/sessions.js +70 -70
- package/src/server/api/skills.js +206 -0
- package/src/server/api/terminal.js +26 -0
- package/src/server/api/workspaces.js +2 -2
- package/src/server/index.js +5 -2
- package/src/server/services/config-export-service.js +229 -23
- package/src/server/services/config-sync-service.js +515 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/pty-manager.js +35 -1
- package/src/server/services/sessions.js +133 -46
- package/src/server/services/skill-service.js +252 -2
- package/src/server/services/terminal-commands.js +5 -0
- package/src/server/services/workspace-service.js +90 -51
- package/src/server/websocket-server.js +4 -1
- package/dist/web/assets/index-Bu1oPcKu.js +0 -4009
- package/dist/web/assets/index-XSok7-mN.css +0 -41
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const fs = require('fs');
|
|
8
9
|
|
|
9
10
|
// 尝试加载 node-pty,如果失败则提示
|
|
10
11
|
let pty = null;
|
|
@@ -40,7 +41,22 @@ class PtyManager {
|
|
|
40
41
|
if (process.platform === 'win32') {
|
|
41
42
|
return process.env.COMSPEC || 'cmd.exe';
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
|
|
45
|
+
// 优先使用环境变量指定的 shell
|
|
46
|
+
if (process.env.SHELL && fs.existsSync(process.env.SHELL)) {
|
|
47
|
+
return process.env.SHELL;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 回退到常见的 shell,按优先级检查
|
|
51
|
+
const commonShells = ['/bin/bash', '/bin/sh', '/usr/bin/bash', '/usr/bin/sh'];
|
|
52
|
+
for (const shell of commonShells) {
|
|
53
|
+
if (fs.existsSync(shell)) {
|
|
54
|
+
return shell;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 最后回退
|
|
59
|
+
return '/bin/sh';
|
|
44
60
|
}
|
|
45
61
|
|
|
46
62
|
/**
|
|
@@ -78,6 +94,9 @@ class PtyManager {
|
|
|
78
94
|
// 检查 PTY 是否可用
|
|
79
95
|
if (!pty) {
|
|
80
96
|
const errMsg = this.getPtyError() || 'node-pty is not available';
|
|
97
|
+
console.error('[PTY] Cannot create terminal:', errMsg);
|
|
98
|
+
console.error('[PTY] Node version:', process.version);
|
|
99
|
+
console.error('[PTY] Platform:', process.platform);
|
|
81
100
|
throw new Error(`Cannot create terminal: ${errMsg}`);
|
|
82
101
|
}
|
|
83
102
|
|
|
@@ -94,6 +113,21 @@ class PtyManager {
|
|
|
94
113
|
startCommand = null
|
|
95
114
|
} = options;
|
|
96
115
|
|
|
116
|
+
// 验证 shell 和 cwd 存在
|
|
117
|
+
if (!fs.existsSync(shell)) {
|
|
118
|
+
const error = `Shell not found: ${shell}`;
|
|
119
|
+
console.error('[PTY]', error);
|
|
120
|
+
throw new Error(error);
|
|
121
|
+
}
|
|
122
|
+
if (!fs.existsSync(cwd)) {
|
|
123
|
+
const error = `Working directory not found: ${cwd}`;
|
|
124
|
+
console.error('[PTY]', error);
|
|
125
|
+
throw new Error(error);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`[PTY] Creating terminal: shell=${shell}, cwd=${cwd}`);
|
|
129
|
+
|
|
130
|
+
|
|
97
131
|
const terminalId = `term_${this.nextId++}_${Date.now()}`;
|
|
98
132
|
|
|
99
133
|
// 合并环境变量
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
checkHasMessagesCache,
|
|
12
12
|
rememberHasMessages
|
|
13
13
|
} = require('./session-cache');
|
|
14
|
+
const { globalCache, CacheKeys } = require('./enhanced-cache');
|
|
14
15
|
const { PATHS } = require('../../config/paths');
|
|
15
16
|
|
|
16
17
|
// Base directory for cc-tool data
|
|
@@ -82,15 +83,15 @@ function saveForkRelations(relations) {
|
|
|
82
83
|
fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
// Get all projects with stats
|
|
86
|
-
function getProjects(config) {
|
|
86
|
+
// Get all projects with stats (async version)
|
|
87
|
+
async function getProjects(config) {
|
|
87
88
|
const projectsDir = config.projectsDir;
|
|
88
89
|
|
|
89
90
|
if (!fs.existsSync(projectsDir)) {
|
|
90
91
|
return [];
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const entries = fs.
|
|
94
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
94
95
|
return entries
|
|
95
96
|
.filter(entry => entry.isDirectory())
|
|
96
97
|
.map(entry => entry.name);
|
|
@@ -283,32 +284,52 @@ function extractCwdFromSessionHeader(sessionFile) {
|
|
|
283
284
|
return null;
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
// Get projects with detailed stats (with caching)
|
|
287
|
-
function getProjectsWithStats(config, options = {}) {
|
|
287
|
+
// Get projects with detailed stats (with caching) - async version
|
|
288
|
+
async function getProjectsWithStats(config, options = {}) {
|
|
288
289
|
if (!options.force) {
|
|
290
|
+
// Check enhanced cache first
|
|
291
|
+
const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
|
|
292
|
+
const enhancedCached = globalCache.get(cacheKey);
|
|
293
|
+
if (enhancedCached) {
|
|
294
|
+
return enhancedCached;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check old cache
|
|
289
298
|
const cached = getCachedProjects(config);
|
|
290
299
|
if (cached) {
|
|
300
|
+
globalCache.set(cacheKey, cached, 300000); // 5分钟
|
|
291
301
|
return cached;
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
305
|
+
try {
|
|
306
|
+
const data = await buildProjectsWithStats(config);
|
|
307
|
+
if (!Array.isArray(data)) {
|
|
308
|
+
console.warn(`[getProjectsWithStats] Unexpected non-array result for ${config.projectsDir}, returning empty array.`);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
setCachedProjects(config, data);
|
|
312
|
+
globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
|
|
313
|
+
return data;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
298
318
|
}
|
|
299
319
|
|
|
300
|
-
function buildProjectsWithStats(config) {
|
|
320
|
+
async function buildProjectsWithStats(config) {
|
|
301
321
|
const projectsDir = config.projectsDir;
|
|
302
322
|
|
|
303
323
|
if (!fs.existsSync(projectsDir)) {
|
|
304
324
|
return [];
|
|
305
325
|
}
|
|
306
326
|
|
|
307
|
-
const entries = fs.
|
|
327
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
308
328
|
|
|
309
|
-
|
|
329
|
+
// Process all projects concurrently
|
|
330
|
+
const projectPromises = entries
|
|
310
331
|
.filter(entry => entry.isDirectory())
|
|
311
|
-
.map(entry => {
|
|
332
|
+
.map(async (entry) => {
|
|
312
333
|
const projectName = entry.name;
|
|
313
334
|
const projectPath = path.join(projectsDir, projectName);
|
|
314
335
|
|
|
@@ -320,24 +341,29 @@ function buildProjectsWithStats(config) {
|
|
|
320
341
|
let lastUsed = null;
|
|
321
342
|
|
|
322
343
|
try {
|
|
323
|
-
const files = fs.
|
|
344
|
+
const files = await fs.promises.readdir(projectPath);
|
|
324
345
|
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
325
346
|
|
|
326
|
-
// Filter: only count sessions that have actual messages (
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
347
|
+
// Filter: only count sessions that have actual messages (in parallel)
|
|
348
|
+
const sessionChecks = await Promise.all(
|
|
349
|
+
jsonlFiles.map(async (f) => {
|
|
350
|
+
const filePath = path.join(projectPath, f);
|
|
351
|
+
const hasMessages = await hasActualMessages(filePath);
|
|
352
|
+
return hasMessages ? f : null;
|
|
353
|
+
})
|
|
354
|
+
);
|
|
331
355
|
|
|
356
|
+
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
332
357
|
sessionCount = sessionFilesWithMessages.length;
|
|
333
358
|
|
|
334
359
|
// Find most recent session (only from sessions with messages)
|
|
335
360
|
if (sessionFilesWithMessages.length > 0) {
|
|
336
|
-
const
|
|
361
|
+
const statPromises = sessionFilesWithMessages.map(async (f) => {
|
|
337
362
|
const filePath = path.join(projectPath, f);
|
|
338
|
-
const stat = fs.
|
|
363
|
+
const stat = await fs.promises.stat(filePath);
|
|
339
364
|
return stat.mtime.getTime();
|
|
340
365
|
});
|
|
366
|
+
const stats = await Promise.all(statPromises);
|
|
341
367
|
lastUsed = Math.max(...stats);
|
|
342
368
|
}
|
|
343
369
|
} catch (err) {
|
|
@@ -351,8 +377,10 @@ function buildProjectsWithStats(config) {
|
|
|
351
377
|
sessionCount,
|
|
352
378
|
lastUsed
|
|
353
379
|
};
|
|
354
|
-
})
|
|
355
|
-
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const projects = await Promise.all(projectPromises);
|
|
383
|
+
return projects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
|
|
356
384
|
}
|
|
357
385
|
|
|
358
386
|
// 获取 Claude 项目/会话数量(轻量统计)
|
|
@@ -383,16 +411,27 @@ function getProjectAndSessionCounts(config) {
|
|
|
383
411
|
return { projectCount, sessionCount };
|
|
384
412
|
}
|
|
385
413
|
|
|
386
|
-
// Check if a session file has actual messages (
|
|
387
|
-
function hasActualMessages(filePath) {
|
|
414
|
+
// Check if a session file has actual messages (async with enhanced caching)
|
|
415
|
+
async function hasActualMessages(filePath) {
|
|
388
416
|
try {
|
|
389
|
-
const stats = fs.
|
|
390
|
-
|
|
417
|
+
const stats = await fs.promises.stat(filePath);
|
|
418
|
+
|
|
419
|
+
// Check enhanced cache first
|
|
420
|
+
const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
|
|
421
|
+
const cached = globalCache.get(cacheKey);
|
|
391
422
|
if (typeof cached === 'boolean') {
|
|
392
423
|
return cached;
|
|
393
424
|
}
|
|
394
425
|
|
|
395
|
-
|
|
426
|
+
// Check old cache mechanism
|
|
427
|
+
const oldCached = checkHasMessagesCache(filePath, stats);
|
|
428
|
+
if (typeof oldCached === 'boolean') {
|
|
429
|
+
globalCache.set(cacheKey, oldCached, 600000); // 10分钟
|
|
430
|
+
return oldCached;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = await scanSessionFileForMessagesAsync(filePath);
|
|
434
|
+
globalCache.set(cacheKey, result, 600000);
|
|
396
435
|
rememberHasMessages(filePath, stats, result);
|
|
397
436
|
return result;
|
|
398
437
|
} catch (err) {
|
|
@@ -434,30 +473,74 @@ function scanSessionFileForMessages(filePath) {
|
|
|
434
473
|
}
|
|
435
474
|
}
|
|
436
475
|
|
|
437
|
-
//
|
|
438
|
-
function
|
|
476
|
+
// Async version using streams for better performance
|
|
477
|
+
function scanSessionFileForMessagesAsync(filePath) {
|
|
478
|
+
return new Promise((resolve) => {
|
|
479
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 64 * 1024 });
|
|
480
|
+
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
481
|
+
let found = false;
|
|
482
|
+
let leftover = '';
|
|
483
|
+
|
|
484
|
+
stream.on('data', (chunk) => {
|
|
485
|
+
if (found) return;
|
|
486
|
+
const combined = leftover + chunk;
|
|
487
|
+
if (pattern.test(combined)) {
|
|
488
|
+
found = true;
|
|
489
|
+
stream.destroy();
|
|
490
|
+
resolve(true);
|
|
491
|
+
}
|
|
492
|
+
leftover = combined.slice(-64);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
stream.on('end', () => {
|
|
496
|
+
if (!found) resolve(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
stream.on('error', () => {
|
|
500
|
+
resolve(false);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Get sessions for a project - async version
|
|
506
|
+
async function getSessionsForProject(config, projectName) {
|
|
507
|
+
// Check cache first
|
|
508
|
+
const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
|
|
509
|
+
const cached = globalCache.get(cacheKey);
|
|
510
|
+
if (cached) {
|
|
511
|
+
return cached;
|
|
512
|
+
}
|
|
513
|
+
|
|
439
514
|
const projectConfig = { ...config, currentProject: projectName };
|
|
440
515
|
const sessions = getAllSessions(projectConfig);
|
|
441
516
|
const forkRelations = getForkRelations();
|
|
442
517
|
const savedOrder = getSessionOrder(projectName);
|
|
443
518
|
|
|
444
|
-
// Parse session info and calculate total size, filter out sessions with no messages
|
|
519
|
+
// Parse session info and calculate total size, filter out sessions with no messages (in parallel)
|
|
445
520
|
let totalSize = 0;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
.map(session => {
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
521
|
+
|
|
522
|
+
const sessionChecks = await Promise.all(
|
|
523
|
+
sessions.map(async (session) => {
|
|
524
|
+
const hasMessages = await hasActualMessages(session.filePath);
|
|
525
|
+
return hasMessages ? session : null;
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const validSessions = sessionChecks.filter(s => s !== null);
|
|
530
|
+
|
|
531
|
+
const sessionsWithInfo = validSessions.map(session => {
|
|
532
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
533
|
+
totalSize += session.size || 0;
|
|
534
|
+
return {
|
|
535
|
+
sessionId: session.sessionId,
|
|
536
|
+
mtime: session.mtime,
|
|
537
|
+
size: session.size,
|
|
538
|
+
filePath: session.filePath,
|
|
539
|
+
gitBranch: info.gitBranch || null,
|
|
540
|
+
firstMessage: info.firstMessage || null,
|
|
541
|
+
forkedFrom: forkRelations[session.sessionId] || null
|
|
542
|
+
};
|
|
543
|
+
});
|
|
461
544
|
|
|
462
545
|
// Apply saved order if exists
|
|
463
546
|
let orderedSessions = sessionsWithInfo;
|
|
@@ -478,10 +561,14 @@ function getSessionsForProject(config, projectName) {
|
|
|
478
561
|
orderedSessions = ordered;
|
|
479
562
|
}
|
|
480
563
|
|
|
481
|
-
|
|
564
|
+
const result = {
|
|
482
565
|
sessions: orderedSessions,
|
|
483
566
|
totalSize
|
|
484
567
|
};
|
|
568
|
+
|
|
569
|
+
// Cache for 2 minutes
|
|
570
|
+
globalCache.set(cacheKey, result, 120000);
|
|
571
|
+
return result;
|
|
485
572
|
}
|
|
486
573
|
|
|
487
574
|
// Delete a session
|
|
@@ -847,14 +847,14 @@ class SkillService {
|
|
|
847
847
|
|
|
848
848
|
request.on('error', (err) => {
|
|
849
849
|
file.close();
|
|
850
|
-
fs.unlink(dest, () => {});
|
|
850
|
+
fs.unlink(dest, () => { });
|
|
851
851
|
reject(err);
|
|
852
852
|
});
|
|
853
853
|
|
|
854
854
|
request.on('timeout', () => {
|
|
855
855
|
request.destroy();
|
|
856
856
|
file.close();
|
|
857
|
-
fs.unlink(dest, () => {});
|
|
857
|
+
fs.unlink(dest, () => { });
|
|
858
858
|
reject(new Error('Download timeout'));
|
|
859
859
|
});
|
|
860
860
|
});
|
|
@@ -912,6 +912,256 @@ ${content}
|
|
|
912
912
|
return { success: true, message: '技能创建成功', directory };
|
|
913
913
|
}
|
|
914
914
|
|
|
915
|
+
/**
|
|
916
|
+
* 创建带多文件的技能
|
|
917
|
+
* @param {string} directory - 技能目录名
|
|
918
|
+
* @param {Array<{path: string, content: string}>} files - 文件数组
|
|
919
|
+
* @returns {Object} 创建结果
|
|
920
|
+
*/
|
|
921
|
+
createSkillWithFiles({ directory, files }) {
|
|
922
|
+
const dest = path.join(this.installDir, directory);
|
|
923
|
+
|
|
924
|
+
// 检查是否已存在
|
|
925
|
+
if (fs.existsSync(dest)) {
|
|
926
|
+
throw new Error(`技能目录 "${directory}" 已存在`);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 验证必须包含 SKILL.md
|
|
930
|
+
const hasSkillMd = files.some(f =>
|
|
931
|
+
f.path === 'SKILL.md' || f.path.endsWith('/SKILL.md')
|
|
932
|
+
);
|
|
933
|
+
if (!hasSkillMd) {
|
|
934
|
+
throw new Error('技能必须包含 SKILL.md 文件');
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// 创建目录
|
|
938
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
939
|
+
|
|
940
|
+
// 写入所有文件
|
|
941
|
+
for (const file of files) {
|
|
942
|
+
const filePath = path.join(dest, file.path);
|
|
943
|
+
const fileDir = path.dirname(filePath);
|
|
944
|
+
|
|
945
|
+
// 确保父目录存在
|
|
946
|
+
if (!fs.existsSync(fileDir)) {
|
|
947
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// 写入文件内容
|
|
951
|
+
if (file.isBase64) {
|
|
952
|
+
// 二进制文件使用 base64 编码
|
|
953
|
+
fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
954
|
+
} else {
|
|
955
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// 清除缓存
|
|
960
|
+
this.skillsCache = null;
|
|
961
|
+
this.cacheTime = 0;
|
|
962
|
+
|
|
963
|
+
return {
|
|
964
|
+
success: true,
|
|
965
|
+
message: '技能创建成功',
|
|
966
|
+
directory,
|
|
967
|
+
fileCount: files.length
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* 获取技能目录下所有文件列表
|
|
973
|
+
* @param {string} directory - 技能目录名
|
|
974
|
+
* @returns {Array<{path: string, size: number, isDirectory: boolean}>}
|
|
975
|
+
*/
|
|
976
|
+
getSkillFiles(directory) {
|
|
977
|
+
const skillPath = path.join(this.installDir, directory);
|
|
978
|
+
|
|
979
|
+
if (!fs.existsSync(skillPath)) {
|
|
980
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const files = [];
|
|
984
|
+
this._scanFilesRecursive(skillPath, skillPath, files);
|
|
985
|
+
return files;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* 递归扫描目录获取文件列表
|
|
990
|
+
*/
|
|
991
|
+
_scanFilesRecursive(currentDir, baseDir, files) {
|
|
992
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
993
|
+
|
|
994
|
+
for (const entry of entries) {
|
|
995
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
996
|
+
const relativePath = path.relative(baseDir, fullPath);
|
|
997
|
+
|
|
998
|
+
if (entry.isDirectory()) {
|
|
999
|
+
files.push({
|
|
1000
|
+
path: relativePath,
|
|
1001
|
+
size: 0,
|
|
1002
|
+
isDirectory: true
|
|
1003
|
+
});
|
|
1004
|
+
this._scanFilesRecursive(fullPath, baseDir, files);
|
|
1005
|
+
} else {
|
|
1006
|
+
const stats = fs.statSync(fullPath);
|
|
1007
|
+
files.push({
|
|
1008
|
+
path: relativePath,
|
|
1009
|
+
size: stats.size,
|
|
1010
|
+
isDirectory: false
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
/**
|
|
1017
|
+
* 获取技能文件内容
|
|
1018
|
+
* @param {string} directory - 技能目录名
|
|
1019
|
+
* @param {string} filePath - 文件相对路径
|
|
1020
|
+
* @returns {Object} 文件内容
|
|
1021
|
+
*/
|
|
1022
|
+
getSkillFileContent(directory, filePath) {
|
|
1023
|
+
const fullPath = path.join(this.installDir, directory, filePath);
|
|
1024
|
+
|
|
1025
|
+
if (!fs.existsSync(fullPath)) {
|
|
1026
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const stats = fs.statSync(fullPath);
|
|
1030
|
+
if (stats.isDirectory()) {
|
|
1031
|
+
throw new Error(`"${filePath}" 是目录,不是文件`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// 判断是否是文本文件
|
|
1035
|
+
const textExtensions = ['.md', '.txt', '.json', '.js', '.ts', '.py', '.sh', '.yaml', '.yml', '.toml', '.xml', '.html', '.css'];
|
|
1036
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1037
|
+
const isText = textExtensions.includes(ext);
|
|
1038
|
+
|
|
1039
|
+
if (isText) {
|
|
1040
|
+
return {
|
|
1041
|
+
path: filePath,
|
|
1042
|
+
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
1043
|
+
isBase64: false,
|
|
1044
|
+
size: stats.size
|
|
1045
|
+
};
|
|
1046
|
+
} else {
|
|
1047
|
+
return {
|
|
1048
|
+
path: filePath,
|
|
1049
|
+
content: fs.readFileSync(fullPath).toString('base64'),
|
|
1050
|
+
isBase64: true,
|
|
1051
|
+
size: stats.size
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 添加文件到现有技能
|
|
1058
|
+
* @param {string} directory - 技能目录名
|
|
1059
|
+
* @param {Array<{path: string, content: string, isBase64?: boolean}>} files - 文件数组
|
|
1060
|
+
*/
|
|
1061
|
+
addSkillFiles(directory, files) {
|
|
1062
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1063
|
+
|
|
1064
|
+
if (!fs.existsSync(skillPath)) {
|
|
1065
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
const added = [];
|
|
1069
|
+
for (const file of files) {
|
|
1070
|
+
const filePath = path.join(skillPath, file.path);
|
|
1071
|
+
const fileDir = path.dirname(filePath);
|
|
1072
|
+
|
|
1073
|
+
// 确保父目录存在
|
|
1074
|
+
if (!fs.existsSync(fileDir)) {
|
|
1075
|
+
fs.mkdirSync(fileDir, { recursive: true });
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// 写入文件
|
|
1079
|
+
if (file.isBase64) {
|
|
1080
|
+
fs.writeFileSync(filePath, Buffer.from(file.content, 'base64'));
|
|
1081
|
+
} else {
|
|
1082
|
+
fs.writeFileSync(filePath, file.content, 'utf-8');
|
|
1083
|
+
}
|
|
1084
|
+
added.push(file.path);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// 清除缓存
|
|
1088
|
+
this.skillsCache = null;
|
|
1089
|
+
this.cacheTime = 0;
|
|
1090
|
+
|
|
1091
|
+
return { success: true, added };
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/**
|
|
1095
|
+
* 删除技能中的文件
|
|
1096
|
+
* @param {string} directory - 技能目录名
|
|
1097
|
+
* @param {string} filePath - 文件相对路径
|
|
1098
|
+
*/
|
|
1099
|
+
deleteSkillFile(directory, filePath) {
|
|
1100
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1101
|
+
|
|
1102
|
+
if (!fs.existsSync(skillPath)) {
|
|
1103
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// 不允许删除 SKILL.md
|
|
1107
|
+
if (filePath === 'SKILL.md') {
|
|
1108
|
+
throw new Error('不能删除 SKILL.md 文件');
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
const fullPath = path.join(skillPath, filePath);
|
|
1112
|
+
|
|
1113
|
+
if (!fs.existsSync(fullPath)) {
|
|
1114
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const stats = fs.statSync(fullPath);
|
|
1118
|
+
if (stats.isDirectory()) {
|
|
1119
|
+
fs.rmSync(fullPath, { recursive: true, force: true });
|
|
1120
|
+
} else {
|
|
1121
|
+
fs.unlinkSync(fullPath);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// 清除缓存
|
|
1125
|
+
this.skillsCache = null;
|
|
1126
|
+
this.cacheTime = 0;
|
|
1127
|
+
|
|
1128
|
+
return { success: true, deleted: filePath };
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* 更新技能文件内容
|
|
1133
|
+
* @param {string} directory - 技能目录名
|
|
1134
|
+
* @param {string} filePath - 文件相对路径
|
|
1135
|
+
* @param {string} content - 新内容
|
|
1136
|
+
* @param {boolean} isBase64 - 是否为 base64 编码
|
|
1137
|
+
*/
|
|
1138
|
+
updateSkillFile(directory, filePath, content, isBase64 = false) {
|
|
1139
|
+
const skillPath = path.join(this.installDir, directory);
|
|
1140
|
+
|
|
1141
|
+
if (!fs.existsSync(skillPath)) {
|
|
1142
|
+
throw new Error(`技能 "${directory}" 不存在`);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const fullPath = path.join(skillPath, filePath);
|
|
1146
|
+
|
|
1147
|
+
if (!fs.existsSync(fullPath)) {
|
|
1148
|
+
throw new Error(`文件 "${filePath}" 不存在`);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (isBase64) {
|
|
1152
|
+
fs.writeFileSync(fullPath, Buffer.from(content, 'base64'));
|
|
1153
|
+
} else {
|
|
1154
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// 清除缓存
|
|
1158
|
+
this.skillsCache = null;
|
|
1159
|
+
this.cacheTime = 0;
|
|
1160
|
+
|
|
1161
|
+
return { success: true, updated: filePath };
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
|
|
915
1165
|
/**
|
|
916
1166
|
* 卸载技能
|
|
917
1167
|
*/
|
|
@@ -108,6 +108,11 @@ function saveTerminalCommands(commands) {
|
|
|
108
108
|
* @returns {string} 启动命令
|
|
109
109
|
*/
|
|
110
110
|
function getCommandForChannel(channel, sessionId = null, cwd = null) {
|
|
111
|
+
// Add support for plain shell (do not auto-run any command)
|
|
112
|
+
if (channel === 'shell') {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
const commands = loadTerminalCommands();
|
|
112
117
|
const channelConfig = commands[channel] || commands.claude;
|
|
113
118
|
|