sloth-d2c-mcp 1.0.4-beta75 → 1.0.4-beta77

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.
@@ -10,15 +10,20 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { fileURLToPath } from 'url';
12
12
  import { v4 as uuidv4 } from 'uuid';
13
+ import { nanoid } from 'nanoid';
13
14
  import open from 'open';
14
15
  import { fileManager } from './index.js';
15
16
  import * as flatted from 'flatted';
16
- import { ComponentMappingSystem } from './component-mapping/index.js';
17
17
  import multer from 'multer';
18
18
  import { ImageMatcher } from './utils/image-matcher.js';
19
19
  import { initOpenCV } from './utils/opencv-loader.js';
20
20
  import { extractJson } from './utils/extract.js';
21
21
  import { componentAnalysisPrompt, componentAnalysisPromptVue } from 'sloth-d2c-node/convert';
22
+ import { SocketServer } from './socket-server.js';
23
+ /**
24
+ * 生成组件 ID(4位随机字符串)
25
+ */
26
+ export const generateComponentId = () => `comp_${nanoid(4)}`;
22
27
  // 配置 multer 用于文件上传
23
28
  const storage = multer.memoryStorage();
24
29
  const upload = multer({
@@ -29,6 +34,8 @@ const upload = multer({
29
34
  import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, } from 'sloth-d2c-node/convert';
30
35
  // 保存 HTTP 服务器实例
31
36
  let httpServer = null;
37
+ // 保存 Socket 服务器实例
38
+ let socketServer = null;
32
39
  // 管理所有活跃的传输对象,按 sessionId 分类
33
40
  const transports = {
34
41
  streamable: {}, // 流式 HTTP 传输
@@ -38,11 +45,14 @@ const transports = {
38
45
  const pendingRequests = new Map(); // 等待中的认证请求
39
46
  let configStorage = {}; // 配置缓存
40
47
  let configManager = null; // 配置管理器实例
41
- let mappingSystem = null; // 组件映射系统实例
42
48
  const __filename = fileURLToPath(import.meta.url);
43
49
  const __dirname = path.dirname(__filename);
44
50
  // 获取端口号
45
51
  let PORT = 0;
52
+ // 导出获取端口号的函数
53
+ export function getPort() {
54
+ return PORT;
55
+ }
46
56
  export async function loadConfig(mcpServer, configManagerInstance) {
47
57
  if (configManagerInstance) {
48
58
  configManager = configManagerInstance;
@@ -112,14 +122,13 @@ export async function loadConfig(mcpServer, configManagerInstance) {
112
122
  }
113
123
  else {
114
124
  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
- }
125
+ await configManager.saveFrameworkConfig('vue', {
126
+ chunkOptimizePrompt: chunkOptimizeCodePromptVue,
127
+ aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
128
+ finalOptimizePrompt: finalOptimizeCodePromptVue,
129
+ componentAnalysisPrompt: componentAnalysisPromptVue,
130
+ ...(vueConfig || {}),
131
+ });
123
132
  }
124
133
  }
125
134
  catch (error) {
@@ -326,11 +335,29 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
326
335
  // const fileManager = new FileManager('d2c-mcp')
327
336
  const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
328
337
  const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
338
+ let curFramework = fileConfig?.convertSetting?.framework;
339
+ // 获取框架列表
340
+ const frameworks = await configManager.getFrameworks();
341
+ if (!curFramework || !frameworks.find((fw) => fw.value === curFramework)) {
342
+ curFramework = globalConfig.defaultFramework || 'react';
343
+ }
344
+ const curFrameworkDefaultConfig = (await configManager.loadFrameworkConfig(curFramework)) ||
345
+ (curFramework !== 'vue'
346
+ ? {
347
+ chunkOptimizePrompt: chunkOptimizeCodePrompt,
348
+ aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
349
+ finalOptimizePrompt: finalOptimizeCodePrompt,
350
+ }
351
+ : {
352
+ chunkOptimizePrompt: chunkOptimizeCodePromptVue,
353
+ aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
354
+ finalOptimizePrompt: finalOptimizeCodePromptVue,
355
+ });
329
356
  // 如果指定了框架,加载框架配置的提示词
330
357
  let promptSetting = {
331
- chunkOptimizePrompt: savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt,
332
- aggregationOptimizePrompt: savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt,
333
- finalOptimizePrompt: savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt,
358
+ chunkOptimizePrompt: savedPromptSetting?.chunkOptimizePrompt || curFrameworkDefaultConfig.chunkOptimizePrompt,
359
+ aggregationOptimizePrompt: savedPromptSetting?.aggregationOptimizePrompt || curFrameworkDefaultConfig.aggregationOptimizePrompt,
360
+ finalOptimizePrompt: savedPromptSetting?.finalOptimizePrompt || curFrameworkDefaultConfig.finalOptimizePrompt,
334
361
  };
335
362
  if (framework) {
336
363
  // 加载框架配置
@@ -344,8 +371,6 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
344
371
  };
345
372
  }
346
373
  }
347
- // 获取框架列表
348
- const frameworks = await configManager.getFrameworks();
349
374
  res.json({
350
375
  success: true,
351
376
  data: {
@@ -428,7 +453,15 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
428
453
  let promptSetting = {};
429
454
  const fw = frameworks.find((f) => f.value === framework) ? framework : defaultFramework;
430
455
  const frameworkConfig = await configManager.loadFrameworkConfig(fw);
431
- promptSetting[fw] = frameworkConfig;
456
+ promptSetting[fw] = {
457
+ ...frameworkConfig,
458
+ enableFrameworkGuide: frameworkConfig.enableFrameworkGuide || false,
459
+ frameworkPrompt: frameworkConfig.frameworkPrompt || '',
460
+ chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || chunkOptimizeCodePrompt,
461
+ aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || aggregationOptimizeCodePrompt,
462
+ finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || finalOptimizeCodePrompt,
463
+ componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || componentAnalysisPrompt,
464
+ };
432
465
  res.json({
433
466
  success: true,
434
467
  data: {
@@ -494,14 +527,16 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
494
527
  chunkOptimizePrompt: chunkOptimizeCodePrompt,
495
528
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
496
529
  finalOptimizePrompt: finalOptimizeCodePrompt,
530
+ componentAnalysisPrompt: componentAnalysisPrompt,
497
531
  };
498
532
  const frameworkConfig = await configManager.loadFrameworkConfig(framework);
499
533
  if (frameworkConfig && Object.keys(frameworkConfig).length > 0) {
500
534
  // 框架配置优先级更高
501
535
  promptSetting = {
502
- chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt,
503
- aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt,
504
- finalOptimizePrompt: frameworkConfig.finalOptimizePrompt,
536
+ chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || promptSetting.chunkOptimizePrompt,
537
+ aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
538
+ finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
539
+ componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || promptSetting.componentAnalysisPrompt,
505
540
  };
506
541
  }
507
542
  res.json({
@@ -607,14 +642,19 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
607
642
  await configManager.save(globalConfig);
608
643
  Logger.log('已更新全局 MCP 配置');
609
644
  }
645
+ // 首先尝试主进程的 pendingRequests(主进程场景),查询对应token的Promise,判断是否是主进程提交的
610
646
  const request = pendingRequests.get(token);
647
+ Logger.log('submit pendingRequests', [...pendingRequests.keys()]);
611
648
  if (request && request.resolve) {
612
649
  request.resolve(JSON.stringify(value));
613
650
  res.send('提交成功!您可以关闭此窗口。');
651
+ return;
614
652
  }
615
- else {
616
- res.status(404).send('无效或已过期的 token');
617
- }
653
+ // 如果主进程查不到pendingRequests,则判断是子进程提交的,通过 Socket 发送提交数据给子进程
654
+ socketServer?.sendSubmitResponse(token, value);
655
+ Logger.log(`已通过 Socket 发送认证响应: token=${token}`);
656
+ res.send('提交成功!您可以关闭此窗口。');
657
+ return;
618
658
  }
619
659
  catch (error) {
620
660
  Logger.error('保存配置失败:', error);
@@ -793,6 +833,10 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
793
833
  });
794
834
  }
795
835
  });
836
+ // 多文件分析限制
837
+ const MAX_FILES = 10; // 最多 10 个文件
838
+ const MAX_TOTAL_CHARS = 40000; // 总内容最多 40000 字符
839
+ const MAX_FILE_CHARS = 5000; // 单文件最多 5000 字符
796
840
  /**
797
841
  * 分析项目中的组件
798
842
  */
@@ -806,10 +850,17 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
806
850
  });
807
851
  return;
808
852
  }
809
- Logger.log(`分析 ${filePaths.length} 个项目文件`);
853
+ // 限制文件数量
854
+ if (filePaths.length > MAX_FILES) {
855
+ res.status(400).json({
856
+ success: false,
857
+ message: `文件数量超过限制,最多支持 ${MAX_FILES} 个文件`,
858
+ });
859
+ return;
860
+ }
861
+ Logger.log(`批量分析 ${filePaths.length} 个项目文件`);
810
862
  const projectPath = getProjectRoot();
811
- const results = [];
812
- // 尝试加载保存的提示词设置(如果提供了 fileKey 和 nodeId)
863
+ // 尝试加载保存的提示词设置
813
864
  let savedPromptSetting = null;
814
865
  if (fileKey && nodeId) {
815
866
  try {
@@ -822,148 +873,163 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
822
873
  }
823
874
  const frameworkGuidePrompt = savedPromptSetting?.frameworkGuidePrompt;
824
875
  const _componentAnalysisPrompt = savedPromptSetting?.componentAnalysisPrompt || componentAnalysisPrompt;
825
- // 读取并分析每个文件
876
+ // 读取所有文件内容
877
+ const fileContents = [];
878
+ let totalChars = 0;
826
879
  for (const filePath of filePaths) {
827
880
  try {
828
881
  const fullPath = path.join(projectPath, filePath);
829
882
  // 安全检查:确保文件在项目目录内
830
883
  if (!fullPath.startsWith(projectPath)) {
831
- results.push({
832
- success: false,
833
- filename: filePath,
884
+ fileContents.push({
885
+ path: filePath,
886
+ content: '',
834
887
  error: '无权访问该文件',
835
888
  });
836
889
  continue;
837
890
  }
838
891
  // 读取文件内容
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
- }
892
+ let content = await fs.promises.readFile(fullPath, 'utf-8');
893
+ // 单文件截断
894
+ if (content.length > MAX_FILE_CHARS) {
895
+ content = content.slice(0, MAX_FILE_CHARS) + '\n// ... 内容已截断 ...';
896
+ Logger.warn(`文件 ${filePath} 内容过长,已截断至 ${MAX_FILE_CHARS} 字符`);
922
897
  }
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,
898
+ // 检查总字符数
899
+ if (totalChars + content.length > MAX_TOTAL_CHARS) {
900
+ Logger.warn(`总内容超过 ${MAX_TOTAL_CHARS} 字符限制,跳过文件: ${filePath}`);
901
+ fileContents.push({
902
+ path: filePath,
903
+ content: '',
904
+ error: '总内容超过限制,已跳过',
938
905
  });
906
+ continue;
939
907
  }
908
+ totalChars += content.length;
909
+ fileContents.push({
910
+ path: filePath,
911
+ content,
912
+ });
940
913
  }
941
914
  catch (error) {
942
915
  Logger.error(`读取文件失败: ${filePath}`, error);
943
- results.push({
944
- success: false,
945
- filename: filePath,
916
+ fileContents.push({
917
+ path: filePath,
918
+ content: '',
946
919
  error: error instanceof Error ? error.message : String(error),
947
920
  });
948
921
  }
949
922
  }
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
- });
923
+ // 过滤出成功读取的文件
924
+ const validFiles = fileContents.filter((f) => f.content && !f.error);
925
+ const errorFiles = fileContents.filter((f) => f.error);
926
+ if (validFiles.length === 0) {
927
+ res.json({
928
+ success: true,
929
+ data: {
930
+ total: filePaths.length,
931
+ succeeded: 0,
932
+ failed: errorFiles.length,
933
+ components: [],
934
+ errors: errorFiles.map((f) => ({ filename: f.path, error: f.error })),
935
+ },
936
+ });
937
+ return;
938
+ }
939
+ // 构建多文件内容部分
940
+ const filesSection = validFiles
941
+ .map((f) => {
942
+ const ext = path.extname(f.path).slice(1) || 'txt';
943
+ return `### 文件:${f.path}\n\`\`\`${ext}\n${f.content}\n\`\`\``;
944
+ })
945
+ .join('\n\n');
946
+ // 构建最终 Prompt
947
+ const prompt = _componentAnalysisPrompt.replace('{filesSection}', filesSection);
948
+ Logger.log(`构建 Prompt 完成,总字符数: ${prompt.length}`);
949
+ // 调用 MCP Sampling API
950
+ if (!mcpServer) {
951
+ Logger.warn('MCP 服务器未初始化,返回空结果');
952
+ res.json({
953
+ success: true,
954
+ data: {
955
+ total: filePaths.length,
956
+ succeeded: 0,
957
+ failed: filePaths.length,
958
+ components: [],
959
+ errors: filePaths.map((p) => ({ filename: p, error: 'MCP 服务器未初始化' })),
960
+ },
961
+ });
962
+ return;
963
+ }
964
+ try {
965
+ const { content: { text }, } = await mcpServer.server.createMessage({
966
+ messages: [
967
+ frameworkGuidePrompt && {
968
+ role: 'user',
969
+ content: {
970
+ type: 'text',
971
+ text: frameworkGuidePrompt,
972
+ },
973
+ },
974
+ {
975
+ role: 'user',
976
+ content: {
977
+ type: 'text',
978
+ text: prompt,
979
+ },
980
+ },
981
+ ].filter(Boolean),
982
+ maxTokens: 8000,
983
+ }, { timeout: 2 * 60 * 1000 });
984
+ // 解析 AI 返回的 JSON
985
+ const extractResult = extractJson(text);
986
+ if (extractResult.state === 'successful-parse' && extractResult.value) {
987
+ const analysisResult = extractResult.value;
988
+ const components = (analysisResult.components || []).map((comp) => ({
989
+ id: generateComponentId(),
990
+ name: comp.componentName || 'Unknown',
991
+ type: 'custom',
992
+ path: comp.path || '',
993
+ import: comp.import || '',
994
+ props: comp.props || [],
995
+ description: comp.description || '',
996
+ super: comp.super || undefined,
997
+ functions: comp.functions || undefined,
998
+ }));
999
+ Logger.log(`成功分析 ${components.length} 个组件`);
1000
+ res.json({
1001
+ success: true,
1002
+ data: {
1003
+ total: filePaths.length,
1004
+ succeeded: validFiles.length,
1005
+ failed: errorFiles.length,
1006
+ components,
1007
+ errors: errorFiles.map((f) => ({ filename: f.path, error: f.error })),
1008
+ },
1009
+ });
1010
+ }
1011
+ else {
1012
+ Logger.warn(`解析组件分析结果失败: ${extractResult.error}`);
1013
+ res.json({
1014
+ success: true,
1015
+ data: {
1016
+ total: filePaths.length,
1017
+ succeeded: 0,
1018
+ failed: filePaths.length,
1019
+ components: [],
1020
+ errors: [{ filename: 'all', error: `AI 返回结果解析失败: ${extractResult.error}` }],
1021
+ },
1022
+ });
1023
+ }
1024
+ }
1025
+ catch (samplingError) {
1026
+ Logger.error('调用采样 API 失败:', samplingError);
1027
+ res.status(500).json({
1028
+ success: false,
1029
+ message: '调用 Sampling 分析失败,请调用 #mark_components tool进行组件分析',
1030
+ error: samplingError instanceof Error ? samplingError.message : String(samplingError),
1031
+ });
1032
+ }
967
1033
  }
968
1034
  catch (error) {
969
1035
  Logger.error('分析项目组件失败:', error);
@@ -976,6 +1042,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
976
1042
  });
977
1043
  /**
978
1044
  * 保存项目中的组件到components.json
1045
+ * 根据组件 id 自动判断是新增还是更新
979
1046
  */
980
1047
  app.post('/saveComponents', async (req, res) => {
981
1048
  try {
@@ -990,19 +1057,35 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
990
1057
  Logger.log(`准备保存 ${components.length} 个组件`);
991
1058
  // 读取现有组件
992
1059
  const existingComponents = await fileManager.loadComponentsDatabase();
993
- // 直接追加(暂不处理冲突)
994
- const allComponents = [...components, ...existingComponents];
1060
+ const existingMap = new Map(existingComponents.map((c) => [c.id, c]));
1061
+ let addedCount = 0;
1062
+ let updatedCount = 0;
1063
+ // 根据 id 判断新增或更新
1064
+ for (const comp of components) {
1065
+ if (existingMap.has(comp.id)) {
1066
+ // 更新现有组件
1067
+ existingMap.set(comp.id, { ...existingMap.get(comp.id), ...comp });
1068
+ updatedCount++;
1069
+ }
1070
+ else {
1071
+ // 新增组件
1072
+ existingMap.set(comp.id, comp);
1073
+ addedCount++;
1074
+ }
1075
+ }
1076
+ const allComponents = Array.from(existingMap.values());
995
1077
  // 保存到文件(带备份)
996
1078
  await fileManager.saveComponentsDatabase(allComponents);
997
- Logger.log(`✅ 成功保存,当前共 ${allComponents.length} 个组件`);
1079
+ Logger.log(`✅ 成功保存:新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${allComponents.length} 个组件`);
998
1080
  res.json({
999
1081
  success: true,
1000
1082
  data: {
1001
- added: components.length,
1083
+ added: addedCount,
1084
+ updated: updatedCount,
1002
1085
  total: allComponents.length,
1003
1086
  components: allComponents,
1004
1087
  },
1005
- message: '组件导入成功',
1088
+ message: '组件保存成功',
1006
1089
  });
1007
1090
  }
1008
1091
  catch (error) {
@@ -1014,32 +1097,13 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1014
1097
  });
1015
1098
  }
1016
1099
  });
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
- // 扫描项目组件接口(同时返回项目组件和外部依赖组件)
1100
+ // 扫描项目组件接口
1030
1101
  app.get('/scanComponents', async (req, res) => {
1031
1102
  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
- }
1103
+ const { includePaths, excludePaths } = req.query;
1040
1104
  const projectPath = getProjectRoot();
1041
- Logger.log('扫描项目组件:', { platform, includePaths, excludePaths });
1042
- // 读取外部依赖组件(从 .sloth/components.json)
1105
+ Logger.log('扫描项目组件:', { includePaths, excludePaths });
1106
+ // 读取项目组件(从 .sloth/components.json)
1043
1107
  let projectComponents = [];
1044
1108
  const slothPath = path.join(projectPath, '.sloth', 'components.json');
1045
1109
  const fs = await import('fs/promises');
@@ -1063,7 +1127,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1063
1127
  type: p.type || 'any',
1064
1128
  required: p.required || false,
1065
1129
  })),
1066
- description: c.description || c.metadata?.description,
1130
+ description: c.description,
1067
1131
  import: c.import,
1068
1132
  }));
1069
1133
  Logger.log(`从 .sloth/components.json 加载了 ${projectComponents.length} 个项目组件`);
@@ -1082,7 +1146,6 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1082
1146
  success: true,
1083
1147
  data: {
1084
1148
  total: projectComponents.length,
1085
- platform,
1086
1149
  projectComponents, // 项目组件
1087
1150
  },
1088
1151
  });
@@ -1096,43 +1159,28 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1096
1159
  });
1097
1160
  }
1098
1161
  });
1099
- // 建议组件映射接口(基于历史组件截图匹配)
1162
+ // 建议组件映射接口(基于 components.json 中组件的截图匹配)
1100
1163
  app.post('/suggestMappings', upload.single('currentScreenshot'), async (req, res) => {
1101
1164
  try {
1102
- const { fileKey, nodeId, threshold = '0.8' } = req.body;
1165
+ const { threshold = '0.8' } = req.body;
1103
1166
  const currentScreenshotFile = req.file;
1104
- if (!currentScreenshotFile || !fileKey) {
1167
+ if (!currentScreenshotFile) {
1105
1168
  res.status(400).json({
1106
1169
  success: false,
1107
- message: '缺少必要参数: currentScreenshot, fileKey',
1170
+ message: '缺少必要参数: currentScreenshot',
1108
1171
  });
1109
1172
  return;
1110
1173
  }
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('当前设计稿无分组数据,继续执行自动建议');
1174
+ Logger.log(`开始生成建议组件映射: threshold=${threshold}`);
1127
1175
  // 保存当前设计稿截图到临时文件
1128
1176
  const tempDir = path.join(fileManager.getBaseDir(), 'temp');
1129
1177
  await fs.promises.mkdir(tempDir, { recursive: true });
1130
1178
  const currentScreenshotPath = path.join(tempDir, `suggest_${Date.now()}.png`);
1131
1179
  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('未找到历史分组数据,无法生成建议');
1180
+ // components.json 加载所有组件
1181
+ const components = await fileManager.loadComponentsDatabase();
1182
+ if (!components || components.length === 0) {
1183
+ Logger.log('未找到已保存的组件,无法生成建议');
1136
1184
  await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1137
1185
  res.json({
1138
1186
  success: true,
@@ -1140,34 +1188,37 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1140
1188
  });
1141
1189
  return;
1142
1190
  }
1143
- Logger.log(`从整个项目加载了 ${allProjectGroupsData.length} 个节点的历史数据`);
1191
+ Logger.log(`从 components.json 加载了 ${components.length} 个组件`);
1192
+ // 构建匹配任务:查找每个组件的截图文件
1144
1193
  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
- });
1194
+ for (const component of components) {
1195
+ // 只处理有 signature(截图 hash)的组件
1196
+ if (!component.signature) {
1197
+ Logger.log(`组件 ${component.name} 没有 signature,跳过`);
1198
+ continue;
1167
1199
  }
1200
+ // 根据 signature 搜索截图文件
1201
+ const screenshotPath = await fileManager.findScreenshotByHash(component.signature);
1202
+ if (!screenshotPath) {
1203
+ Logger.log(`组件 ${component.name} 的截图未找到 (hash: ${component.signature}),跳过`);
1204
+ continue;
1205
+ }
1206
+ matchTasks.push({
1207
+ path: screenshotPath,
1208
+ componentData: {
1209
+ id: component.id,
1210
+ name: component.name,
1211
+ type: component.type,
1212
+ path: component.path,
1213
+ import: component.import,
1214
+ props: component.props,
1215
+ description: component.description,
1216
+ signature: component.signature,
1217
+ },
1218
+ });
1168
1219
  }
1169
1220
  if (matchTasks.length === 0) {
1170
- Logger.log('没有可用的历史截图,返回空建议');
1221
+ Logger.log('没有可用的组件截图,返回空建议');
1171
1222
  await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1172
1223
  res.json({
1173
1224
  success: true,
@@ -1175,12 +1226,12 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1175
1226
  });
1176
1227
  return;
1177
1228
  }
1178
- Logger.log(`开始匹配,共 ${matchTasks.length} 个历史组件截图`);
1229
+ Logger.log(`开始匹配,共 ${matchTasks.length} 个组件截图`);
1179
1230
  const matcher = new ImageMatcher();
1180
- const matches = await matcher.batchMatch(currentScreenshotPath, matchTasks, parseFloat(threshold));
1231
+ const matches = await matcher.batchMatch(currentScreenshotPath, matchTasks.map((t) => ({ path: t.path, groupData: t.componentData })), parseFloat(threshold));
1181
1232
  Logger.log(`匹配完成,找到 ${matches.length} 个相似组件`);
1182
1233
  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)}%`);
1234
+ Logger.log(` 匹配 ${index + 1}: 组件=${match.groupData?.name || 'N/A'}, 相似度=${Math.round(match.confidence * 100)}%, 位置=(${match.position.left}, ${match.position.top}, ${match.position.width}x${match.position.height})`);
1184
1235
  });
1185
1236
  await fs.promises.unlink(currentScreenshotPath).catch(() => { });
1186
1237
  res.json({
@@ -1188,14 +1239,22 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1188
1239
  data: {
1189
1240
  suggestions: matches.map((match) => ({
1190
1241
  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
1242
+ position: {
1243
+ left: match.position.left,
1244
+ top: match.position.top,
1245
+ width: match.position.width,
1246
+ height: match.position.height,
1247
+ },
1248
+ component: {
1249
+ id: match.groupData?.id,
1250
+ name: match.groupData?.name,
1251
+ type: match.groupData?.type,
1252
+ path: match.groupData?.path,
1253
+ import: match.groupData?.import,
1254
+ props: match.groupData?.props,
1255
+ description: match.groupData?.description,
1256
+ signature: match.groupData?.signature,
1257
+ },
1199
1258
  })),
1200
1259
  },
1201
1260
  });
@@ -1241,7 +1300,12 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1241
1300
  }
1242
1301
  });
1243
1302
  // 启动 HTTP 服务器,监听端口
1244
- httpServer = app.listen(port, () => {
1303
+ httpServer = app.listen(port, async (err) => {
1304
+ if (err) {
1305
+ Logger.error('HTTP 服务器启动失败:', err);
1306
+ httpServer = null;
1307
+ return;
1308
+ }
1245
1309
  Logger.log(`HTTP server listening on port ${port}`);
1246
1310
  Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
1247
1311
  Logger.log(`Message endpoint available at http://localhost:${port}/messages`);
@@ -1253,6 +1317,15 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1253
1317
  Logger.log(`Framework Config at http://localhost:${port}/getFrameworkConfig`);
1254
1318
  Logger.log(`Save Nodes endpoint available at http://localhost:${port}/saveNodes`);
1255
1319
  Logger.log(`Logging reconnect endpoint available at http://localhost:${port}/reconnect-logging`);
1320
+ // 启动 Socket 服务器,监听端口 + 1
1321
+ try {
1322
+ socketServer = new SocketServer();
1323
+ await socketServer.start(port + 1);
1324
+ }
1325
+ catch (error) {
1326
+ Logger.error('Socket 服务器启动失败:', error);
1327
+ Logger.error('进入子进程模式');
1328
+ }
1256
1329
  });
1257
1330
  // 监听 SIGINT 信号,优雅关闭服务器和所有会话
1258
1331
  process.on('SIGINT', async () => {
@@ -1260,6 +1333,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1260
1333
  Logger.log('正在关闭服务器...');
1261
1334
  // 清理等待中的认证请求
1262
1335
  pendingRequests.clear();
1336
+ // 关闭 Socket 服务器
1337
+ await stopSocketServer();
1263
1338
  // 关闭所有活跃的 SSE 和 streamable 传输,释放资源
1264
1339
  await closeTransports(transports.sse);
1265
1340
  await closeTransports(transports.streamable);
@@ -1282,30 +1357,66 @@ async function closeTransports(transports) {
1282
1357
  // 认证功能 - 获取用户输入函数
1283
1358
  export async function getUserInput(payload) {
1284
1359
  return new Promise(async (resolve, reject) => {
1285
- if (!httpServer) {
1286
- reject(new Error('HTTP 服务器未运行。请先启动服务器。'));
1287
- return;
1288
- }
1289
1360
  const token = uuidv4();
1290
- const authUrl = `http://localhost:${PORT}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}`;
1291
- // 存储解析函数 - 无超时限制
1292
- pendingRequests.set(token, {
1293
- resolve: (value) => {
1294
- pendingRequests.delete(token);
1295
- resolve(value);
1296
- },
1297
- timeout: null, // 不再使用超时
1298
- });
1299
- // 打开浏览器
1361
+ const port = getPort();
1362
+ const authUrl = `http://localhost:${port}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}`;
1363
+ Logger.log('authUrl', authUrl);
1364
+ // 判断是主进程还是子进程
1365
+ const isMainProcess = httpServer !== null;
1366
+ if (isMainProcess) {
1367
+ // 主进程:存在 pendingRequests中, 在/submit里resolve
1368
+ Logger.log('主进程:使用 pendingRequests 等待认证响应');
1369
+ pendingRequests.set(token, {
1370
+ resolve: (value) => {
1371
+ pendingRequests.delete(token);
1372
+ resolve(value);
1373
+ },
1374
+ timeout: null,
1375
+ });
1376
+ Logger.log('getUserInput pendingRequests', [...pendingRequests.keys()]);
1377
+ }
1378
+ else {
1379
+ // 子进程:使用 Socket 客户端,监听SocketServer的submit-response消息
1380
+ Logger.log('子进程:使用 Socket 客户端等待认证响应');
1381
+ try {
1382
+ const { SocketClient } = await import('./socket-client.js');
1383
+ const socketClient = new SocketClient('localhost', port + 1);
1384
+ // 连接到 Socket 服务器
1385
+ await socketClient.connect();
1386
+ Logger.log('Socket 客户端已连接');
1387
+ // 注册 token 并等待响应
1388
+ const responsePromise = socketClient.registerToken(token);
1389
+ // 打开浏览器
1390
+ await open(authUrl);
1391
+ // 等待认证响应
1392
+ const response = await responsePromise;
1393
+ // 断开连接
1394
+ socketClient.disconnect();
1395
+ return resolve(response);
1396
+ }
1397
+ catch (err) {
1398
+ reject(new Error(`Socket 客户端错误: ${err.message}`));
1399
+ return;
1400
+ }
1401
+ }
1402
+ // 打开浏览器(主进程)
1300
1403
  try {
1301
1404
  await open(authUrl);
1302
1405
  }
1303
1406
  catch (err) {
1304
- pendingRequests.delete(token);
1407
+ if (isMainProcess) {
1408
+ pendingRequests.delete(token);
1409
+ }
1305
1410
  reject(new Error(`打开浏览器失败: ${err.message}`));
1306
1411
  }
1307
1412
  });
1308
1413
  }
1414
+ // 停止 Socket 服务器
1415
+ export async function stopSocketServer() {
1416
+ if (socketServer) {
1417
+ await socketServer.stop();
1418
+ }
1419
+ }
1309
1420
  // 停止 HTTP 服务器,并关闭所有 SSE 传输
1310
1421
  export async function stopHttpServer() {
1311
1422
  if (!httpServer) {
@@ -1314,6 +1425,8 @@ export async function stopHttpServer() {
1314
1425
  return new Promise((resolve, reject) => {
1315
1426
  // 清理等待中的认证请求
1316
1427
  pendingRequests.clear();
1428
+ // 关闭 Socket 服务器
1429
+ const closeSocket = stopSocketServer();
1317
1430
  httpServer.close((err) => {
1318
1431
  if (err) {
1319
1432
  reject(err);
@@ -1324,7 +1437,7 @@ export async function stopHttpServer() {
1324
1437
  const closing = Object.values(transports.sse).map((transport) => {
1325
1438
  return transport.close();
1326
1439
  });
1327
- Promise.all(closing).then(() => {
1440
+ Promise.all([...closing, closeSocket]).then(() => {
1328
1441
  resolve();
1329
1442
  });
1330
1443
  });