@adversity/coding-tool-x 2.3.0 → 2.4.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +8 -0
  3. package/dist/web/assets/icons-Dom8a0SN.js +1 -0
  4. package/dist/web/assets/index-CQeUIH7E.css +41 -0
  5. package/dist/web/assets/index-YrjlFzC4.js +14 -0
  6. package/dist/web/assets/naive-ui-BjMHakwv.js +1 -0
  7. package/dist/web/assets/vendors-DtJKdpSj.js +7 -0
  8. package/dist/web/assets/vue-vendor-VFuFB5f4.js +44 -0
  9. package/dist/web/index.html +6 -2
  10. package/package.json +2 -2
  11. package/src/commands/export-config.js +205 -0
  12. package/src/config/default.js +1 -1
  13. package/src/server/api/config-export.js +122 -0
  14. package/src/server/api/config-sync.js +155 -0
  15. package/src/server/api/config-templates.js +12 -6
  16. package/src/server/api/health-check.js +1 -89
  17. package/src/server/api/permissions.js +92 -69
  18. package/src/server/api/projects.js +2 -2
  19. package/src/server/api/sessions.js +70 -70
  20. package/src/server/api/skills.js +206 -0
  21. package/src/server/api/terminal.js +26 -0
  22. package/src/server/index.js +7 -11
  23. package/src/server/services/config-export-service.js +415 -0
  24. package/src/server/services/config-sync-service.js +515 -0
  25. package/src/server/services/config-templates-service.js +61 -38
  26. package/src/server/services/enhanced-cache.js +196 -0
  27. package/src/server/services/health-check.js +1 -315
  28. package/src/server/services/permission-templates-service.js +339 -0
  29. package/src/server/services/pty-manager.js +35 -1
  30. package/src/server/services/sessions.js +122 -44
  31. package/src/server/services/skill-service.js +252 -2
  32. package/src/server/services/workspace-service.js +44 -84
  33. package/src/server/websocket-server.js +4 -1
  34. package/dist/web/assets/index-dhun1bYQ.js +0 -3555
  35. package/dist/web/assets/index-hHb7DAda.css +0 -41
@@ -25,6 +25,7 @@ const express = require('express');
25
25
  const fs = require('fs');
26
26
  const path = require('path');
27
27
  const os = require('os');
28
+ const permissionTemplatesService = require('../services/permission-templates-service');
28
29
 
29
30
  const router = express.Router();
30
31
 
@@ -207,81 +208,15 @@ router.post('/all-allow', (req, res) => {
207
208
  });
208
209
 
209
210
  /**
210
- * 获取权限模版
211
+ * 获取所有权限模版(内置 + 自定义)
211
212
  * GET /api/permissions/templates
212
213
  */
213
214
  router.get('/templates', (req, res) => {
214
215
  try {
215
- const templates = {
216
- safe: {
217
- name: '安全模式',
218
- description: '仅允许只读命令,危险操作需要确认',
219
- allow: [
220
- 'Bash(cat:*)',
221
- 'Bash(ls:*)',
222
- 'Bash(pwd)',
223
- 'Bash(echo:*)',
224
- 'Bash(head:*)',
225
- 'Bash(tail:*)',
226
- 'Bash(grep:*)',
227
- 'Read(*)'
228
- ],
229
- deny: [
230
- 'Bash(rm:*)',
231
- 'Bash(sudo:*)',
232
- 'Bash(git push:*)',
233
- 'Bash(git reset --hard:*)',
234
- 'Bash(chmod:*)',
235
- 'Bash(chown:*)',
236
- 'Edit(*)'
237
- ]
238
- },
239
- balanced: {
240
- name: '平衡模式',
241
- description: '允许常用开发命令,危险操作需要确认',
242
- allow: [
243
- 'Bash(cat:*)',
244
- 'Bash(ls:*)',
245
- 'Bash(pwd)',
246
- 'Bash(echo:*)',
247
- 'Bash(head:*)',
248
- 'Bash(tail:*)',
249
- 'Bash(grep:*)',
250
- 'Bash(find:*)',
251
- 'Bash(git status)',
252
- 'Bash(git diff:*)',
253
- 'Bash(git log:*)',
254
- 'Bash(npm run:*)',
255
- 'Bash(pnpm:*)',
256
- 'Bash(yarn:*)',
257
- 'Read(*)',
258
- 'Edit(*)'
259
- ],
260
- deny: [
261
- 'Bash(rm -rf:*)',
262
- 'Bash(sudo:*)',
263
- 'Bash(git push --force:*)',
264
- 'Bash(git reset --hard:*)'
265
- ]
266
- },
267
- permissive: {
268
- name: '宽松模式',
269
- description: '允许大多数命令,仅阻止极度危险的操作',
270
- allow: [
271
- 'Bash(*)',
272
- 'Read(*)',
273
- 'Edit(*)'
274
- ],
275
- deny: [
276
- 'Bash(rm -rf /*)',
277
- 'Bash(sudo rm -rf:*)'
278
- ]
279
- }
280
- };
281
-
216
+ const templates = permissionTemplatesService.getAllTemplates();
282
217
  res.json({
283
218
  success: true,
284
- templates
219
+ data: templates
285
220
  });
286
221
  } catch (err) {
287
222
  console.error('[Permissions API] Get templates error:', err);
@@ -292,6 +227,94 @@ router.get('/templates', (req, res) => {
292
227
  }
293
228
  });
294
229
 
230
+ /**
231
+ * 获取单个权限模版
232
+ * GET /api/permissions/templates/:id
233
+ */
234
+ router.get('/templates/:id', (req, res) => {
235
+ try {
236
+ const template = permissionTemplatesService.getTemplateById(req.params.id);
237
+ if (!template) {
238
+ return res.status(404).json({
239
+ success: false,
240
+ message: '模版不存在'
241
+ });
242
+ }
243
+ res.json({
244
+ success: true,
245
+ data: template
246
+ });
247
+ } catch (err) {
248
+ console.error('[Permissions API] Get template error:', err);
249
+ res.status(500).json({
250
+ success: false,
251
+ message: err.message
252
+ });
253
+ }
254
+ });
255
+
256
+ /**
257
+ * 创建自定义权限模版
258
+ * POST /api/permissions/templates
259
+ */
260
+ router.post('/templates', (req, res) => {
261
+ try {
262
+ const template = permissionTemplatesService.createTemplate(req.body);
263
+ res.json({
264
+ success: true,
265
+ data: template,
266
+ message: '模版创建成功'
267
+ });
268
+ } catch (err) {
269
+ console.error('[Permissions API] Create template error:', err);
270
+ res.status(400).json({
271
+ success: false,
272
+ message: err.message
273
+ });
274
+ }
275
+ });
276
+
277
+ /**
278
+ * 更新自定义权限模版
279
+ * PUT /api/permissions/templates/:id
280
+ */
281
+ router.put('/templates/:id', (req, res) => {
282
+ try {
283
+ const template = permissionTemplatesService.updateTemplate(req.params.id, req.body);
284
+ res.json({
285
+ success: true,
286
+ data: template,
287
+ message: '模版更新成功'
288
+ });
289
+ } catch (err) {
290
+ console.error('[Permissions API] Update template error:', err);
291
+ res.status(400).json({
292
+ success: false,
293
+ message: err.message
294
+ });
295
+ }
296
+ });
297
+
298
+ /**
299
+ * 删除自定义权限模版
300
+ * DELETE /api/permissions/templates/:id
301
+ */
302
+ router.delete('/templates/:id', (req, res) => {
303
+ try {
304
+ permissionTemplatesService.deleteTemplate(req.params.id);
305
+ res.json({
306
+ success: true,
307
+ message: '模版删除成功'
308
+ });
309
+ } catch (err) {
310
+ console.error('[Permissions API] Delete template error:', err);
311
+ res.status(400).json({
312
+ success: false,
313
+ message: err.message
314
+ });
315
+ }
316
+ });
317
+
295
318
  /**
296
319
  * 获取各 CLI 工具的启动参数配置说明
297
320
  * GET /api/permissions/cli-config
@@ -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 之前定义,否则会被动态路由捕获
@@ -146,6 +146,12 @@ async function startServer(port) {
146
146
  // 命令执行权限 API
147
147
  app.use('/api/permissions', require('./api/permissions'));
148
148
 
149
+ // 配置导出/导入 API
150
+ app.use('/api/config-export', require('./api/config-export'));
151
+
152
+ // 配置同步 API
153
+ app.use('/api/config-sync', require('./api/config-sync'));
154
+
149
155
  // 健康检查 API
150
156
  app.use('/api/health-check', require('./api/health-check')(config));
151
157
 
@@ -259,7 +265,7 @@ function autoRestoreProxies() {
259
265
 
260
266
  // 启动时执行健康检查
261
267
  function performStartupHealthCheck() {
262
- const { healthCheckAllProjects, scanLegacySessionFiles } = require('./services/health-check');
268
+ const { healthCheckAllProjects } = require('./services/health-check');
263
269
  const { getProjects } = require('./services/sessions');
264
270
 
265
271
  try {
@@ -289,16 +295,6 @@ function performStartupHealthCheck() {
289
295
  console.log(chalk.green(` ✓ 所有 ${healthResult.summary.healthy} 个项目状态正常`));
290
296
  }
291
297
 
292
- // 扫描旧文件
293
- const legacyResult = scanLegacySessionFiles();
294
-
295
- if (legacyResult.found && legacyResult.projectCount > 0) {
296
- console.log(chalk.yellow(`\n ⚠ 发现 ${legacyResult.projectCount} 个项目的旧会话文件在全局目录`));
297
- console.log(chalk.gray(' 💡 提示: 可通过 Web UI 或 API 清理这些文件'));
298
- console.log(chalk.gray(` - Web UI: 设置 -> 系统维护 -> 清理旧文件`));
299
- console.log(chalk.gray(` - API: POST /api/health-check/clean-legacy`));
300
- }
301
-
302
298
  console.log('');
303
299
  } catch (err) {
304
300
  console.error(chalk.red(' ✗ 健康检查失败:'), err.message);