@adversity/coding-tool-x 2.5.1 → 2.6.1

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,416 @@
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
+ * GET /api/plugins/market
37
+ */
38
+ router.get('/market', async (req, res) => {
39
+ try {
40
+ const plugins = await pluginsService.getMarketPlugins();
41
+
42
+ res.json({
43
+ success: true,
44
+ plugins
45
+ });
46
+ } catch (err) {
47
+ console.error('[Plugins API] Get market plugins error:', err);
48
+ res.status(500).json({
49
+ success: false,
50
+ message: err.message
51
+ });
52
+ }
53
+ });
54
+
55
+ /**
56
+ * 安装插件
57
+ * POST /api/plugins/install
58
+ * Body: { directory, repo: { owner, name, branch } }
59
+ */
60
+ router.post('/install', async (req, res) => {
61
+ try {
62
+ const { directory, repo, gitUrl } = req.body;
63
+
64
+ // Support both new format (directory + repo) and legacy format (gitUrl)
65
+ let installUrl;
66
+ if (directory && repo) {
67
+ installUrl = `https://github.com/${repo.owner}/${repo.name}/tree/${repo.branch || 'main'}/${directory}`;
68
+ } else if (gitUrl) {
69
+ installUrl = gitUrl;
70
+ } else {
71
+ return res.status(400).json({
72
+ success: false,
73
+ message: 'Either (directory + repo) or gitUrl is required'
74
+ });
75
+ }
76
+
77
+ const result = await pluginsService.installPlugin(installUrl);
78
+
79
+ if (!result.success) {
80
+ return res.status(400).json({
81
+ success: false,
82
+ message: result.error
83
+ });
84
+ }
85
+
86
+ res.json({
87
+ success: true,
88
+ plugin: result.plugin,
89
+ message: `Plugin "${result.plugin.name}" installed successfully`
90
+ });
91
+ } catch (err) {
92
+ console.error('[Plugins API] Install plugin error:', err);
93
+ res.status(500).json({
94
+ success: false,
95
+ message: err.message
96
+ });
97
+ }
98
+ });
99
+
100
+ // ==================== 仓库管理 API ====================
101
+
102
+ /**
103
+ * 获取插件仓库列表
104
+ * GET /api/plugins/repos
105
+ */
106
+ router.get('/repos', (req, res) => {
107
+ try {
108
+ const repos = pluginsService.getRepos();
109
+ res.json({
110
+ success: true,
111
+ repos
112
+ });
113
+ } catch (err) {
114
+ console.error('[Plugins API] Get repos error:', err);
115
+ res.status(500).json({
116
+ success: false,
117
+ message: err.message
118
+ });
119
+ }
120
+ });
121
+
122
+ /**
123
+ * 添加插件仓库
124
+ * POST /api/plugins/repos
125
+ * Body: { url, name, description }
126
+ */
127
+ router.post('/repos', (req, res) => {
128
+ try {
129
+ const repo = req.body;
130
+
131
+ if (!repo || !repo.url) {
132
+ return res.status(400).json({
133
+ success: false,
134
+ message: 'Repository URL is required'
135
+ });
136
+ }
137
+
138
+ const repos = pluginsService.addRepo(repo);
139
+
140
+ res.json({
141
+ success: true,
142
+ repos,
143
+ message: 'Repository added successfully'
144
+ });
145
+ } catch (err) {
146
+ console.error('[Plugins API] Add repo error:', err);
147
+ res.status(500).json({
148
+ success: false,
149
+ message: err.message
150
+ });
151
+ }
152
+ });
153
+
154
+ /**
155
+ * 删除插件仓库
156
+ * DELETE /api/plugins/repos/:owner/:name
157
+ */
158
+ router.delete('/repos/:owner/:name', (req, res) => {
159
+ try {
160
+ const { owner, name } = req.params;
161
+
162
+ const repos = pluginsService.removeRepo(owner, name);
163
+
164
+ res.json({
165
+ success: true,
166
+ repos,
167
+ message: 'Repository removed successfully'
168
+ });
169
+ } catch (err) {
170
+ console.error('[Plugins API] Remove repo error:', err);
171
+ res.status(500).json({
172
+ success: false,
173
+ message: err.message
174
+ });
175
+ }
176
+ });
177
+
178
+ /**
179
+ * 切换插件仓库启用状态
180
+ * PUT /api/plugins/repos/:owner/:name/toggle
181
+ * Body: { enabled }
182
+ */
183
+ router.put('/repos/:owner/:name/toggle', (req, res) => {
184
+ try {
185
+ const { owner, name } = req.params;
186
+ const { enabled } = req.body;
187
+
188
+ if (typeof enabled !== 'boolean') {
189
+ return res.status(400).json({
190
+ success: false,
191
+ message: 'enabled must be a boolean'
192
+ });
193
+ }
194
+
195
+ const repos = pluginsService.toggleRepo(owner, name, enabled);
196
+
197
+ res.json({
198
+ success: true,
199
+ repos,
200
+ message: `Repository ${enabled ? 'enabled' : 'disabled'} successfully`
201
+ });
202
+ } catch (err) {
203
+ console.error('[Plugins API] Toggle repo error:', err);
204
+ res.status(500).json({
205
+ success: false,
206
+ message: err.message
207
+ });
208
+ }
209
+ });
210
+
211
+ /**
212
+ * 同步仓库到 Claude Code marketplace
213
+ * POST /api/plugins/repos/sync
214
+ */
215
+ router.post('/repos/sync', async (req, res) => {
216
+ try {
217
+ const result = await pluginsService.syncRepos();
218
+
219
+ res.json({
220
+ success: true,
221
+ ...result,
222
+ message: 'Repositories synced successfully'
223
+ });
224
+ } catch (err) {
225
+ console.error('[Plugins API] Sync repos error:', err);
226
+ res.status(500).json({
227
+ success: false,
228
+ message: err.message
229
+ });
230
+ }
231
+ });
232
+
233
+ /**
234
+ * 同步本地插件列表
235
+ * POST /api/plugins/sync
236
+ */
237
+ router.post('/sync', async (req, res) => {
238
+ try {
239
+ const result = await pluginsService.syncPlugins();
240
+
241
+ res.json({
242
+ success: true,
243
+ ...result,
244
+ message: 'Plugins synced successfully'
245
+ });
246
+ } catch (err) {
247
+ console.error('[Plugins API] Sync plugins error:', err);
248
+ res.status(500).json({
249
+ success: false,
250
+ message: err.message
251
+ });
252
+ }
253
+ });
254
+
255
+ /**
256
+ * 获取插件 README
257
+ * GET /api/plugins/:name/readme
258
+ * Query: repoOwner, repoName, repoBranch, directory, source, repoUrl
259
+ */
260
+ router.get('/:name/readme', async (req, res) => {
261
+ try {
262
+ const { name } = req.params;
263
+ const { repoOwner, repoName, repoBranch, directory, source, repoUrl } = req.query;
264
+
265
+ const pluginInfo = {
266
+ name,
267
+ repoOwner,
268
+ repoName,
269
+ repoBranch,
270
+ directory,
271
+ source,
272
+ repoUrl
273
+ };
274
+
275
+ const readme = await pluginsService.getPluginReadme(pluginInfo);
276
+
277
+ res.json({
278
+ success: true,
279
+ readme
280
+ });
281
+ } catch (err) {
282
+ console.error('[Plugins API] Get plugin README error:', err);
283
+ res.status(500).json({
284
+ success: false,
285
+ message: err.message,
286
+ readme: ''
287
+ });
288
+ }
289
+ });
290
+
291
+ /**
292
+ * 获取单个插件详情
293
+ * GET /api/plugins/:name
294
+ */
295
+ router.get('/:name', (req, res) => {
296
+ try {
297
+ const { name } = req.params;
298
+
299
+ const plugin = pluginsService.getPlugin(name);
300
+
301
+ if (!plugin) {
302
+ return res.status(404).json({
303
+ success: false,
304
+ message: `Plugin "${name}" not found`
305
+ });
306
+ }
307
+
308
+ res.json({
309
+ success: true,
310
+ plugin
311
+ });
312
+ } catch (err) {
313
+ console.error('[Plugins API] Get plugin error:', err);
314
+ res.status(500).json({
315
+ success: false,
316
+ message: err.message
317
+ });
318
+ }
319
+ });
320
+
321
+ /**
322
+ * 卸载插件
323
+ * DELETE /api/plugins/:name
324
+ */
325
+ router.delete('/:name', (req, res) => {
326
+ try {
327
+ const { name } = req.params;
328
+
329
+ const result = pluginsService.uninstallPlugin(name);
330
+
331
+ if (!result.success) {
332
+ return res.status(400).json({
333
+ success: false,
334
+ message: result.error
335
+ });
336
+ }
337
+
338
+ res.json({
339
+ success: true,
340
+ message: result.message
341
+ });
342
+ } catch (err) {
343
+ console.error('[Plugins API] Uninstall plugin error:', err);
344
+ res.status(500).json({
345
+ success: false,
346
+ message: err.message
347
+ });
348
+ }
349
+ });
350
+
351
+ /**
352
+ * 切换插件启用状态
353
+ * PUT /api/plugins/:name/toggle
354
+ * Body: { enabled }
355
+ */
356
+ router.put('/:name/toggle', (req, res) => {
357
+ try {
358
+ const { name } = req.params;
359
+ const { enabled } = req.body;
360
+
361
+ if (typeof enabled !== 'boolean') {
362
+ return res.status(400).json({
363
+ success: false,
364
+ message: 'enabled must be a boolean'
365
+ });
366
+ }
367
+
368
+ const plugin = pluginsService.togglePlugin(name, enabled);
369
+
370
+ res.json({
371
+ success: true,
372
+ plugin,
373
+ message: `Plugin "${name}" ${enabled ? 'enabled' : 'disabled'} successfully`
374
+ });
375
+ } catch (err) {
376
+ console.error('[Plugins API] Toggle plugin error:', err);
377
+ res.status(500).json({
378
+ success: false,
379
+ message: err.message
380
+ });
381
+ }
382
+ });
383
+
384
+ /**
385
+ * 更新插件配置
386
+ * PUT /api/plugins/:name/config
387
+ * Body: { config }
388
+ */
389
+ router.put('/:name/config', (req, res) => {
390
+ try {
391
+ const { name } = req.params;
392
+ const { config } = req.body;
393
+
394
+ if (!config || typeof config !== 'object') {
395
+ return res.status(400).json({
396
+ success: false,
397
+ message: 'config must be an object'
398
+ });
399
+ }
400
+
401
+ const result = pluginsService.updatePluginConfig(name, config);
402
+
403
+ res.json({
404
+ success: true,
405
+ message: result.message
406
+ });
407
+ } catch (err) {
408
+ console.error('[Plugins API] Update plugin config error:', err);
409
+ res.status(500).json({
410
+ success: false,
411
+ message: err.message
412
+ });
413
+ }
414
+ });
415
+
416
+ module.exports = router;
@@ -10,7 +10,7 @@ const { broadcastLog } = require('../websocket-server');
10
10
 
11
11
  module.exports = (config) => {
12
12
  // GET /api/sessions/search/global - Search sessions across all projects
13
- router.get('/search/global', (req, res) => {
13
+ router.get('/search/global', async (req, res) => {
14
14
  try {
15
15
  const { keyword, context } = req.query;
16
16
 
@@ -19,7 +19,7 @@ module.exports = (config) => {
19
19
  }
20
20
 
21
21
  const contextLength = context ? parseInt(context) : 35;
22
- const results = searchSessionsAcrossProjects(config, keyword, contextLength);
22
+ const results = await searchSessionsAcrossProjects(config, keyword, contextLength);
23
23
 
24
24
  res.json({
25
25
  keyword,
@@ -33,10 +33,10 @@ module.exports = (config) => {
33
33
  });
34
34
 
35
35
  // GET /api/sessions/recent - Get recent sessions across all projects
36
- router.get('/recent/list', (req, res) => {
36
+ router.get('/recent/list', async (req, res) => {
37
37
  try {
38
38
  const limit = parseInt(req.query.limit) || 5;
39
- const sessions = getRecentSessions(config, limit);
39
+ const sessions = await getRecentSessions(config, limit);
40
40
  res.json({ sessions });
41
41
  } catch (error) {
42
42
  console.error('Error fetching recent sessions:', error);
@@ -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 basePricing = PRICING[model] || {};
45
- const pricing = resolvePricing('claude', basePricing, CLAUDE_BASE_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;