@adversity/coding-tool-x 2.5.0 → 2.6.0

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/src/index.js CHANGED
@@ -21,7 +21,10 @@ const { handleLogs } = require('./commands/logs');
21
21
  const { handleStats, handleStatsExport } = require('./commands/stats');
22
22
  const { handleDoctor } = require('./commands/doctor');
23
23
  const { workspaceMenu } = require('./commands/workspace');
24
+ const PluginManager = require('./plugins/plugin-manager');
25
+ const eventBus = require('./plugins/event-bus');
24
26
  const chalk = require('chalk');
27
+ const inquirer = require('inquirer');
25
28
  const path = require('path');
26
29
  const fs = require('fs');
27
30
 
@@ -79,6 +82,17 @@ function showHelp() {
79
82
  console.log(' ctx --version, -v 显示版本');
80
83
  console.log(' ctx --help, -h 显示帮助\n');
81
84
 
85
+ console.log(chalk.yellow('🔌 插件管理:'));
86
+ console.log(' ctx plugin list 列出已安装插件');
87
+ console.log(' ctx plugin install <url> 从 Git 安装插件');
88
+ console.log(' ctx plugin remove <name> 卸载插件');
89
+ console.log(' ctx plugin enable <name> 启用插件');
90
+ console.log(' ctx plugin disable <name> 禁用插件');
91
+ console.log(' ctx plugin info <name> 查看插件详情');
92
+ console.log(' ctx plugin config <name> 配置插件');
93
+ console.log(' ctx plugin update <name> 更新插件');
94
+ console.log(' ctx plugin update --all 更新所有插件\n');
95
+
82
96
  console.log(chalk.yellow('💡 快速开始:'));
83
97
  console.log(chalk.gray(' $ ctx start # 后台启动服务(推荐)'));
84
98
  console.log(chalk.gray(' $ ctx status # 查看服务状态'));
@@ -106,7 +120,9 @@ process.on('uncaughtException', (err) => {
106
120
  });
107
121
 
108
122
  // 处理 SIGINT 信号(Ctrl+C)
109
- process.on('SIGINT', () => {
123
+ process.on('SIGINT', async () => {
124
+ eventBus.emitSync('cli:shutdown', {});
125
+ PluginManager.shutdownPlugins();
110
126
  process.exit(0);
111
127
  });
112
128
 
@@ -288,13 +304,46 @@ async function main() {
288
304
  }
289
305
  }
290
306
 
307
+ // plugin 命令 - 插件管理
308
+ if (args[0] === 'plugin') {
309
+ const { handlePluginCommand } = require('./commands/plugin');
310
+ await handlePluginCommand(args.slice(1));
311
+ return;
312
+ }
313
+
291
314
  // 加载配置
292
315
  let config = loadConfig();
293
316
 
317
+ // 初始化插件系统
318
+ const pluginResult = PluginManager.initializePlugins({ config, args });
319
+ if (pluginResult.loaded > 0) {
320
+ console.log(chalk.gray(`[Plugin] 已加载 ${pluginResult.loaded} 个插件`));
321
+ }
322
+ if (pluginResult.failed.length > 0) {
323
+ console.log(chalk.yellow(`[Plugin] ${pluginResult.failed.length} 个插件加载失败`));
324
+ }
325
+
326
+ // 检查是否为插件注册的命令
327
+ if (args[0] && PluginManager.isPluginCommand(args[0])) {
328
+ eventBus.emitSync('cli:command:before', { command: args[0], args: args.slice(1), config });
329
+ const result = await PluginManager.executePluginCommand(args[0], args.slice(1));
330
+ eventBus.emitSync('cli:command:after', { command: args[0], args: args.slice(1), result });
331
+ if (!result.success) {
332
+ console.error(chalk.red(result.error));
333
+ process.exit(1);
334
+ }
335
+ return;
336
+ }
337
+
294
338
  while (true) {
295
339
  // 显示主菜单
296
340
  const action = await showMainMenu(config);
297
341
 
342
+ // 发送命令开始事件
343
+ eventBus.emitSync('cli:command:before', { command: action, args: [], config });
344
+
345
+ let result = { success: true };
346
+
298
347
  switch (action) {
299
348
  case 'list':
300
349
  await handleList(config, async () => {
@@ -370,15 +419,194 @@ async function main() {
370
419
  await resetConfig();
371
420
  break;
372
421
 
422
+ case 'plugin-menu': {
423
+ const { handlePluginCommand } = require('./commands/plugin');
424
+
425
+ // Show plugin management submenu
426
+ const pluginAction = await inquirer.prompt([{
427
+ type: 'list',
428
+ name: 'action',
429
+ message: chalk.cyan('选择插件操作:'),
430
+ choices: [
431
+ { name: '📋 列出已安装插件', value: 'list' },
432
+ { name: '📦 安装插件', value: 'install' },
433
+ { name: '🗑️ 卸载插件', value: 'remove' },
434
+ { name: '🔄 启用/禁用插件', value: 'toggle' },
435
+ { name: 'ℹ️ 查看插件信息', value: 'info' },
436
+ { name: '⬆️ 更新插件', value: 'update' },
437
+ { name: '⚙️ 配置插件', value: 'config' },
438
+ { name: '◀️ 返回主菜单', value: 'back' }
439
+ ]
440
+ }]);
441
+
442
+ if (pluginAction.action === 'back') {
443
+ break;
444
+ }
445
+
446
+ // Build args array based on action
447
+ let args = [pluginAction.action];
448
+
449
+ switch (pluginAction.action) {
450
+ case 'list':
451
+ // No additional args needed
452
+ await handlePluginCommand(args);
453
+ break;
454
+
455
+ case 'install': {
456
+ const installPrompt = await inquirer.prompt([{
457
+ type: 'input',
458
+ name: 'url',
459
+ message: '请输入插件 Git URL:',
460
+ validate: (input) => {
461
+ if (!input || input.trim() === '') {
462
+ return '请输入有效的 Git URL';
463
+ }
464
+ if (!input.match(/^https?:\/\//)) {
465
+ return '请输入完整的 URL (http:// 或 https://)';
466
+ }
467
+ return true;
468
+ }
469
+ }]);
470
+ args.push(installPrompt.url);
471
+ await handlePluginCommand(args);
472
+ break;
473
+ }
474
+
475
+ case 'remove': {
476
+ const removePrompt = await inquirer.prompt([{
477
+ type: 'input',
478
+ name: 'name',
479
+ message: '请输入要卸载的插件名称:',
480
+ validate: (input) => {
481
+ if (!input || input.trim() === '') {
482
+ return '请输入插件名称';
483
+ }
484
+ return true;
485
+ }
486
+ }]);
487
+ args.push(removePrompt.name);
488
+ await handlePluginCommand(args);
489
+ break;
490
+ }
491
+
492
+ case 'toggle': {
493
+ const toggleChoice = await inquirer.prompt([{
494
+ type: 'list',
495
+ name: 'operation',
496
+ message: '选择操作:',
497
+ choices: [
498
+ { name: '✅ 启用插件', value: 'enable' },
499
+ { name: '❌ 禁用插件', value: 'disable' }
500
+ ]
501
+ }]);
502
+
503
+ const togglePrompt = await inquirer.prompt([{
504
+ type: 'input',
505
+ name: 'name',
506
+ message: '请输入插件名称:',
507
+ validate: (input) => {
508
+ if (!input || input.trim() === '') {
509
+ return '请输入插件名称';
510
+ }
511
+ return true;
512
+ }
513
+ }]);
514
+
515
+ args = [toggleChoice.operation, togglePrompt.name];
516
+ await handlePluginCommand(args);
517
+ break;
518
+ }
519
+
520
+ case 'info': {
521
+ const infoPrompt = await inquirer.prompt([{
522
+ type: 'input',
523
+ name: 'name',
524
+ message: '请输入插件名称:',
525
+ validate: (input) => {
526
+ if (!input || input.trim() === '') {
527
+ return '请输入插件名称';
528
+ }
529
+ return true;
530
+ }
531
+ }]);
532
+ args.push(infoPrompt.name);
533
+ await handlePluginCommand(args);
534
+ break;
535
+ }
536
+
537
+ case 'update': {
538
+ const updateChoice = await inquirer.prompt([{
539
+ type: 'list',
540
+ name: 'option',
541
+ message: '选择更新选项:',
542
+ choices: [
543
+ { name: '🔄 更新指定插件', value: 'single' },
544
+ { name: '🔄 更新所有插件', value: 'all' }
545
+ ]
546
+ }]);
547
+
548
+ if (updateChoice.option === 'all') {
549
+ args.push('--all');
550
+ } else {
551
+ const updatePrompt = await inquirer.prompt([{
552
+ type: 'input',
553
+ name: 'name',
554
+ message: '请输入插件名称:',
555
+ validate: (input) => {
556
+ if (!input || input.trim() === '') {
557
+ return '请输入插件名称';
558
+ }
559
+ return true;
560
+ }
561
+ }]);
562
+ args.push(updatePrompt.name);
563
+ }
564
+ await handlePluginCommand(args);
565
+ break;
566
+ }
567
+
568
+ case 'config': {
569
+ const configPrompt = await inquirer.prompt([{
570
+ type: 'input',
571
+ name: 'name',
572
+ message: '请输入插件名称:',
573
+ validate: (input) => {
574
+ if (!input || input.trim() === '') {
575
+ return '请输入插件名称';
576
+ }
577
+ return true;
578
+ }
579
+ }]);
580
+ args.push(configPrompt.name);
581
+ await handlePluginCommand(args);
582
+ break;
583
+ }
584
+ }
585
+
586
+ // Wait for user to continue
587
+ await inquirer.prompt([{
588
+ type: 'input',
589
+ name: 'continue',
590
+ message: chalk.gray('按 Enter 继续...')
591
+ }]);
592
+ break;
593
+ }
594
+
373
595
  case 'exit':
374
596
  console.log('\n👋 再见!\n');
597
+ eventBus.emitSync('cli:shutdown', {});
598
+ PluginManager.shutdownPlugins();
375
599
  process.exit(0);
376
600
  break;
377
601
 
378
602
  default:
379
603
  console.log('未知操作');
604
+ result = { success: false, error: '未知操作' };
380
605
  break;
381
606
  }
607
+
608
+ // 发送命令完成事件
609
+ eventBus.emitSync('cli:command:after', { command: action, args: [], result });
382
610
  }
383
611
  }
384
612
 
@@ -4,23 +4,74 @@
4
4
 
5
5
  const express = require('express');
6
6
  const configExportService = require('../services/config-export-service');
7
+ const AdmZip = require('adm-zip');
7
8
 
8
9
  const router = express.Router();
9
10
 
11
+ function parseConfigZip(buffer) {
12
+ const zip = new AdmZip(buffer);
13
+ const entry = zip.getEntry('config.json');
14
+ if (!entry) {
15
+ throw new Error('配置包缺少 config.json');
16
+ }
17
+ const content = entry.getData().toString('utf8');
18
+ return JSON.parse(content);
19
+ }
20
+
21
+ function buildPreviewSummary(data) {
22
+ return {
23
+ version: data.version,
24
+ exportedAt: data.exportedAt,
25
+ counts: {
26
+ permissionTemplates: (data.data.permissionTemplates || []).length,
27
+ configTemplates: (data.data.configTemplates || []).length,
28
+ channels: (data.data.channels || []).length
29
+ },
30
+ items: {
31
+ permissionTemplates: (data.data.permissionTemplates || []).map(t => ({
32
+ id: t.id,
33
+ name: t.name,
34
+ description: t.description
35
+ })),
36
+ configTemplates: (data.data.configTemplates || []).map(t => ({
37
+ id: t.id,
38
+ name: t.name,
39
+ description: t.description
40
+ })),
41
+ channels: (data.data.channels || []).map(c => ({
42
+ id: c.id,
43
+ name: c.name,
44
+ type: c.type
45
+ }))
46
+ }
47
+ };
48
+ }
49
+
10
50
  /**
11
51
  * 导出所有配置
12
52
  * GET /api/config-export
13
53
  */
14
54
  router.get('/', (req, res) => {
15
55
  try {
16
- const result = configExportService.exportAllConfigs();
56
+ const format = (req.query.format || 'json').toLowerCase();
57
+ const result = format === 'zip'
58
+ ? configExportService.exportAllConfigsZip()
59
+ : configExportService.exportAllConfigs();
17
60
 
18
61
  if (result.success) {
19
- // 设置响应头,触发文件下载
20
- const filename = `ctx-config-${new Date().toISOString().split('T')[0]}.json`;
21
- res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
22
- res.setHeader('Content-Type', 'application/json');
23
- res.json(result.data);
62
+ if (format === 'zip') {
63
+ // 设置响应头,触发文件下载
64
+ const filename = result.filename || `ctx-config-${new Date().toISOString().split('T')[0]}.zip`;
65
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
66
+ res.setHeader('Content-Type', 'application/zip');
67
+ res.send(result.data);
68
+ } else {
69
+ // 设置响应头,触发文件下载
70
+ const filename = `ctx-config-${new Date().toISOString().split('T')[0]}.json`;
71
+ res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
72
+ res.setHeader('Content-Type', 'application/json');
73
+ res.json(result.data);
74
+ }
24
75
  } else {
25
76
  res.status(500).json({
26
77
  success: false,
@@ -64,6 +115,34 @@ router.post('/import', (req, res) => {
64
115
  }
65
116
  });
66
117
 
118
+ /**
119
+ * 导入 ZIP 配置
120
+ * POST /api/config-export/import-zip
121
+ */
122
+ router.post('/import-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
123
+ try {
124
+ const overwrite = req.query.overwrite === 'true';
125
+ const buffer = req.body;
126
+
127
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
128
+ return res.status(400).json({
129
+ success: false,
130
+ message: '缺少 ZIP 文件内容'
131
+ });
132
+ }
133
+
134
+ const data = parseConfigZip(buffer);
135
+ const result = configExportService.importConfigs(data, { overwrite });
136
+ res.json(result);
137
+ } catch (err) {
138
+ console.error('[ConfigExport API] 导入 ZIP 失败:', err);
139
+ res.status(500).json({
140
+ success: false,
141
+ message: err.message
142
+ });
143
+ }
144
+ });
145
+
67
146
  /**
68
147
  * 预览导入配置(不实际导入)
69
148
  * POST /api/config-export/preview
@@ -79,32 +158,7 @@ router.post('/preview', (req, res) => {
79
158
  });
80
159
  }
81
160
 
82
- const summary = {
83
- version: data.version,
84
- exportedAt: data.exportedAt,
85
- counts: {
86
- permissionTemplates: (data.data.permissionTemplates || []).length,
87
- configTemplates: (data.data.configTemplates || []).length,
88
- channels: (data.data.channels || []).length
89
- },
90
- items: {
91
- permissionTemplates: (data.data.permissionTemplates || []).map(t => ({
92
- id: t.id,
93
- name: t.name,
94
- description: t.description
95
- })),
96
- configTemplates: (data.data.configTemplates || []).map(t => ({
97
- id: t.id,
98
- name: t.name,
99
- description: t.description
100
- })),
101
- channels: (data.data.channels || []).map(c => ({
102
- id: c.id,
103
- name: c.name,
104
- type: c.type
105
- }))
106
- }
107
- };
161
+ const summary = buildPreviewSummary(data);
108
162
 
109
163
  res.json({
110
164
  success: true,
@@ -119,4 +173,40 @@ router.post('/preview', (req, res) => {
119
173
  }
120
174
  });
121
175
 
176
+ /**
177
+ * 预览 ZIP 导入配置(不实际导入)
178
+ * POST /api/config-export/preview-zip
179
+ */
180
+ router.post('/preview-zip', express.raw({ type: ['application/zip', 'application/octet-stream'], limit: '100mb' }), (req, res) => {
181
+ try {
182
+ const buffer = req.body;
183
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) {
184
+ return res.status(400).json({
185
+ success: false,
186
+ message: '缺少 ZIP 文件内容'
187
+ });
188
+ }
189
+
190
+ const data = parseConfigZip(buffer);
191
+ if (!data || !data.data) {
192
+ return res.status(400).json({
193
+ success: false,
194
+ message: '无效的导入数据格式'
195
+ });
196
+ }
197
+
198
+ const summary = buildPreviewSummary(data);
199
+ res.json({
200
+ success: true,
201
+ data: summary
202
+ });
203
+ } catch (err) {
204
+ console.error('[ConfigExport API] 预览 ZIP 失败:', err);
205
+ res.status(500).json({
206
+ success: false,
207
+ message: err.message
208
+ });
209
+ }
210
+ });
211
+
122
212
  module.exports = router;
@@ -74,16 +74,17 @@ router.get('/init', async (req, res) => {
74
74
  Promise.resolve(getGeminiCounts())
75
75
  ]);
76
76
 
77
- // 格式化统计数据:取 summary 中的数据
77
+ // 格式化统计数据:取 summary 和 byModel 中的数据
78
78
  const formatStats = (stats) => {
79
79
  if (stats && stats.summary) {
80
80
  return {
81
81
  requests: stats.summary.requests || 0,
82
82
  tokens: stats.summary.tokens || 0,
83
- cost: stats.summary.cost || 0
83
+ cost: stats.summary.cost || 0,
84
+ byModel: stats.byModel || {}
84
85
  };
85
86
  }
86
- return { requests: 0, tokens: 0, cost: 0 };
87
+ return { requests: 0, tokens: 0, cost: 0, byModel: {} };
87
88
  };
88
89
 
89
90
  res.json({
@@ -333,4 +333,67 @@ router.get('/export/download', (req, res) => {
333
333
  }
334
334
  });
335
335
 
336
+ /**
337
+ * GET /api/mcp/servers/:id/tools
338
+ * 获取 MCP 服务器的工具列表
339
+ */
340
+ router.get('/servers/:id/tools', async (req, res) => {
341
+ try {
342
+ const { id } = req.params;
343
+ const result = await mcpService.getServerTools(id);
344
+ res.json(result);
345
+ } catch (err) {
346
+ res.status(404).json({ success: false, error: err.message });
347
+ }
348
+ });
349
+
350
+ /**
351
+ * POST /api/mcp/servers/:id/tools/test
352
+ * 测试 MCP 服务器的工具
353
+ */
354
+ router.post('/servers/:id/tools/test', async (req, res) => {
355
+ try {
356
+ const { id } = req.params;
357
+ const { toolName, arguments: args } = req.body;
358
+
359
+ if (!toolName) {
360
+ return res.status(400).json({ success: false, error: '缺少 toolName 参数' });
361
+ }
362
+
363
+ const result = await mcpService.callServerTool(id, toolName, args || {});
364
+ res.json({ success: true, ...result });
365
+ } catch (err) {
366
+ res.status(500).json({ success: false, error: err.message });
367
+ }
368
+ });
369
+
370
+ /**
371
+ * GET /api/mcp/servers/:id/info
372
+ * 获取 MCP 服务器的详细信息
373
+ */
374
+ router.get('/servers/:id/info', async (req, res) => {
375
+ try {
376
+ const { id } = req.params;
377
+ const result = await mcpService.getServerTools(id);
378
+
379
+ const serverData = mcpService.getServer(id);
380
+
381
+ res.json({
382
+ success: true,
383
+ capabilities: {
384
+ tools: true,
385
+ resources: false,
386
+ prompts: false
387
+ },
388
+ tools: result.tools,
389
+ serverInfo: serverData ? {
390
+ name: serverData.name || id,
391
+ type: serverData.server?.type || 'stdio'
392
+ } : {}
393
+ });
394
+ } catch (err) {
395
+ res.status(500).json({ success: false, error: err.message });
396
+ }
397
+ });
398
+
336
399
  module.exports = router;