sloth-d2c-mcp 1.0.4-beta70 → 1.0.4-beta72

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.
@@ -11,10 +11,22 @@ import * as path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { v4 as uuidv4 } from 'uuid';
13
13
  import open from 'open';
14
- import FileManager from './utils/file-manager.js';
14
+ import { fileManager } from './index.js';
15
15
  import * as flatted from 'flatted';
16
+ import { ComponentMappingSystem } from './component-mapping/index.js';
17
+ import multer from 'multer';
18
+ import { ImageMatcher } from './utils/image-matcher.js';
19
+ import { initOpenCV } from './utils/opencv-loader.js';
20
+ import { extractJson } from './utils/extract.js';
21
+ import { componentAnalysisPrompt, componentAnalysisPromptVue } from 'sloth-d2c-node/convert';
22
+ // 配置 multer 用于文件上传
23
+ const storage = multer.memoryStorage();
24
+ const upload = multer({
25
+ storage,
26
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
27
+ });
16
28
  // 导入默认提示词
17
- import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt } from 'sloth-d2c-node/convert';
29
+ import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, } from 'sloth-d2c-node/convert';
18
30
  // 保存 HTTP 服务器实例
19
31
  let httpServer = null;
20
32
  // 管理所有活跃的传输对象,按 sessionId 分类
@@ -26,6 +38,7 @@ const transports = {
26
38
  const pendingRequests = new Map(); // 等待中的认证请求
27
39
  let configStorage = {}; // 配置缓存
28
40
  let configManager = null; // 配置管理器实例
41
+ let mappingSystem = null; // 组件映射系统实例
29
42
  const __filename = fileURLToPath(import.meta.url);
30
43
  const __dirname = path.dirname(__filename);
31
44
  // 获取端口号
@@ -40,10 +53,11 @@ export async function loadConfig(mcpServer, configManagerInstance) {
40
53
  // 检查配置文件是否存在
41
54
  const configExists = await configManager.exists();
42
55
  Logger.log(`配置文件存在: ${configExists ? '是' : '否'}`);
56
+ let config = {};
43
57
  if (configExists) {
44
58
  // 如果配置文件存在,加载并显示配置信息
45
59
  try {
46
- const config = await configManager.load();
60
+ config = await configManager.load();
47
61
  mcpServer.setConfig(config.mcp);
48
62
  Logger.log(`当前配置内容: ${JSON.stringify(config, null, 2)}`);
49
63
  }
@@ -51,6 +65,62 @@ export async function loadConfig(mcpServer, configManagerInstance) {
51
65
  Logger.warn(`读取配置文件失败: ${error}`);
52
66
  }
53
67
  }
68
+ // 初始化默认框架配置
69
+ if (!config.frameworks || config.frameworks.length === 0) {
70
+ Logger.log('初始化默认框架配置...');
71
+ if (!config.frameworks) {
72
+ config.frameworks = [];
73
+ }
74
+ if (typeof config.frameworks[0] === 'string') {
75
+ config.frameworks = config.frameworks.map((f) => {
76
+ return {
77
+ label: f.charAt(0).toUpperCase() + f.slice(1),
78
+ value: f,
79
+ isCustom: f !== 'react' && f !== 'vue', // todo: 暂时兼容,仅 react 是默认框架,若新增框架需调整
80
+ };
81
+ });
82
+ }
83
+ if (!config.frameworks.find((fw) => fw.value === 'react') || !config.frameworks.find((fw) => fw.value === 'vue')) {
84
+ const newFrameworks = config.frameworks.filter((fw) => fw.value !== 'react' || fw.value !== 'vue');
85
+ config.frameworks = [{ value: 'react', label: 'React' }, { value: 'vue', label: 'Vue' }, ...newFrameworks];
86
+ await configManager.save(config);
87
+ }
88
+ }
89
+ // 检查 react.json 是否存在,如果不存在则创建
90
+ const reactConfigExists = await configManager.frameworkConfigExists('react');
91
+ if (!reactConfigExists) {
92
+ await configManager.saveFrameworkConfig('react', {
93
+ chunkOptimizePrompt: chunkOptimizeCodePrompt,
94
+ aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
95
+ finalOptimizePrompt: finalOptimizeCodePrompt,
96
+ componentAnalysisPrompt: componentAnalysisPrompt,
97
+ });
98
+ const reactConfigPath = configManager.getFrameworkConfigPath('react');
99
+ Logger.log(`已创建默认 react 配置文件: ${reactConfigPath}`);
100
+ }
101
+ // 检查 vue.json 是否存在,如果不存在则创建
102
+ const vueConfigExists = await configManager.frameworkConfigExists('vue');
103
+ if (!vueConfigExists) {
104
+ await configManager.saveFrameworkConfig('vue', {
105
+ chunkOptimizePrompt: chunkOptimizeCodePromptVue,
106
+ aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
107
+ finalOptimizePrompt: finalOptimizeCodePromptVue,
108
+ componentAnalysisPrompt: componentAnalysisPromptVue,
109
+ });
110
+ const vueConfigPath = configManager.getFrameworkConfigPath('vue');
111
+ Logger.log(`已创建默认 vue 配置文件: ${vueConfigPath}`);
112
+ }
113
+ else {
114
+ const vueConfig = await configManager.loadFrameworkConfig('vue');
115
+ if (!vueConfig || Object.keys(vueConfig).length < 3) {
116
+ await configManager.saveFrameworkConfig('vue', {
117
+ chunkOptimizePrompt: chunkOptimizeCodePromptVue,
118
+ aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
119
+ finalOptimizePrompt: finalOptimizeCodePromptVue,
120
+ ...(vueConfig || {}),
121
+ });
122
+ }
123
+ }
54
124
  }
55
125
  catch (error) {
56
126
  Logger.error(`获取配置信息时出错: ${error}`);
@@ -61,6 +131,15 @@ export async function loadConfig(mcpServer, configManagerInstance) {
61
131
  export async function startHttpServer(port = PORT, mcpServer, configManagerInstance, isWeb) {
62
132
  const app = express();
63
133
  PORT = port;
134
+ // 初始化 OpenCV
135
+ try {
136
+ await initOpenCV();
137
+ Logger.log('✅ OpenCV.js 初始化成功');
138
+ }
139
+ catch (error) {
140
+ Logger.error('❌ OpenCV.js 初始化失败:', error);
141
+ Logger.warn('图像匹配功能将不可用,但不影响其他功能');
142
+ }
64
143
  // 存储配置管理器实例(如果提供)
65
144
  loadConfig(mcpServer, configManagerInstance);
66
145
  // if (configManagerInstance) {
@@ -90,10 +169,13 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
90
169
  app.use('/mcp', express.json());
91
170
  app.use('/submit', express.json());
92
171
  app.use('/saveNodes', express.json({
93
- limit: '100mb'
172
+ limit: '100mb',
94
173
  }));
95
174
  // 为认证端点添加中间件 - 需要同时支持 JSON 和表单数据
96
175
  app.use(express.urlencoded({ extended: true }));
176
+ app.use('/saveGlobalConfig', express.json());
177
+ app.use('/analyzeComponents', express.json());
178
+ app.use('/saveComponents', express.json());
97
179
  // 设置跨域
98
180
  app.use((req, res, next) => {
99
181
  res.header('Access-Control-Allow-Origin', '*');
@@ -234,20 +316,36 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
234
316
  try {
235
317
  const fileKey = req.query.fileKey;
236
318
  const nodeId = req.query.nodeId;
319
+ const framework = req.query.framework;
237
320
  if (fileKey) {
238
321
  // 如果提供了 fileKey,返回该 fileKey 的特定配置
239
322
  const globalConfig = await configManager.load();
240
- const fileConfig = globalConfig.fileConfigs?.[fileKey] || {};
323
+ // const fileConfig = globalConfig.fileConfigs?.[fileKey] || {}
324
+ const fileConfig = (await fileManager.loadConfigSetting(fileKey, nodeId)) || {};
241
325
  // 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
242
- const fileManager = new FileManager('d2c-mcp');
326
+ // const fileManager = new FileManager('d2c-mcp')
243
327
  const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
244
328
  const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
245
- // 合并用户自定义提示词和默认提示词
246
- const promptSetting = {
329
+ // 如果指定了框架,加载框架配置的提示词
330
+ let promptSetting = {
247
331
  chunkOptimizePrompt: savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt,
248
332
  aggregationOptimizePrompt: savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt,
249
333
  finalOptimizePrompt: savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt,
250
334
  };
335
+ if (framework) {
336
+ // 加载框架配置
337
+ const frameworkConfig = await configManager.loadFrameworkConfig(framework);
338
+ if (frameworkConfig && Object.keys(frameworkConfig).length > 0) {
339
+ // 框架配置优先级更高,但如果框架配置为空,则使用已保存的或默认的
340
+ promptSetting = {
341
+ chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || promptSetting.chunkOptimizePrompt,
342
+ aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
343
+ finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
344
+ };
345
+ }
346
+ }
347
+ // 获取框架列表
348
+ const frameworks = await configManager.getFrameworks();
251
349
  res.json({
252
350
  success: true,
253
351
  data: {
@@ -255,6 +353,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
255
353
  convertSetting: fileConfig.convertSetting,
256
354
  imageSetting: fileConfig.imageSetting,
257
355
  promptSetting: promptSetting,
356
+ frameworks: frameworks,
357
+ defaultFramework: globalConfig.defaultFramework || 'react',
258
358
  fileKey: fileKey,
259
359
  groupsData: groupsData,
260
360
  },
@@ -265,12 +365,26 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
265
365
  if (await configManager.exists()) {
266
366
  configStorage = await configManager.load();
267
367
  }
268
- // 返回默认提示词
269
- const promptSetting = {
368
+ // 如果指定了框架,加载框架配置的提示词
369
+ let promptSetting = {
270
370
  chunkOptimizePrompt: chunkOptimizeCodePrompt,
271
371
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
272
372
  finalOptimizePrompt: finalOptimizeCodePrompt,
273
373
  };
374
+ if (framework) {
375
+ // 加载框架配置
376
+ const frameworkConfig = await configManager.loadFrameworkConfig(framework);
377
+ if (frameworkConfig && Object.keys(frameworkConfig).length > 0) {
378
+ // 框架配置优先级更高
379
+ promptSetting = {
380
+ chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || promptSetting.chunkOptimizePrompt,
381
+ aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
382
+ finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
383
+ };
384
+ }
385
+ }
386
+ // 获取框架列表
387
+ const frameworks = await configManager.getFrameworks();
274
388
  res.json({
275
389
  success: true,
276
390
  data: {
@@ -278,6 +392,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
278
392
  convertSetting: configStorage.convertSetting || {},
279
393
  imageSetting: configStorage.imageSetting || {},
280
394
  promptSetting: promptSetting,
395
+ frameworks: frameworks,
281
396
  },
282
397
  });
283
398
  }
@@ -293,13 +408,117 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
293
408
  });
294
409
  app.get('/getHtml', async (req, res) => {
295
410
  // 加载现有配置
296
- const fileManager = new FileManager('d2c-mcp');
411
+ // const fileManager = new FileManager('d2c-mcp')
297
412
  const html = await fileManager.loadFile(req.query.fileKey, req.query.nodeId, 'absolute.html');
298
413
  res.json({
299
414
  success: true,
300
415
  data: html,
301
416
  });
302
417
  });
418
+ // 获取全局配置接口
419
+ app.get('/getGlobalConfig', async (req, res) => {
420
+ try {
421
+ const framework = req.query.framework;
422
+ const globalConfig = await configManager.load();
423
+ let frameworks = (await configManager.getFrameworks()) || [
424
+ { value: 'react', label: 'React' },
425
+ { value: 'vue', label: 'Vue' },
426
+ ];
427
+ const defaultFramework = globalConfig?.defaultFramework || 'react';
428
+ let promptSetting = {};
429
+ const fw = frameworks.find((f) => f.value === framework) ? framework : defaultFramework;
430
+ const frameworkConfig = await configManager.loadFrameworkConfig(fw);
431
+ promptSetting[fw] = frameworkConfig;
432
+ res.json({
433
+ success: true,
434
+ data: {
435
+ frameworks,
436
+ defaultFramework,
437
+ promptSetting,
438
+ },
439
+ message: '获取成功',
440
+ });
441
+ }
442
+ catch (error) {
443
+ Logger.error('获取全局配置失败:', error);
444
+ res.status(500).json({
445
+ success: false,
446
+ message: '获取全局配置失败',
447
+ error: error instanceof Error ? error.message : String(error),
448
+ });
449
+ }
450
+ });
451
+ // 保存全局配置接口
452
+ app.post('/saveGlobalConfig', async (req, res) => {
453
+ try {
454
+ const { token, value } = req.body;
455
+ if (!token || !value) {
456
+ res.status(400).json({
457
+ success: false,
458
+ message: '请求体中缺少 token 或 value',
459
+ });
460
+ return;
461
+ }
462
+ const { frameworks, defaultFramework, promptSetting: promptSettingMap } = value;
463
+ const globalConfig = await configManager.load();
464
+ globalConfig.frameworks = frameworks;
465
+ globalConfig.defaultFramework = defaultFramework;
466
+ await Promise.all(Object.entries(promptSettingMap).map(([key, value]) => configManager.saveFrameworkConfig(key, value)));
467
+ await configManager.save(globalConfig);
468
+ res.json({
469
+ success: true,
470
+ message: '保存全局配置成功',
471
+ });
472
+ }
473
+ catch (error) {
474
+ Logger.error('保存全局配置失败:', error);
475
+ res.status(500).json({
476
+ success: false,
477
+ message: '保存全局配置失败',
478
+ error: error instanceof Error ? error.message : String(error),
479
+ });
480
+ }
481
+ });
482
+ // 获取框架配置接口
483
+ app.get('/getFrameworkConfig', async (req, res) => {
484
+ try {
485
+ const framework = req.query.framework;
486
+ if (!framework) {
487
+ res.status(400).json({
488
+ success: false,
489
+ message: '缺少必要参数: framework',
490
+ });
491
+ return;
492
+ }
493
+ let promptSetting = {
494
+ chunkOptimizePrompt: chunkOptimizeCodePrompt,
495
+ aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
496
+ finalOptimizePrompt: finalOptimizeCodePrompt,
497
+ };
498
+ const frameworkConfig = await configManager.loadFrameworkConfig(framework);
499
+ if (frameworkConfig && Object.keys(frameworkConfig).length > 0) {
500
+ // 框架配置优先级更高
501
+ promptSetting = {
502
+ chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt,
503
+ aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt,
504
+ finalOptimizePrompt: frameworkConfig.finalOptimizePrompt,
505
+ };
506
+ }
507
+ res.json({
508
+ success: true,
509
+ data: promptSetting,
510
+ message: '获取框架配置成功',
511
+ });
512
+ }
513
+ catch (error) {
514
+ Logger.error('获取框架配置失败:', error);
515
+ res.status(500).json({
516
+ success: false,
517
+ message: '获取框架配置失败',
518
+ error: error instanceof Error ? error.message : String(error),
519
+ });
520
+ }
521
+ });
303
522
  // 认证页面
304
523
  app.get('/auth-page', (req, res) => {
305
524
  try {
@@ -372,7 +591,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
372
591
  Logger.log(`已保存 fileKey "${fileKey}" 的配置:`, value);
373
592
  }
374
593
  // 使用 fileManager 按 nodeId 保存 groupsData 和 promptSetting
375
- const fileManager = new FileManager('d2c-mcp');
594
+ // const fileManager = new FileManager('d2c-mcp')
376
595
  if (fileKey && value.groupsData && Array.isArray(value.groupsData)) {
377
596
  await fileManager.saveGroupsData(fileKey, nodeId, value.groupsData);
378
597
  Logger.log(`已保存 groupsData 到 fileKey "${fileKey}", nodeId "${nodeId}":`, value.groupsData.length, '个分组');
@@ -414,9 +633,9 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
414
633
  });
415
634
  return;
416
635
  }
417
- const fileManager = new FileManager('d2c-mcp');
636
+ // const fileManager = new FileManager('d2c-mcp')
418
637
  // 保存节点列表
419
- await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', (nodeList));
638
+ await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', nodeList);
420
639
  // 保存图片映射
421
640
  if (imageMap) {
422
641
  await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
@@ -450,6 +669,577 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
450
669
  message: '日志重连已启动',
451
670
  });
452
671
  });
672
+ // ==================== 组件映射相关接口 ====================
673
+ /**
674
+ * 获取项目根目录
675
+ */
676
+ function getProjectRoot() {
677
+ try {
678
+ const projectPath = fileManager.getWorkspaceRoot();
679
+ return projectPath || './';
680
+ }
681
+ catch (error) {
682
+ Logger.warn('获取根目录时出错,使用默认路径:', error);
683
+ return './';
684
+ }
685
+ }
686
+ /**
687
+ * 获取项目文件树
688
+ */
689
+ app.get('/getProjectFiles', async (req, res) => {
690
+ try {
691
+ const { directory = '' } = req.query;
692
+ const projectPath = getProjectRoot();
693
+ const targetPath = path.join(projectPath, directory);
694
+ Logger.log(`获取项目文件树: ${targetPath}`);
695
+ // 排除的文件扩展名(静态资源和配置文件)
696
+ const excludeExtensions = [
697
+ // 图片
698
+ '.png',
699
+ '.jpg',
700
+ '.jpeg',
701
+ '.gif',
702
+ '.svg',
703
+ '.ico',
704
+ '.webp',
705
+ '.bmp',
706
+ // 视频/音频
707
+ '.mp4',
708
+ '.avi',
709
+ '.mov',
710
+ '.mp3',
711
+ '.wav',
712
+ // 字体
713
+ '.ttf',
714
+ '.woff',
715
+ '.woff2',
716
+ '.eot',
717
+ // 配置文件
718
+ '.json',
719
+ '.lock',
720
+ '.yaml',
721
+ '.yml',
722
+ '.toml',
723
+ // 文档
724
+ '.md',
725
+ '.txt',
726
+ '.pdf',
727
+ '.doc',
728
+ '.docx',
729
+ // 其他
730
+ '.zip',
731
+ '.tar',
732
+ '.gz',
733
+ '.rar',
734
+ '.map',
735
+ '.d.ts.map',
736
+ ];
737
+ // 递归读取文件树
738
+ const buildFileTree = async (dirPath, relativePath = '') => {
739
+ const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
740
+ const files = [];
741
+ for (const entry of entries) {
742
+ // 排除特殊目录
743
+ const excludeDirs = ['node_modules', 'dist', 'build', '.git', '.next', 'coverage', '.sloth', 'public', 'static', 'assets'];
744
+ if (excludeDirs.includes(entry.name) || entry.name.startsWith('.')) {
745
+ continue;
746
+ }
747
+ const entryRelativePath = path.join(relativePath, entry.name);
748
+ const entryFullPath = path.join(dirPath, entry.name);
749
+ if (entry.isDirectory()) {
750
+ const children = await buildFileTree(entryFullPath, entryRelativePath);
751
+ if (children.length > 0) {
752
+ files.push({
753
+ title: entry.name,
754
+ key: entryRelativePath,
755
+ path: entryRelativePath,
756
+ isLeaf: false,
757
+ selectable: false,
758
+ children,
759
+ });
760
+ }
761
+ }
762
+ else {
763
+ // 排除静态资源文件
764
+ const ext = path.extname(entry.name).toLowerCase();
765
+ if (!excludeExtensions.includes(ext)) {
766
+ files.push({
767
+ title: entry.name,
768
+ key: entryRelativePath,
769
+ path: entryRelativePath,
770
+ isLeaf: true,
771
+ selectable: true,
772
+ });
773
+ }
774
+ }
775
+ }
776
+ return files;
777
+ };
778
+ const fileTree = await buildFileTree(targetPath);
779
+ res.json({
780
+ success: true,
781
+ data: {
782
+ projectPath,
783
+ files: fileTree,
784
+ },
785
+ });
786
+ }
787
+ catch (error) {
788
+ Logger.error('获取项目文件树失败:', error);
789
+ res.status(500).json({
790
+ success: false,
791
+ message: '获取项目文件树失败',
792
+ error: error instanceof Error ? error.message : String(error),
793
+ });
794
+ }
795
+ });
796
+ /**
797
+ * 分析项目中的组件
798
+ */
799
+ app.post('/analyzeComponents', async (req, res) => {
800
+ try {
801
+ const { filePaths, fileKey, nodeId } = req.body;
802
+ if (!filePaths || !Array.isArray(filePaths)) {
803
+ res.status(400).json({
804
+ success: false,
805
+ message: '缺少必要参数: filePaths',
806
+ });
807
+ return;
808
+ }
809
+ Logger.log(`分析 ${filePaths.length} 个项目文件`);
810
+ const projectPath = getProjectRoot();
811
+ const results = [];
812
+ // 尝试加载保存的提示词设置(如果提供了 fileKey 和 nodeId)
813
+ let savedPromptSetting = null;
814
+ if (fileKey && nodeId) {
815
+ try {
816
+ savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
817
+ Logger.log(`已加载提示词设置: fileKey=${fileKey}, nodeId=${nodeId}`);
818
+ }
819
+ catch (error) {
820
+ Logger.warn(`加载提示词设置失败: ${error}`);
821
+ }
822
+ }
823
+ const frameworkGuidePrompt = savedPromptSetting?.frameworkGuidePrompt;
824
+ const _componentAnalysisPrompt = savedPromptSetting?.componentAnalysisPrompt || componentAnalysisPrompt;
825
+ // 读取并分析每个文件
826
+ for (const filePath of filePaths) {
827
+ try {
828
+ const fullPath = path.join(projectPath, filePath);
829
+ // 安全检查:确保文件在项目目录内
830
+ if (!fullPath.startsWith(projectPath)) {
831
+ results.push({
832
+ success: false,
833
+ filename: filePath,
834
+ error: '无权访问该文件',
835
+ });
836
+ continue;
837
+ }
838
+ // 读取文件内容
839
+ const content = await fs.promises.readFile(fullPath, 'utf-8');
840
+ const filename = path.basename(filePath);
841
+ // 使用 MCP Sampling 进行实际分析
842
+ if (mcpServer) {
843
+ try {
844
+ // 构建提示词,替换占位符
845
+ const prompt = _componentAnalysisPrompt.replace(/{filename}/g, filename).replace(/{fileContent}/g, content);
846
+ // 调用 MCP Sampling API
847
+ const { content: { text }, } = await mcpServer.server.createMessage({
848
+ messages: [
849
+ frameworkGuidePrompt && {
850
+ role: 'user',
851
+ content: {
852
+ type: 'text',
853
+ text: frameworkGuidePrompt,
854
+ },
855
+ },
856
+ {
857
+ role: 'user',
858
+ content: {
859
+ type: 'text',
860
+ text: prompt,
861
+ },
862
+ },
863
+ ].filter(Boolean),
864
+ maxTokens: 8000,
865
+ }, { timeout: 2 * 60 * 1000 });
866
+ // 解析 AI 返回的 JSON
867
+ const extractResult = extractJson(text);
868
+ if (extractResult.state === 'successful-parse' && extractResult.value) {
869
+ const analysisResult = extractResult.value;
870
+ // 构建组件信息
871
+ const component = {
872
+ id: `comp-${Date.now()}-${Math.random()}`,
873
+ name: analysisResult.componentName || path.basename(filePath, path.extname(filePath)),
874
+ type: 'custom',
875
+ path: filePath,
876
+ import: analysisResult.import,
877
+ props: analysisResult.props || [],
878
+ description: analysisResult.description || '组件描述',
879
+ };
880
+ results.push({
881
+ success: true,
882
+ component,
883
+ filename: filePath,
884
+ });
885
+ Logger.log(`成功分析组件: ${filePath}, 组件名: ${component.name}`);
886
+ }
887
+ else {
888
+ // JSON 解析失败,使用默认值
889
+ Logger.warn(`解析组件分析结果失败: ${extractResult.error}, 文件: ${filePath}`);
890
+ results.push({
891
+ success: true,
892
+ component: {
893
+ id: `comp-${Date.now()}-${Math.random()}`,
894
+ name: path.basename(filePath, path.extname(filePath)),
895
+ type: 'custom',
896
+ path: filePath,
897
+ import: `import { ${path.basename(filePath, path.extname(filePath))} } from './${filePath}'`,
898
+ props: [],
899
+ description: '组件描述',
900
+ },
901
+ filename: filePath,
902
+ });
903
+ }
904
+ }
905
+ catch (samplingError) {
906
+ Logger.error(`调用采样 API 失败: ${filePath}`, samplingError);
907
+ // 采样失败时使用默认值
908
+ results.push({
909
+ success: true,
910
+ component: {
911
+ id: `comp-${Date.now()}-${Math.random()}`,
912
+ name: path.basename(filePath, path.extname(filePath)),
913
+ type: 'custom',
914
+ path: filePath,
915
+ import: `import { ${path.basename(filePath, path.extname(filePath))} } from './${filePath}'`,
916
+ props: [],
917
+ description: '组件描述',
918
+ },
919
+ filename: filePath,
920
+ });
921
+ }
922
+ }
923
+ else {
924
+ // MCP 服务器未初始化,使用默认值
925
+ Logger.warn('MCP 服务器未初始化,使用默认组件信息');
926
+ results.push({
927
+ success: true,
928
+ component: {
929
+ id: `comp-${Date.now()}-${Math.random()}`,
930
+ name: path.basename(filePath, path.extname(filePath)),
931
+ type: 'custom',
932
+ path: filePath,
933
+ import: `import { ${path.basename(filePath, path.extname(filePath))} } from './${filePath}'`,
934
+ props: [],
935
+ description: '组件描述',
936
+ },
937
+ filename: filePath,
938
+ });
939
+ }
940
+ }
941
+ catch (error) {
942
+ Logger.error(`读取文件失败: ${filePath}`, error);
943
+ results.push({
944
+ success: false,
945
+ filename: filePath,
946
+ error: error instanceof Error ? error.message : String(error),
947
+ });
948
+ }
949
+ }
950
+ const succeeded = results.filter((r) => r.success).length;
951
+ const failed = results.filter((r) => !r.success).length;
952
+ res.json({
953
+ success: true,
954
+ data: {
955
+ total: filePaths.length,
956
+ succeeded,
957
+ failed,
958
+ components: results.filter((r) => r.success).map((r) => r.component),
959
+ errors: results
960
+ .filter((r) => !r.success)
961
+ .map((r) => ({
962
+ filename: r.filename,
963
+ error: r.error,
964
+ })),
965
+ },
966
+ });
967
+ }
968
+ catch (error) {
969
+ Logger.error('分析项目组件失败:', error);
970
+ res.status(500).json({
971
+ success: false,
972
+ message: '分析项目组件失败',
973
+ error: error instanceof Error ? error.message : String(error),
974
+ });
975
+ }
976
+ });
977
+ /**
978
+ * 保存项目中的组件到components.json
979
+ */
980
+ app.post('/saveComponents', async (req, res) => {
981
+ try {
982
+ const { components } = req.body;
983
+ if (!components || !Array.isArray(components)) {
984
+ res.status(400).json({
985
+ success: false,
986
+ message: '无效的组件数据',
987
+ });
988
+ return;
989
+ }
990
+ Logger.log(`准备保存 ${components.length} 个组件`);
991
+ // 读取现有组件
992
+ const existingComponents = await fileManager.loadComponentsDatabase();
993
+ // 直接追加(暂不处理冲突)
994
+ const allComponents = [...components, ...existingComponents];
995
+ // 保存到文件(带备份)
996
+ await fileManager.saveComponentsDatabase(allComponents);
997
+ Logger.log(`✅ 成功保存,当前共 ${allComponents.length} 个组件`);
998
+ res.json({
999
+ success: true,
1000
+ data: {
1001
+ added: components.length,
1002
+ total: allComponents.length,
1003
+ components: allComponents,
1004
+ },
1005
+ message: '组件导入成功',
1006
+ });
1007
+ }
1008
+ catch (error) {
1009
+ Logger.error('保存组件失败:', error);
1010
+ res.status(500).json({
1011
+ success: false,
1012
+ message: '保存组件失败',
1013
+ error: error instanceof Error ? error.message : String(error),
1014
+ });
1015
+ }
1016
+ });
1017
+ /**
1018
+ * 获取或创建映射系统实例
1019
+ */
1020
+ async function getMappingSystem() {
1021
+ if (!mappingSystem) {
1022
+ const projectPath = getProjectRoot();
1023
+ mappingSystem = new ComponentMappingSystem(projectPath);
1024
+ await mappingSystem.initialize();
1025
+ Logger.log('组件映射系统已初始化,项目路径:', projectPath);
1026
+ }
1027
+ return mappingSystem;
1028
+ }
1029
+ // 扫描项目组件接口(同时返回项目组件和外部依赖组件)
1030
+ app.get('/scanComponents', async (req, res) => {
1031
+ try {
1032
+ const { platform, includePaths, excludePaths } = req.query;
1033
+ if (!platform) {
1034
+ res.status(400).json({
1035
+ success: false,
1036
+ message: '缺少必要参数: platform',
1037
+ });
1038
+ return;
1039
+ }
1040
+ const projectPath = getProjectRoot();
1041
+ Logger.log('扫描项目组件:', { platform, includePaths, excludePaths });
1042
+ // 读取外部依赖组件(从 .sloth/components.json)
1043
+ let projectComponents = [];
1044
+ const slothPath = path.join(projectPath, '.sloth', 'components.json');
1045
+ const fs = await import('fs/promises');
1046
+ try {
1047
+ await fs.access(slothPath);
1048
+ // 文件存在,读取内容
1049
+ const fileContent = await fs.readFile(slothPath, 'utf-8');
1050
+ const componentsData = JSON.parse(fileContent);
1051
+ // 验证数据结构
1052
+ if (Array.isArray(componentsData)) {
1053
+ // 验证每个组件的基本字段
1054
+ projectComponents = componentsData
1055
+ .filter((comp) => comp && comp.id && comp.name && comp.path)
1056
+ .map((c) => ({
1057
+ id: c.id,
1058
+ name: c.name,
1059
+ type: c.type || 'custom',
1060
+ path: c.path,
1061
+ props: (c.props || []).map((p) => ({
1062
+ name: p.name,
1063
+ type: p.type || 'any',
1064
+ required: p.required || false,
1065
+ })),
1066
+ description: c.description || c.metadata?.description,
1067
+ import: c.import,
1068
+ }));
1069
+ Logger.log(`从 .sloth/components.json 加载了 ${projectComponents.length} 个项目组件`);
1070
+ }
1071
+ else {
1072
+ Logger.warn('.sloth/components.json 格式错误:应为组件数组');
1073
+ }
1074
+ }
1075
+ catch (error) {
1076
+ // 文件不存在或读取失败,忽略错误,继续返回项目组件
1077
+ if (error.code !== 'ENOENT') {
1078
+ Logger.warn('读取外部依赖组件失败:', error.message);
1079
+ }
1080
+ }
1081
+ res.json({
1082
+ success: true,
1083
+ data: {
1084
+ total: projectComponents.length,
1085
+ platform,
1086
+ projectComponents, // 项目组件
1087
+ },
1088
+ });
1089
+ }
1090
+ catch (error) {
1091
+ Logger.error('扫描项目组件失败:', error);
1092
+ res.status(500).json({
1093
+ success: false,
1094
+ message: '扫描项目组件失败',
1095
+ error: error instanceof Error ? error.message : String(error),
1096
+ });
1097
+ }
1098
+ });
1099
+ // 建议组件映射接口(基于历史组件截图匹配)
1100
+ app.post('/suggestMappings', upload.single('currentScreenshot'), async (req, res) => {
1101
+ try {
1102
+ const { fileKey, nodeId, threshold = '0.8' } = req.body;
1103
+ const currentScreenshotFile = req.file;
1104
+ if (!currentScreenshotFile || !fileKey) {
1105
+ res.status(400).json({
1106
+ success: false,
1107
+ message: '缺少必要参数: currentScreenshot, fileKey',
1108
+ });
1109
+ return;
1110
+ }
1111
+ Logger.log(`开始生成建议组件映射: fileKey=${fileKey}, nodeId=${nodeId}, threshold=${threshold}`);
1112
+ // 检查当前 fileKey + nodeId 是否已经有 groupsData
1113
+ const currentGroupsData = await fileManager.loadGroupsData(fileKey, nodeId);
1114
+ if (currentGroupsData && currentGroupsData.length > 0) {
1115
+ Logger.log(`当前设计稿已有 ${currentGroupsData.length} 个分组,跳过自动建议`);
1116
+ res.json({
1117
+ success: true,
1118
+ data: {
1119
+ suggestions: [],
1120
+ reason: 'current_has_groups',
1121
+ message: '当前设计稿已有分组数据,不执行自动建议',
1122
+ },
1123
+ });
1124
+ return;
1125
+ }
1126
+ Logger.log('当前设计稿无分组数据,继续执行自动建议');
1127
+ // 保存当前设计稿截图到临时文件
1128
+ const tempDir = path.join(fileManager.getBaseDir(), 'temp');
1129
+ await fs.promises.mkdir(tempDir, { recursive: true });
1130
+ const currentScreenshotPath = path.join(tempDir, `suggest_${Date.now()}.png`);
1131
+ await fs.promises.writeFile(currentScreenshotPath, currentScreenshotFile.buffer);
1132
+ // 加载整个项目所有 fileKey 的所有 groupsData(跨文件匹配)
1133
+ const allProjectGroupsData = await fileManager.loadAllProjectGroupsData();
1134
+ if (!allProjectGroupsData || allProjectGroupsData.length === 0) {
1135
+ Logger.log('未找到历史分组数据,无法生成建议');
1136
+ await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1137
+ res.json({
1138
+ success: true,
1139
+ data: { suggestions: [] },
1140
+ });
1141
+ return;
1142
+ }
1143
+ Logger.log(`从整个项目加载了 ${allProjectGroupsData.length} 个节点的历史数据`);
1144
+ const matchTasks = [];
1145
+ for (const { fileKey: storedFileKey, nodeId: storedNodeId, groups } of allProjectGroupsData) {
1146
+ const normalizedNodeId = storedNodeId === 'root' ? undefined : storedNodeId;
1147
+ for (const group of groups) {
1148
+ // 只处理标记的分组
1149
+ if (!group.marked || !group.screenshot)
1150
+ continue;
1151
+ const screenshotPath = fileManager.getScreenshotPath(storedFileKey, normalizedNodeId, group.screenshot.hash);
1152
+ const exists = await fileManager.screenshotExists(storedFileKey, normalizedNodeId, group.screenshot.hash);
1153
+ if (!exists)
1154
+ continue;
1155
+ matchTasks.push({
1156
+ path: screenshotPath,
1157
+ groupData: {
1158
+ fileKey: storedFileKey,
1159
+ nodeId: normalizedNodeId ?? 'root',
1160
+ componentMapping: group.componentMapping,
1161
+ userPrompt: group.userPrompt,
1162
+ originalRect: group.rect,
1163
+ componentName: group.componentName, // 传递组件名称
1164
+ screenshot: group.screenshot, // 传递截图信息(包含 hash)
1165
+ },
1166
+ });
1167
+ }
1168
+ }
1169
+ if (matchTasks.length === 0) {
1170
+ Logger.log('没有可用的历史截图,返回空建议');
1171
+ await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1172
+ res.json({
1173
+ success: true,
1174
+ data: { suggestions: [] },
1175
+ });
1176
+ return;
1177
+ }
1178
+ Logger.log(`开始匹配,共 ${matchTasks.length} 个历史组件截图`);
1179
+ const matcher = new ImageMatcher();
1180
+ const matches = await matcher.batchMatch(currentScreenshotPath, matchTasks, parseFloat(threshold));
1181
+ Logger.log(`匹配完成,找到 ${matches.length} 个相似组件`);
1182
+ matches.forEach((match, index) => {
1183
+ Logger.log(` 匹配 ${index + 1}: 来源 fileKey=${match.groupData?.fileKey}, nodeId=${match.groupData?.nodeId}, 组件=${match.groupData?.componentName || 'N/A'}, 相似度=${Math.round(match.confidence * 100)}%`);
1184
+ });
1185
+ await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1186
+ res.json({
1187
+ success: true,
1188
+ data: {
1189
+ suggestions: matches.map((match) => ({
1190
+ confidence: Math.round(match.confidence * 100) / 100,
1191
+ position: match.position,
1192
+ componentMapping: match.groupData?.componentMapping,
1193
+ userPrompt: match.groupData?.userPrompt,
1194
+ originalRect: match.groupData?.originalRect,
1195
+ fileKey: match.groupData?.fileKey,
1196
+ nodeId: match.groupData?.nodeId,
1197
+ componentName: match.groupData?.componentName,
1198
+ screenshotHash: match.groupData?.screenshot?.hash, // 返回截图 hash,用于匹配组件 signature
1199
+ })),
1200
+ },
1201
+ });
1202
+ }
1203
+ catch (error) {
1204
+ Logger.error('建议组件映射失败:', error);
1205
+ res.status(500).json({
1206
+ success: false,
1207
+ message: '建议组件映射失败',
1208
+ error: error instanceof Error ? error.message : String(error),
1209
+ });
1210
+ }
1211
+ });
1212
+ // 上传截图接口
1213
+ app.post('/uploadScreenshot', upload.single('file'), async (req, res) => {
1214
+ try {
1215
+ const { fileKey, nodeId, hash } = req.body;
1216
+ const file = req.file;
1217
+ if (!file || !fileKey || !nodeId || !hash) {
1218
+ res.status(400).json({
1219
+ success: false,
1220
+ message: '缺少必要参数: file, fileKey, nodeId, hash',
1221
+ });
1222
+ return;
1223
+ }
1224
+ Logger.log(`接收到截图上传请求: fileKey=${fileKey}, nodeId=${nodeId}, hash=${hash}`);
1225
+ // 保存截图到 .sloth/{fileKey}/{nodeId}/screenshots/{hash}.png
1226
+ await fileManager.saveScreenshot(fileKey, nodeId, hash, file.buffer);
1227
+ Logger.log(`截图上传成功: ${hash}.png`);
1228
+ res.json({
1229
+ success: true,
1230
+ message: '截图上传成功',
1231
+ hash,
1232
+ });
1233
+ }
1234
+ catch (error) {
1235
+ Logger.error('上传截图失败:', error);
1236
+ res.status(500).json({
1237
+ success: false,
1238
+ message: '上传截图失败',
1239
+ error: error instanceof Error ? error.message : String(error),
1240
+ });
1241
+ }
1242
+ });
453
1243
  // 启动 HTTP 服务器,监听端口
454
1244
  httpServer = app.listen(port, () => {
455
1245
  Logger.log(`HTTP server listening on port ${port}`);
@@ -458,6 +1248,9 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
458
1248
  Logger.log(`StreamableHTTP endpoint available at http://localhost:${port}/mcp`);
459
1249
  Logger.log(`Auth endpoints available at http://localhost:${port}/auth-page`);
460
1250
  Logger.log(`MCP Config at http://localhost:${port}/getConfig`);
1251
+ Logger.log(`Global Config at http://localhost:${port}/getGlobalConfig`);
1252
+ Logger.log(`Save Global Config endpoint at http://localhost:${port}/saveGlobalConfig`);
1253
+ Logger.log(`Framework Config at http://localhost:${port}/getFrameworkConfig`);
461
1254
  Logger.log(`Save Nodes endpoint available at http://localhost:${port}/saveNodes`);
462
1255
  Logger.log(`Logging reconnect endpoint available at http://localhost:${port}/reconnect-logging`);
463
1256
  });