@adversity/coding-tool-x 2.5.1 → 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/CHANGELOG.md +14 -0
- package/dist/web/assets/icons-CNM9_Fh0.js +1 -0
- package/dist/web/assets/index-BcmuQT-z.css +41 -0
- package/dist/web/assets/{index-DZjidyED.js → index-Ej0MPDUI.js} +2 -2
- package/dist/web/index.html +3 -3
- package/package.json +3 -1
- package/src/commands/plugin.js +585 -0
- package/src/config/default.js +22 -3
- package/src/config/loader.js +6 -1
- package/src/index.js +229 -1
- package/src/server/api/dashboard.js +4 -3
- package/src/server/api/mcp.js +63 -0
- package/src/server/api/plugins.js +276 -0
- package/src/server/index.js +1 -0
- package/src/server/proxy-server.js +6 -3
- package/src/server/services/mcp-client.js +775 -0
- package/src/server/services/mcp-service.js +203 -0
- package/src/server/services/model-detector.js +350 -0
- package/src/server/services/plugins-service.js +177 -0
- package/src/server/services/pty-manager.js +65 -2
- package/src/server/services/speed-test.js +68 -37
- package/src/server/utils/pricing.js +32 -1
- package/src/ui/menu.js +1 -0
- package/dist/web/assets/icons-BALJo7bE.js +0 -1
- package/dist/web/assets/index-CvHZsWbE.css +0 -41
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
|
|
|
@@ -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({
|
package/src/server/api/mcp.js
CHANGED
|
@@ -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;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugins API 路由
|
|
3
|
+
*
|
|
4
|
+
* 管理 CTX 插件系统
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const express = require('express');
|
|
8
|
+
const { PluginsService } = require('../services/plugins-service');
|
|
9
|
+
|
|
10
|
+
const router = express.Router();
|
|
11
|
+
const pluginsService = new PluginsService();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取插件列表
|
|
15
|
+
* GET /api/plugins
|
|
16
|
+
*/
|
|
17
|
+
router.get('/', (req, res) => {
|
|
18
|
+
try {
|
|
19
|
+
const result = pluginsService.listPlugins();
|
|
20
|
+
|
|
21
|
+
res.json({
|
|
22
|
+
success: true,
|
|
23
|
+
...result
|
|
24
|
+
});
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[Plugins API] List plugins error:', err);
|
|
27
|
+
res.status(500).json({
|
|
28
|
+
success: false,
|
|
29
|
+
message: err.message
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 安装插件
|
|
36
|
+
* POST /api/plugins/install
|
|
37
|
+
* Body: { gitUrl }
|
|
38
|
+
*/
|
|
39
|
+
router.post('/install', async (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const { gitUrl } = req.body;
|
|
42
|
+
|
|
43
|
+
if (!gitUrl) {
|
|
44
|
+
return res.status(400).json({
|
|
45
|
+
success: false,
|
|
46
|
+
message: 'Git URL is required'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await pluginsService.installPlugin(gitUrl);
|
|
51
|
+
|
|
52
|
+
if (!result.success) {
|
|
53
|
+
return res.status(400).json({
|
|
54
|
+
success: false,
|
|
55
|
+
message: result.error
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
res.json({
|
|
60
|
+
success: true,
|
|
61
|
+
plugin: result.plugin,
|
|
62
|
+
message: `Plugin "${result.plugin.name}" installed successfully`
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error('[Plugins API] Install plugin error:', err);
|
|
66
|
+
res.status(500).json({
|
|
67
|
+
success: false,
|
|
68
|
+
message: err.message
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ==================== 仓库管理 API ====================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 获取插件仓库列表
|
|
77
|
+
* GET /api/plugins/repos
|
|
78
|
+
*/
|
|
79
|
+
router.get('/repos', (req, res) => {
|
|
80
|
+
try {
|
|
81
|
+
const repos = pluginsService.getRepos();
|
|
82
|
+
res.json({
|
|
83
|
+
success: true,
|
|
84
|
+
repos
|
|
85
|
+
});
|
|
86
|
+
} catch (err) {
|
|
87
|
+
console.error('[Plugins API] Get repos error:', err);
|
|
88
|
+
res.status(500).json({
|
|
89
|
+
success: false,
|
|
90
|
+
message: err.message
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 添加插件仓库
|
|
97
|
+
* POST /api/plugins/repos
|
|
98
|
+
* Body: { url, name, description }
|
|
99
|
+
*/
|
|
100
|
+
router.post('/repos', (req, res) => {
|
|
101
|
+
try {
|
|
102
|
+
const repo = req.body;
|
|
103
|
+
|
|
104
|
+
if (!repo || !repo.url) {
|
|
105
|
+
return res.status(400).json({
|
|
106
|
+
success: false,
|
|
107
|
+
message: 'Repository URL is required'
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const repos = pluginsService.addRepo(repo);
|
|
112
|
+
|
|
113
|
+
res.json({
|
|
114
|
+
success: true,
|
|
115
|
+
repos,
|
|
116
|
+
message: 'Repository added successfully'
|
|
117
|
+
});
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('[Plugins API] Add repo error:', err);
|
|
120
|
+
res.status(500).json({
|
|
121
|
+
success: false,
|
|
122
|
+
message: err.message
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 删除插件仓库
|
|
129
|
+
* DELETE /api/plugins/repos/:id
|
|
130
|
+
*/
|
|
131
|
+
router.delete('/repos/:id', (req, res) => {
|
|
132
|
+
try {
|
|
133
|
+
const { id } = req.params;
|
|
134
|
+
|
|
135
|
+
const repos = pluginsService.removeRepo(id);
|
|
136
|
+
|
|
137
|
+
res.json({
|
|
138
|
+
success: true,
|
|
139
|
+
repos,
|
|
140
|
+
message: 'Repository removed successfully'
|
|
141
|
+
});
|
|
142
|
+
} catch (err) {
|
|
143
|
+
console.error('[Plugins API] Remove repo error:', err);
|
|
144
|
+
res.status(500).json({
|
|
145
|
+
success: false,
|
|
146
|
+
message: err.message
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 获取单个插件详情
|
|
153
|
+
* GET /api/plugins/:name
|
|
154
|
+
*/
|
|
155
|
+
router.get('/:name', (req, res) => {
|
|
156
|
+
try {
|
|
157
|
+
const { name } = req.params;
|
|
158
|
+
|
|
159
|
+
const plugin = pluginsService.getPlugin(name);
|
|
160
|
+
|
|
161
|
+
if (!plugin) {
|
|
162
|
+
return res.status(404).json({
|
|
163
|
+
success: false,
|
|
164
|
+
message: `Plugin "${name}" not found`
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
res.json({
|
|
169
|
+
success: true,
|
|
170
|
+
plugin
|
|
171
|
+
});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.error('[Plugins API] Get plugin error:', err);
|
|
174
|
+
res.status(500).json({
|
|
175
|
+
success: false,
|
|
176
|
+
message: err.message
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 卸载插件
|
|
183
|
+
* DELETE /api/plugins/:name
|
|
184
|
+
*/
|
|
185
|
+
router.delete('/:name', (req, res) => {
|
|
186
|
+
try {
|
|
187
|
+
const { name } = req.params;
|
|
188
|
+
|
|
189
|
+
const result = pluginsService.uninstallPlugin(name);
|
|
190
|
+
|
|
191
|
+
if (!result.success) {
|
|
192
|
+
return res.status(400).json({
|
|
193
|
+
success: false,
|
|
194
|
+
message: result.error
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
res.json({
|
|
199
|
+
success: true,
|
|
200
|
+
message: result.message
|
|
201
|
+
});
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('[Plugins API] Uninstall plugin error:', err);
|
|
204
|
+
res.status(500).json({
|
|
205
|
+
success: false,
|
|
206
|
+
message: err.message
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* 切换插件启用状态
|
|
213
|
+
* PUT /api/plugins/:name/toggle
|
|
214
|
+
* Body: { enabled }
|
|
215
|
+
*/
|
|
216
|
+
router.put('/:name/toggle', (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
const { name } = req.params;
|
|
219
|
+
const { enabled } = req.body;
|
|
220
|
+
|
|
221
|
+
if (typeof enabled !== 'boolean') {
|
|
222
|
+
return res.status(400).json({
|
|
223
|
+
success: false,
|
|
224
|
+
message: 'enabled must be a boolean'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const plugin = pluginsService.togglePlugin(name, enabled);
|
|
229
|
+
|
|
230
|
+
res.json({
|
|
231
|
+
success: true,
|
|
232
|
+
plugin,
|
|
233
|
+
message: `Plugin "${name}" ${enabled ? 'enabled' : 'disabled'} successfully`
|
|
234
|
+
});
|
|
235
|
+
} catch (err) {
|
|
236
|
+
console.error('[Plugins API] Toggle plugin error:', err);
|
|
237
|
+
res.status(500).json({
|
|
238
|
+
success: false,
|
|
239
|
+
message: err.message
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 更新插件配置
|
|
246
|
+
* PUT /api/plugins/:name/config
|
|
247
|
+
* Body: { config }
|
|
248
|
+
*/
|
|
249
|
+
router.put('/:name/config', (req, res) => {
|
|
250
|
+
try {
|
|
251
|
+
const { name } = req.params;
|
|
252
|
+
const { config } = req.body;
|
|
253
|
+
|
|
254
|
+
if (!config || typeof config !== 'object') {
|
|
255
|
+
return res.status(400).json({
|
|
256
|
+
success: false,
|
|
257
|
+
message: 'config must be an object'
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const result = pluginsService.updatePluginConfig(name, config);
|
|
262
|
+
|
|
263
|
+
res.json({
|
|
264
|
+
success: true,
|
|
265
|
+
message: result.message
|
|
266
|
+
});
|
|
267
|
+
} catch (err) {
|
|
268
|
+
console.error('[Plugins API] Update plugin config error:', err);
|
|
269
|
+
res.status(500).json({
|
|
270
|
+
success: false,
|
|
271
|
+
message: err.message
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
module.exports = router;
|
package/src/server/index.js
CHANGED
|
@@ -134,6 +134,7 @@ async function startServer(port) {
|
|
|
134
134
|
app.use('/api/commands', require('./api/commands'));
|
|
135
135
|
app.use('/api/agents', require('./api/agents'));
|
|
136
136
|
app.use('/api/rules', require('./api/rules'));
|
|
137
|
+
app.use('/api/plugins', require('./api/plugins'));
|
|
137
138
|
|
|
138
139
|
// Web 终端 API
|
|
139
140
|
app.use('/api/terminal', require('./api/terminal'));
|
|
@@ -8,9 +8,10 @@ const { recordSuccess, recordFailure } = require('./services/channel-health');
|
|
|
8
8
|
const { broadcastLog, broadcastSchedulerState } = require('./websocket-server');
|
|
9
9
|
const { loadConfig } = require('../config/loader');
|
|
10
10
|
const DEFAULT_CONFIG = require('../config/default');
|
|
11
|
-
const { resolvePricing } = require('./utils/pricing');
|
|
11
|
+
const { resolvePricing, resolveModelPricing } = require('./utils/pricing');
|
|
12
12
|
const { recordRequest } = require('./services/statistics-service');
|
|
13
13
|
const { saveProxyStartTime, clearProxyStartTime, getProxyStartTime, getProxyRuntime } = require('./services/proxy-runtime');
|
|
14
|
+
const eventBus = require('../plugins/event-bus');
|
|
14
15
|
|
|
15
16
|
let proxyServer = null;
|
|
16
17
|
let proxyApp = null;
|
|
@@ -41,8 +42,8 @@ const ONE_MILLION = 1000000;
|
|
|
41
42
|
* @returns {number} 成本(美元)
|
|
42
43
|
*/
|
|
43
44
|
function calculateCost(model, tokens) {
|
|
44
|
-
const
|
|
45
|
-
const pricing =
|
|
45
|
+
const hardcodedPricing = PRICING[model] || {};
|
|
46
|
+
const pricing = resolveModelPricing('claude', model, hardcodedPricing, CLAUDE_BASE_PRICING);
|
|
46
47
|
|
|
47
48
|
const inputRate = typeof pricing.input === 'number' ? pricing.input : CLAUDE_BASE_PRICING.input;
|
|
48
49
|
const outputRate = typeof pricing.output === 'number' ? pricing.output : CLAUDE_BASE_PRICING.output;
|
|
@@ -399,6 +400,7 @@ async function startProxyServer(options = {}) {
|
|
|
399
400
|
proxyServer.listen(port, '127.0.0.1', () => {
|
|
400
401
|
console.log(`✅ Proxy server started on http://127.0.0.1:${port}`);
|
|
401
402
|
saveProxyStartTime('claude', preserveStartTime);
|
|
403
|
+
eventBus.emitSync('proxy:start', { channel: 'claude', port });
|
|
402
404
|
resolve({ success: true, port });
|
|
403
405
|
});
|
|
404
406
|
|
|
@@ -438,6 +440,7 @@ async function stopProxyServer(options = {}) {
|
|
|
438
440
|
if (clearStartTime) {
|
|
439
441
|
clearProxyStartTime('claude');
|
|
440
442
|
}
|
|
443
|
+
eventBus.emitSync('proxy:stop', { channel: 'claude' });
|
|
441
444
|
proxyServer = null;
|
|
442
445
|
proxyApp = null;
|
|
443
446
|
const stoppedPort = currentPort;
|