@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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * 配置同步 API 路由
3
+ */
4
+
5
+ const express = require('express');
6
+ const { ConfigSyncService } = require('../services/config-sync-service');
7
+
8
+ const router = express.Router();
9
+ const configSyncService = new ConfigSyncService();
10
+
11
+ /**
12
+ * 获取可同步的配置列表
13
+ * GET /api/config-sync/available
14
+ * Query: source=global|workspace, projectPath=...
15
+ */
16
+ router.get('/available', (req, res) => {
17
+ try {
18
+ const { source = 'global', projectPath } = req.query;
19
+
20
+ if (source === 'workspace' && !projectPath) {
21
+ return res.status(400).json({
22
+ success: false,
23
+ message: '获取工作区配置需要指定 projectPath'
24
+ });
25
+ }
26
+
27
+ const configs = configSyncService.getAvailableConfigs(source, projectPath);
28
+
29
+ res.json({
30
+ success: true,
31
+ source,
32
+ configs
33
+ });
34
+ } catch (err) {
35
+ console.error('[ConfigSync API] Get available configs error:', err);
36
+ res.status(500).json({
37
+ success: false,
38
+ message: err.message
39
+ });
40
+ }
41
+ });
42
+
43
+ /**
44
+ * 获取同步统计信息
45
+ * GET /api/config-sync/stats
46
+ * Query: projectPath=...
47
+ */
48
+ router.get('/stats', (req, res) => {
49
+ try {
50
+ const { projectPath } = req.query;
51
+ const stats = configSyncService.getStats(projectPath);
52
+
53
+ res.json({
54
+ success: true,
55
+ stats
56
+ });
57
+ } catch (err) {
58
+ console.error('[ConfigSync API] Get stats error:', err);
59
+ res.status(500).json({
60
+ success: false,
61
+ message: err.message
62
+ });
63
+ }
64
+ });
65
+
66
+ /**
67
+ * 预览同步结果
68
+ * POST /api/config-sync/preview
69
+ * Body: { source, target, configTypes, projectPath, selectedItems }
70
+ */
71
+ router.post('/preview', (req, res) => {
72
+ try {
73
+ const { source, target, configTypes, projectPath, selectedItems } = req.body;
74
+
75
+ if (!source || !target) {
76
+ return res.status(400).json({
77
+ success: false,
78
+ message: '请指定源和目标'
79
+ });
80
+ }
81
+
82
+ if (!configTypes || configTypes.length === 0) {
83
+ return res.status(400).json({
84
+ success: false,
85
+ message: '请选择要同步的配置类型'
86
+ });
87
+ }
88
+
89
+ const preview = configSyncService.previewSync({
90
+ source,
91
+ target,
92
+ configTypes,
93
+ projectPath,
94
+ selectedItems
95
+ });
96
+
97
+ res.json({
98
+ success: true,
99
+ preview
100
+ });
101
+ } catch (err) {
102
+ console.error('[ConfigSync API] Preview sync error:', err);
103
+ res.status(500).json({
104
+ success: false,
105
+ message: err.message
106
+ });
107
+ }
108
+ });
109
+
110
+ /**
111
+ * 执行同步
112
+ * POST /api/config-sync/execute
113
+ * Body: { source, target, configTypes, projectPath, selectedItems, overwrite }
114
+ */
115
+ router.post('/execute', (req, res) => {
116
+ try {
117
+ const { source, target, configTypes, projectPath, selectedItems, overwrite = false } = req.body;
118
+
119
+ if (!source || !target) {
120
+ return res.status(400).json({
121
+ success: false,
122
+ message: '请指定源和目标'
123
+ });
124
+ }
125
+
126
+ if (!configTypes || configTypes.length === 0) {
127
+ return res.status(400).json({
128
+ success: false,
129
+ message: '请选择要同步的配置类型'
130
+ });
131
+ }
132
+
133
+ const result = configSyncService.executeSync({
134
+ source,
135
+ target,
136
+ configTypes,
137
+ projectPath,
138
+ selectedItems,
139
+ overwrite
140
+ });
141
+
142
+ res.json({
143
+ success: true,
144
+ result
145
+ });
146
+ } catch (err) {
147
+ console.error('[ConfigSync API] Execute sync error:', err);
148
+ res.status(500).json({
149
+ success: false,
150
+ message: err.message
151
+ });
152
+ }
153
+ });
154
+
155
+ module.exports = router;
@@ -4,9 +4,9 @@ const { getProjectsWithStats, saveProjectOrder, getProjectOrder, deleteProject }
4
4
 
5
5
  module.exports = (config) => {
6
6
  // GET /api/projects - Get all projects with stats
7
- router.get('/', (req, res) => {
7
+ router.get('/', async (req, res) => {
8
8
  try {
9
- const projects = getProjectsWithStats(config);
9
+ const projects = await getProjectsWithStats(config);
10
10
  const order = getProjectOrder(config);
11
11
 
12
12
  // Sort projects by saved order
@@ -45,10 +45,10 @@ module.exports = (config) => {
45
45
  });
46
46
 
47
47
  // GET /api/sessions/:projectName - Get sessions for a project
48
- router.get('/:projectName', (req, res) => {
48
+ router.get('/:projectName', async (req, res) => {
49
49
  try {
50
50
  const { projectName } = req.params;
51
- const result = getSessionsForProject(config, projectName);
51
+ const result = await getSessionsForProject(config, projectName);
52
52
  const aliases = loadAliases();
53
53
 
54
54
  // Parse project path info
@@ -222,8 +222,8 @@ module.exports = (config) => {
222
222
  });
223
223
 
224
224
  // GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
225
- router.get('/:projectName/:sessionId/messages', async (req, res) => {
226
- try {
225
+ router.get('/:projectName/:sessionId/messages', async (req, res) => {
226
+ try {
227
227
  const { projectName, sessionId } = req.params;
228
228
  const { page = 1, limit = 20, order = 'desc' } = req.query;
229
229
 
@@ -271,86 +271,86 @@ router.get('/:projectName/:sessionId/messages', async (req, res) => {
271
271
  }
272
272
 
273
273
  // Read and parse session file
274
- const allMessages = [];
275
- const metadata = {};
274
+ const allMessages = [];
275
+ const metadata = {};
276
276
 
277
- const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
278
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
277
+ const stream = fs.createReadStream(sessionFile, { encoding: 'utf8' });
278
+ const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
279
279
 
280
- try {
281
- for await (const line of rl) {
282
- if (!line.trim()) continue;
283
- try {
284
- const json = JSON.parse(line);
280
+ try {
281
+ for await (const line of rl) {
282
+ if (!line.trim()) continue;
283
+ try {
284
+ const json = JSON.parse(line);
285
285
 
286
- if (json.type === 'summary' && json.summary) {
287
- metadata.summary = json.summary;
288
- }
289
- if (json.gitBranch) {
290
- metadata.gitBranch = json.gitBranch;
291
- }
292
- if (json.cwd) {
293
- metadata.cwd = json.cwd;
294
- }
286
+ if (json.type === 'summary' && json.summary) {
287
+ metadata.summary = json.summary;
288
+ }
289
+ if (json.gitBranch) {
290
+ metadata.gitBranch = json.gitBranch;
291
+ }
292
+ if (json.cwd) {
293
+ metadata.cwd = json.cwd;
294
+ }
295
295
 
296
- if (json.type === 'user' || json.type === 'assistant') {
297
- const message = {
298
- type: json.type,
299
- content: null,
300
- timestamp: json.timestamp || null,
301
- model: json.model || null
302
- };
303
-
304
- if (json.type === 'user') {
305
- if (typeof json.message?.content === 'string') {
306
- message.content = json.message.content;
307
- } else if (Array.isArray(json.message?.content)) {
308
- const parts = [];
309
- for (const item of json.message.content) {
310
- if (item.type === 'text' && item.text) {
311
- parts.push(item.text);
312
- } else if (item.type === 'tool_result') {
313
- const resultContent = typeof item.content === 'string'
314
- ? item.content
315
- : JSON.stringify(item.content, null, 2);
316
- parts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
317
- } else if (item.type === 'image') {
318
- parts.push('[图片]');
296
+ if (json.type === 'user' || json.type === 'assistant') {
297
+ const message = {
298
+ type: json.type,
299
+ content: null,
300
+ timestamp: json.timestamp || null,
301
+ model: json.model || null
302
+ };
303
+
304
+ if (json.type === 'user') {
305
+ if (typeof json.message?.content === 'string') {
306
+ message.content = json.message.content;
307
+ } else if (Array.isArray(json.message?.content)) {
308
+ const parts = [];
309
+ for (const item of json.message.content) {
310
+ if (item.type === 'text' && item.text) {
311
+ parts.push(item.text);
312
+ } else if (item.type === 'tool_result') {
313
+ const resultContent = typeof item.content === 'string'
314
+ ? item.content
315
+ : JSON.stringify(item.content, null, 2);
316
+ parts.push(`**[工具结果]**\n\`\`\`\n${resultContent}\n\`\`\``);
317
+ } else if (item.type === 'image') {
318
+ parts.push('[图片]');
319
+ }
319
320
  }
321
+ message.content = parts.join('\n\n') || '[工具交互]';
320
322
  }
321
- message.content = parts.join('\n\n') || '[工具交互]';
322
- }
323
- } else if (json.type === 'assistant') {
324
- if (Array.isArray(json.message?.content)) {
325
- const parts = [];
326
- for (const item of json.message.content) {
327
- if (item.type === 'text' && item.text) {
328
- parts.push(item.text);
329
- } else if (item.type === 'tool_use') {
330
- const inputStr = JSON.stringify(item.input, null, 2);
331
- parts.push(`**[调用工具: ${item.name}]**\n\`\`\`json\n${inputStr}\n\`\`\``);
332
- } else if (item.type === 'thinking' && item.thinking) {
333
- parts.push(`**[思考]**\n${item.thinking}`);
323
+ } else if (json.type === 'assistant') {
324
+ if (Array.isArray(json.message?.content)) {
325
+ const parts = [];
326
+ for (const item of json.message.content) {
327
+ if (item.type === 'text' && item.text) {
328
+ parts.push(item.text);
329
+ } else if (item.type === 'tool_use') {
330
+ const inputStr = JSON.stringify(item.input, null, 2);
331
+ parts.push(`**[调用工具: ${item.name}]**\n\`\`\`json\n${inputStr}\n\`\`\``);
332
+ } else if (item.type === 'thinking' && item.thinking) {
333
+ parts.push(`**[思考]**\n${item.thinking}`);
334
+ }
334
335
  }
336
+ message.content = parts.join('\n\n') || '[处理中...]';
337
+ } else if (typeof json.message?.content === 'string') {
338
+ message.content = json.message.content;
335
339
  }
336
- message.content = parts.join('\n\n') || '[处理中...]';
337
- } else if (typeof json.message?.content === 'string') {
338
- message.content = json.message.content;
339
340
  }
340
- }
341
341
 
342
- if (message.content && message.content !== 'Warmup') {
343
- allMessages.push(message);
342
+ if (message.content && message.content !== 'Warmup') {
343
+ allMessages.push(message);
344
+ }
344
345
  }
346
+ } catch (err) {
347
+ // Skip invalid lines
345
348
  }
346
- } catch (err) {
347
- // Skip invalid lines
348
349
  }
350
+ } finally {
351
+ rl.close();
352
+ stream.destroy();
349
353
  }
350
- } finally {
351
- rl.close();
352
- stream.destroy();
353
- }
354
354
 
355
355
  // Sort messages (desc = newest first)
356
356
  if (order === 'desc') {
@@ -312,6 +312,212 @@ router.put('/repos/:owner/:name/toggle', (req, res) => {
312
312
  }
313
313
  });
314
314
 
315
+ // ==================== 多文件技能管理 API ====================
316
+
317
+ /**
318
+ * 创建带多文件的技能
319
+ * POST /api/skills/create-with-files
320
+ * Body: { directory, files: [{path, content, isBase64?}] }
321
+ */
322
+ router.post('/create-with-files', (req, res) => {
323
+ try {
324
+ const { directory, files } = req.body;
325
+
326
+ if (!directory) {
327
+ return res.status(400).json({
328
+ success: false,
329
+ message: '请输入目录名称'
330
+ });
331
+ }
332
+
333
+ // 校验目录名:只允许英文、数字、横杠、下划线
334
+ if (!/^[a-zA-Z0-9_-]+$/.test(directory)) {
335
+ return res.status(400).json({
336
+ success: false,
337
+ message: '目录名只能包含英文、数字、横杠和下划线'
338
+ });
339
+ }
340
+
341
+ if (!files || !Array.isArray(files) || files.length === 0) {
342
+ return res.status(400).json({
343
+ success: false,
344
+ message: '请提供文件列表'
345
+ });
346
+ }
347
+
348
+ const result = skillService.createSkillWithFiles({ directory, files });
349
+
350
+ res.json({
351
+ success: true,
352
+ ...result
353
+ });
354
+ } catch (err) {
355
+ console.error('[Skills API] Create skill with files error:', err);
356
+ res.status(500).json({
357
+ success: false,
358
+ message: err.message
359
+ });
360
+ }
361
+ });
362
+
363
+ /**
364
+ * 获取技能文件列表
365
+ * GET /api/skills/:directory/files
366
+ */
367
+ router.get('/:directory/files', (req, res) => {
368
+ try {
369
+ const { directory } = req.params;
370
+ const files = skillService.getSkillFiles(directory);
371
+
372
+ res.json({
373
+ success: true,
374
+ directory,
375
+ files
376
+ });
377
+ } catch (err) {
378
+ console.error('[Skills API] Get skill files error:', err);
379
+ res.status(500).json({
380
+ success: false,
381
+ message: err.message
382
+ });
383
+ }
384
+ });
385
+
386
+ /**
387
+ * 获取技能文件内容
388
+ * GET /api/skills/:directory/files/:filePath
389
+ * 注意:filePath 可能包含子目录,使用通配符
390
+ */
391
+ router.get('/:directory/file/*', (req, res) => {
392
+ try {
393
+ const { directory } = req.params;
394
+ const filePath = req.params[0];
395
+
396
+ if (!filePath) {
397
+ return res.status(400).json({
398
+ success: false,
399
+ message: '请指定文件路径'
400
+ });
401
+ }
402
+
403
+ const result = skillService.getSkillFileContent(directory, filePath);
404
+
405
+ res.json({
406
+ success: true,
407
+ ...result
408
+ });
409
+ } catch (err) {
410
+ console.error('[Skills API] Get skill file content error:', err);
411
+ res.status(500).json({
412
+ success: false,
413
+ message: err.message
414
+ });
415
+ }
416
+ });
417
+
418
+ /**
419
+ * 添加文件到技能
420
+ * POST /api/skills/:directory/files
421
+ * Body: { files: [{path, content, isBase64?}] }
422
+ */
423
+ router.post('/:directory/files', (req, res) => {
424
+ try {
425
+ const { directory } = req.params;
426
+ const { files } = req.body;
427
+
428
+ if (!files || !Array.isArray(files) || files.length === 0) {
429
+ return res.status(400).json({
430
+ success: false,
431
+ message: '请提供文件列表'
432
+ });
433
+ }
434
+
435
+ const result = skillService.addSkillFiles(directory, files);
436
+
437
+ res.json({
438
+ success: true,
439
+ ...result
440
+ });
441
+ } catch (err) {
442
+ console.error('[Skills API] Add skill files error:', err);
443
+ res.status(500).json({
444
+ success: false,
445
+ message: err.message
446
+ });
447
+ }
448
+ });
449
+
450
+ /**
451
+ * 删除技能中的文件
452
+ * DELETE /api/skills/:directory/file/*
453
+ */
454
+ router.delete('/:directory/file/*', (req, res) => {
455
+ try {
456
+ const { directory } = req.params;
457
+ const filePath = req.params[0];
458
+
459
+ if (!filePath) {
460
+ return res.status(400).json({
461
+ success: false,
462
+ message: '请指定文件路径'
463
+ });
464
+ }
465
+
466
+ const result = skillService.deleteSkillFile(directory, filePath);
467
+
468
+ res.json({
469
+ success: true,
470
+ ...result
471
+ });
472
+ } catch (err) {
473
+ console.error('[Skills API] Delete skill file error:', err);
474
+ res.status(500).json({
475
+ success: false,
476
+ message: err.message
477
+ });
478
+ }
479
+ });
480
+
481
+ /**
482
+ * 更新技能文件内容
483
+ * PUT /api/skills/:directory/file/*
484
+ * Body: { content, isBase64? }
485
+ */
486
+ router.put('/:directory/file/*', (req, res) => {
487
+ try {
488
+ const { directory } = req.params;
489
+ const filePath = req.params[0];
490
+ const { content, isBase64 = false } = req.body;
491
+
492
+ if (!filePath) {
493
+ return res.status(400).json({
494
+ success: false,
495
+ message: '请指定文件路径'
496
+ });
497
+ }
498
+
499
+ if (content === undefined) {
500
+ return res.status(400).json({
501
+ success: false,
502
+ message: '请提供文件内容'
503
+ });
504
+ }
505
+
506
+ const result = skillService.updateSkillFile(directory, filePath, content, isBase64);
507
+
508
+ res.json({
509
+ success: true,
510
+ ...result
511
+ });
512
+ } catch (err) {
513
+ console.error('[Skills API] Update skill file error:', err);
514
+ res.status(500).json({
515
+ success: false,
516
+ message: err.message
517
+ });
518
+ }
519
+ });
520
+
315
521
  // ==================== 格式转换 API ====================
316
522
 
317
523
  /**
@@ -28,6 +28,32 @@ router.get('/list', (req, res) => {
28
28
  }
29
29
  });
30
30
 
31
+ /**
32
+ * GET /api/terminal/health - 检查终端健康状态
33
+ */
34
+ router.get('/health', (req, res) => {
35
+ try {
36
+ const isPtyAvailable = ptyManager.isPtyAvailable();
37
+ const ptyError = ptyManager.getPtyError();
38
+
39
+ res.json({
40
+ success: isPtyAvailable,
41
+ pty: {
42
+ available: isPtyAvailable,
43
+ error: ptyError,
44
+ nodeVersion: process.version,
45
+ platform: process.platform
46
+ },
47
+ shell: {
48
+ default: ptyManager.getDefaultShell(),
49
+ env: process.env.SHELL
50
+ }
51
+ });
52
+ } catch (err) {
53
+ res.status(500).json({ success: false, error: err.message });
54
+ }
55
+ });
56
+
31
57
  /**
32
58
  * GET /api/terminal/commands/config - 获取命令配置
33
59
  * 注意:此路由必须在 /:id 之前定义,否则会被动态路由捕获
@@ -109,9 +109,9 @@ router.get('/check-git/*', (req, res) => {
109
109
  * GET /api/workspaces/available-projects
110
110
  * 获取所有渠道(Claude/Codex/Gemini)的项目并集
111
111
  */
112
- router.get('/available-projects', (req, res) => {
112
+ router.get('/available-projects', async (req, res) => {
113
113
  try {
114
- const projects = workspaceService.getAllAvailableProjects();
114
+ const projects = await workspaceService.getAllAvailableProjects();
115
115
  res.json({
116
116
  success: true,
117
117
  data: projects
@@ -149,6 +149,9 @@ async function startServer(port) {
149
149
  // 配置导出/导入 API
150
150
  app.use('/api/config-export', require('./api/config-export'));
151
151
 
152
+ // 配置同步 API
153
+ app.use('/api/config-sync', require('./api/config-sync'));
154
+
152
155
  // 健康检查 API
153
156
  app.use('/api/health-check', require('./api/health-check')(config));
154
157
 
@@ -261,7 +264,7 @@ function autoRestoreProxies() {
261
264
  }
262
265
 
263
266
  // 启动时执行健康检查
264
- function performStartupHealthCheck() {
267
+ async function performStartupHealthCheck() {
265
268
  const { healthCheckAllProjects } = require('./services/health-check');
266
269
  const { getProjects } = require('./services/sessions');
267
270
 
@@ -270,7 +273,7 @@ function performStartupHealthCheck() {
270
273
 
271
274
  // 获取所有项目
272
275
  const config = loadConfig();
273
- const projects = getProjects(config);
276
+ const projects = await getProjects(config);
274
277
 
275
278
  if (projects.length === 0) {
276
279
  console.log(chalk.gray(' 未发现项目,跳过健康检查'));