sloth-d2c-mcp 1.0.4-beta74 → 1.0.4-beta76

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.
@@ -49,12 +49,14 @@ export function buildUserPromptText(userPrompt) {
49
49
  return '\n## 用户提示词\n以下是用户针对该代码的优化提出的补充说明和要求,请在优化代码时特别关注这些指导并尽可能实现:\n' + userPrompt;
50
50
  }
51
51
  /**
52
- * 构建组件映射提示词(包含组件映射和组件上下文)
52
+ * 构建组件映射提示词(包含组件映射和组件上下文和组件标记)
53
53
  * @param componentMappings 组件映射数组
54
54
  * @param componentContexts 所有组的组件上下文数组
55
+ * @param markedComponents 标记的组件数组
55
56
  * @returns 组件映射提示词
56
57
  */
57
- export function buildComponentMappingPrompt(componentMappings, componentContexts = []) {
58
+ export function buildComponentMappingPrompt(componentMappings, componentContexts = [], markedComponents = []) {
59
+ let prompt = '';
58
60
  // 收集所有组件:组件映射 + 组件上下文(按名称去重)
59
61
  const allComponents = new Map();
60
62
  // 添加组件映射的组件
@@ -69,26 +71,46 @@ export function buildComponentMappingPrompt(componentMappings, componentContexts
69
71
  }
70
72
  });
71
73
  });
72
- if (allComponents.size === 0) {
73
- return '';
74
+ // 构建可用组件提示词
75
+ if (allComponents.size > 0) {
76
+ prompt += '\n\n## 可用组件\n\n以下是项目中可用的组件,请在最终代码中使用这些组件:\n\n';
77
+ allComponents.forEach((component) => {
78
+ const propsInfo = component.props && component.props.length > 0
79
+ ? component.props.map((p) => `${p.name}${p.required ? ' (必需)' : ' (可选)'}: ${p.type || 'any'}`).join(', ')
80
+ : '无';
81
+ const libInfo = component.lib ? ` (来自 ${component.lib})` : ' (项目组件)';
82
+ const descInfo = component.description ? `\n- **描述**: ${component.description}` : '';
83
+ prompt += `### ${component.name}${libInfo}\n`;
84
+ prompt += `- **文件路径**: ${component.path}\n`;
85
+ prompt += `- **导入方式**: ${component.import}\n`;
86
+ prompt += `- **Props**: ${propsInfo}${descInfo}\n\n`;
87
+ });
88
+ prompt +=
89
+ '**重要**:在最终写入代码时,请根据设计稿需求合理传递 props 参数。请按照项目路径规范合理引入组件。\n';
90
+ }
91
+ // 构建标记组件提示词
92
+ if (markedComponents.length > 0) {
93
+ prompt += '\n\n## 组件标记任务\n\n';
94
+ prompt += '以下组件已被用户标记为可复用组件,**请在写入代码后调用 `mark_components` 工具保存这些组件信息**:\n\n';
95
+ prompt += '| 组件名 | signature |\n';
96
+ prompt += '|--------|-----------|';
97
+ markedComponents.forEach((comp) => {
98
+ prompt += `\n| ${comp.name} | ${comp.signature} |`;
99
+ });
100
+ prompt += '\n\n调用示例:\n';
101
+ prompt += '```json\n';
102
+ prompt += JSON.stringify({
103
+ components: markedComponents.map((comp) => ({
104
+ name: comp.name,
105
+ path: '按实际写入路径填写',
106
+ signature: comp.signature,
107
+ import: '按实际写入路径填写导入语句',
108
+ description: '组件的功能描述',
109
+ })),
110
+ }, null, 2);
111
+ prompt += '\n```\n\n';
112
+ prompt += '**重要**:组件标记后可在后续转码中自动匹配复用\n';
74
113
  }
75
- let prompt = '\n\n## 可用组件\n\n以下是项目中可用的组件,请在最终代码中使用这些组件:\n\n';
76
- allComponents.forEach((component) => {
77
- const propsInfo = component.props && component.props.length > 0
78
- ? component.props.map((p) => `${p.name}${p.required ? ' (必需)' : ' (可选)'}: ${p.type || 'any'}`).join(', ')
79
- : '无';
80
- const libInfo = component.lib ? ` (来自 ${component.lib})` : ' (项目组件)';
81
- const descInfo = component.description ? `\n- **描述**: ${component.description}` : '';
82
- // 构建导入方式文案
83
- const importType = component.importType || 'default'; // 默认为 default 导入
84
- const importTypeText = importType === 'named' ? '具名导入' : '默认导入';
85
- prompt += `### ${component.name}${libInfo}\n`;
86
- prompt += `- **文件路径**: ${component.path}\n`;
87
- prompt += `- **导入方式**: ${importTypeText}\n`;
88
- prompt += `- **Props**: ${propsInfo}${descInfo}\n\n`;
89
- });
90
- prompt +=
91
- '**重要**:在最终写入代码时,请严格按照上述"导入方式"来编写 import 语句。"具名导入"表示 import { 组件名 } from \'路径\',"默认导入"表示 import 组件名 from \'路径\'。请根据设计稿需求合理传递 props 参数。请按照项目路径规范合理使用相对路径和路径别名引入组件。\n';
92
114
  return prompt;
93
115
  }
94
116
  /**
@@ -103,8 +125,8 @@ export function buildPlaceholderPrompt(placeholders) {
103
125
  let prompt = '\n\n## 子组件占位符\n\n**重要**:以下占位符代表已实现的子组件,请转换格式但不要重新实现其内部逻辑。\n\n';
104
126
  placeholders.forEach((placeholder) => {
105
127
  const kebabName = placeholder.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
106
- prompt += `- \`<${kebabName}>\` → \`<${placeholder.name} />\`\n`;
128
+ prompt += `- \`<${kebabName}>\`\n`;
107
129
  });
108
- prompt += '\n将 HTML 标签转换为 React 组件引用,不需要添加对应的 import 语句,保持其在布局中的位置。\n';
130
+ prompt += '\n将 HTML 标签转换为真实组件调用,但不需要添加对应的 import 语句,保持其在布局中的位置。\n';
109
131
  return prompt;
110
132
  }
@@ -135,7 +135,7 @@ export function injectChildPlaceholders(nodeList, childResults, nestingInfo, par
135
135
  * @returns 采样结果
136
136
  */
137
137
  export async function sampleSingleGroup(group, nodeList, config) {
138
- const { d2cNodeList, imageMap, convertConfig, chunkPrompt, mcpServer, componentMappings, codeSnippets, componentContexts } = config;
138
+ const { d2cNodeList, imageMap, convertConfig, frameworkPrompt, chunkPrompt, mcpServer, componentMappings, codeSnippets, componentContexts } = config;
139
139
  Logger.log(`开始采样组 ${group.groupIndex}, 元素数量: ${group.elements.length}, nodeList 长度: ${nodeList.length}`);
140
140
  // 检查组件映射
141
141
  const mappingResult = handleComponentMapping(group, componentMappings);
@@ -184,7 +184,8 @@ export async function sampleSingleGroup(group, nodeList, config) {
184
184
  role: 'user',
185
185
  content: {
186
186
  type: 'text',
187
- text: chunkPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') +
187
+ text: frameworkPrompt +
188
+ chunkPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') +
188
189
  userPromptText +
189
190
  componentContextPrompt +
190
191
  placeholderPrompt,
@@ -203,18 +204,7 @@ export async function sampleSingleGroup(group, nodeList, config) {
203
204
  const { code: chunkCode, componentName: sampledName } = extractCodeAndComponents(text)[0];
204
205
  Logger.log(`组 ${group.groupIndex} 采样成功,组件名称: ${sampledName}, 代码长度: ${chunkCode.length}`);
205
206
  group.name = sampledName;
206
- // 如果分组被标记且有截图,注入 signature 注释
207
- let finalCode = chunkCode;
208
- if (group.marked && group.screenshot?.hash) {
209
- const document = `/**
210
- * ${sampledName} 组件
211
- * signature: ${group.screenshot.hash}
212
- */
213
- `;
214
- finalCode = document + chunkCode;
215
- Logger.log(`组 ${group.groupIndex} 已标记,注入 signature: ${group.screenshot.hash}, 组件名: ${sampledName}`);
216
- }
217
- codeSnippets.push(finalCode);
207
+ codeSnippets.push(chunkCode);
218
208
  return {
219
209
  code: chunkCode,
220
210
  componentName: sampledName,
@@ -224,15 +214,7 @@ export async function sampleSingleGroup(group, nodeList, config) {
224
214
  }
225
215
  catch (e) {
226
216
  Logger.log(`组 ${group.groupIndex} 调用采样出错:`, e);
227
- let fallbackCode = userPromptText + componentContextPrompt + placeholderPrompt + `\n // Group${group.groupIndex + 1} \n` + codeWithCustomName;
228
- // 如果分组被标记且有截图,注入 signature 注释
229
- if (group.marked && group.screenshot?.hash) {
230
- const document = `/**
231
- * signature: ${group.screenshot.hash}
232
- */
233
- `;
234
- fallbackCode = document + fallbackCode;
235
- }
217
+ const fallbackCode = userPromptText + componentContextPrompt + placeholderPrompt + `\n // Group${group.groupIndex + 1} \n` + codeWithCustomName;
236
218
  codeSnippets.push(fallbackCode);
237
219
  Logger.log(`组 ${group.groupIndex} 使用降级代码,长度: ${fallbackCode.length}`);
238
220
  return {
@@ -7,7 +7,7 @@ import { ConfigManager, defaultConfigData, } from './config-manager/index.js';
7
7
  import { cleanup as cleanupTauri } from './interceptor/client.js';
8
8
  import { cleanup as cleanupVSCode, getUserInputFromVSCode, isVSCodeAvailable } from './interceptor/vscode.js';
9
9
  import { cleanup as cleanupWeb, getUserInput } from './interceptor/web.js';
10
- import { loadConfig, startHttpServer, stopHttpServer } from './server.js';
10
+ import { loadConfig, startHttpServer, stopHttpServer, generateComponentId, stopSocketServer } from './server.js';
11
11
  import { FileManager } from './utils/file-manager.js';
12
12
  import { updateImageMapIfNeeded } from './utils/update.js';
13
13
  import { Logger } from './utils/logger.js';
@@ -20,6 +20,32 @@ import { promises as fs } from 'fs';
20
20
  import * as path from 'path';
21
21
  import { fileURLToPath } from 'url';
22
22
  import { extractCodeAndComponents } from './utils/extract.js';
23
+ import { trackToolCall } from './utils/tj.js';
24
+ /**
25
+ * 启动http & socket监听
26
+ * @param init 是否是初始化,初始化时需要连接stdio transport
27
+ */
28
+ export async function startListening(init = false) {
29
+ const isStdioMode = process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
30
+ const configManager = new ConfigManager('d2c-mcp');
31
+ const port = await getAvailablePort();
32
+ if (isStdioMode) {
33
+ Logger.log(`Stdio模式启动`);
34
+ await loadConfig(mcpServer, configManager);
35
+ if (init) {
36
+ const transport = new StdioServerTransport();
37
+ await mcpServer.connect(transport);
38
+ }
39
+ // 启动 HTTP 服务器 - 仅启用web服务
40
+ await startHttpServer(port, mcpServer, configManager, false);
41
+ }
42
+ else {
43
+ // 启动 HTTP 服务器 - 这是核心功能,不依赖 VSCode 日志
44
+ await startHttpServer(port, mcpServer, configManager, true);
45
+ }
46
+ // 服务器启动成功后再尝试使用 Logger(包含 VSCode 日志)
47
+ Logger.log(`Server started successfully on port ${port}!`);
48
+ }
23
49
  export class D2CMcpServer extends McpServer {
24
50
  figmaApiKey = '';
25
51
  baseURL = '';
@@ -53,6 +79,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
53
79
  local: z.boolean().optional().describe('Whether to use local data cache, default is false'),
54
80
  }, async (args) => {
55
81
  try {
82
+ trackToolCall('d2c_figma');
56
83
  Logger.log(`收到工具调用参数:`, JSON.stringify(args, null, 2));
57
84
  const { fileKey, nodeId, depth, local } = args;
58
85
  let config = await configManager.load();
@@ -71,6 +98,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
71
98
  catch (error) {
72
99
  Logger.log('获取根目录时出错:', error);
73
100
  }
101
+ await startListening();
74
102
  // 没有配置figmaApiKey,无法预览,直接唤起配置页面
75
103
  if (!config.mcp?.figmaApiKey) {
76
104
  hasLaunchWebview = true;
@@ -154,6 +182,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
154
182
  else {
155
183
  configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId });
156
184
  }
185
+ console.log('收到网页提交数据', configDataString);
157
186
  if (!configDataString || configDataString.trim() === '') {
158
187
  throw new Error('未提供有效的转码配置');
159
188
  }
@@ -203,7 +232,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
203
232
  const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
204
233
  const convertConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
205
234
  // 获取提示词,优先使用用户保存的提示词,否则使用默认提示词
206
- const frameworkPrompt = savedPromptSetting?.frameworkGuidePrompt;
235
+ const frameworkPrompt = savedPromptSetting?.enableFrameworkGuide ? savedPromptSetting?.frameworkGuidePrompt : '';
207
236
  const chunkPrompt = savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt;
208
237
  const aggregationPrompt = savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt;
209
238
  const finalPrompt = savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt;
@@ -230,7 +259,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
230
259
  isSupportSampling = await processSampling(groupsData, samplingConfig);
231
260
  // 采样完成后,更新组件名并保存到文件
232
261
  try {
233
- const groupsDataToSave = groupsData.map(group => {
262
+ const groupsDataToSave = groupsData.map((group) => {
234
263
  // 将提取的组件名更新到 componentName(仅标记的分组)
235
264
  if (group.marked && group.name) {
236
265
  group.componentName = group.name;
@@ -241,7 +270,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
241
270
  return groupWithoutNodeList;
242
271
  });
243
272
  await fileManager.saveGroupsData(fileKey, nodeId, groupsDataToSave);
244
- Logger.log(`已保存更新后的 groupsData,包含 ${groupsData.filter(g => g.marked).length} 个标记的组件`);
273
+ Logger.log(`已保存更新后的 groupsData,包含 ${groupsData.filter((g) => g.marked).length} 个标记的组件`);
245
274
  }
246
275
  catch (error) {
247
276
  Logger.error(`保存 groupsData 失败:`, error);
@@ -322,7 +351,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
322
351
  role: 'user',
323
352
  content: {
324
353
  type: 'text',
325
- text: aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') + placeholderPrompt,
354
+ text: frameworkPrompt + aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') + placeholderPrompt,
326
355
  },
327
356
  },
328
357
  {
@@ -352,8 +381,21 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
352
381
  Logger.log('处理完成,生成的代码片段数量:', codeSnippets.length);
353
382
  Logger.log('已映射的组件数量:', componentMappings.length);
354
383
  Logger.log('组件上下文数量:', componentContexts.length);
355
- // 构建组件映射提示词(包含组件映射和组件上下文)
356
- const componentMappingPrompt = buildComponentMappingPrompt(componentMappings, componentContexts);
384
+ // 收集标记的组件(用于提示 LLM 调用 mark_components)
385
+ const markedComponents = [];
386
+ if (groupsData && groupsData.length > 0) {
387
+ groupsData.forEach((group) => {
388
+ if (group.marked && group.screenshot?.hash && group.name) {
389
+ markedComponents.push({
390
+ name: group.name,
391
+ signature: group.screenshot.hash,
392
+ });
393
+ }
394
+ });
395
+ Logger.log(`收集到 ${markedComponents.length} 个标记的组件`);
396
+ }
397
+ // 构建组件映射提示词(包含组件映射、组件上下文和标记组件)
398
+ const componentMappingPrompt = buildComponentMappingPrompt(componentMappings, componentContexts, markedComponents);
357
399
  // 使用提示词(已包含默认值)
358
400
  return {
359
401
  content: [
@@ -387,6 +429,112 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
387
429
  };
388
430
  }
389
431
  });
432
+ // 标记并保存组件到 components.json
433
+ mcpServer.tool('mark_components', 'Mark and save components to project components.json for future reuse and matching', {
434
+ components: z
435
+ .array(z.object({
436
+ name: z.string().describe('Component name in PascalCase'),
437
+ path: z.string().describe('Component file path relative to project root'),
438
+ signature: z.string().describe('Component signature (SHA256 hash) for matching'),
439
+ type: z.enum(['button', 'input', 'card', 'text', 'image', 'container', 'list', 'custom']).optional(),
440
+ description: z.string().optional(),
441
+ import: z.string().optional().describe('Import statement for the component'),
442
+ props: z
443
+ .array(z.object({
444
+ name: z.string(),
445
+ type: z.string().optional(),
446
+ required: z.boolean().optional(),
447
+ description: z.string().optional(),
448
+ }))
449
+ .optional(),
450
+ }))
451
+ .describe('Array of components to mark and save'),
452
+ }, async (args) => {
453
+ try {
454
+ const { components } = args;
455
+ // 1. 设置 workspaceRoot
456
+ let root = './';
457
+ try {
458
+ const rootRes = await mcpServer.server.listRoots();
459
+ Logger.log('获取根目录:', rootRes);
460
+ root = rootRes.roots[0]?.uri?.slice(7) || './';
461
+ // 设置 fileManager 的工作目录根路径
462
+ fileManager.setWorkspaceRoot(root);
463
+ }
464
+ catch (error) {
465
+ Logger.log('获取根目录时出错:', error);
466
+ }
467
+ // 2. 加载现有组件
468
+ const existingComponents = await fileManager.loadComponentsDatabase();
469
+ // 3. 构建 signature 索引(用于去重)
470
+ const signatureIndex = new Map();
471
+ existingComponents.forEach((comp, index) => {
472
+ if (comp.signature) {
473
+ signatureIndex.set(comp.signature, index);
474
+ }
475
+ });
476
+ // 4. 处理新组件(合并逻辑)
477
+ const now = new Date().toISOString();
478
+ let addedCount = 0;
479
+ let updatedCount = 0;
480
+ for (const comp of components) {
481
+ const existingIndex = signatureIndex.get(comp.signature);
482
+ const storedComponent = {
483
+ id: generateComponentId(),
484
+ name: comp.name,
485
+ type: comp.type || 'custom',
486
+ path: comp.path,
487
+ import: comp.import || `import { ${comp.name} } from '${comp.path}'`,
488
+ props: (comp.props || []).map((p) => ({
489
+ name: p.name,
490
+ type: p.type || 'any',
491
+ required: p.required || false,
492
+ description: p.description,
493
+ })),
494
+ description: comp.description,
495
+ signature: comp.signature,
496
+ };
497
+ if (existingIndex !== undefined) {
498
+ // 更新已存在的组件(保留原始导入时间)
499
+ const originalImportedAt = existingComponents[existingIndex].importedAt;
500
+ existingComponents[existingIndex] = {
501
+ ...storedComponent,
502
+ importedAt: originalImportedAt,
503
+ };
504
+ updatedCount++;
505
+ }
506
+ else {
507
+ // 添加新组件
508
+ existingComponents.push(storedComponent);
509
+ signatureIndex.set(comp.signature, existingComponents.length - 1);
510
+ addedCount++;
511
+ }
512
+ }
513
+ // 5. 保存到文件
514
+ await fileManager.saveComponentsDatabase(existingComponents);
515
+ // 6. 返回结果
516
+ Logger.log(`mark_components: 新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${existingComponents.length} 个组件`);
517
+ return {
518
+ content: [
519
+ {
520
+ type: 'text',
521
+ text: `✅ 组件标记完成:新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${existingComponents.length} 个组件`,
522
+ },
523
+ ],
524
+ };
525
+ }
526
+ catch (error) {
527
+ Logger.error('mark_components 处理失败:', error);
528
+ return {
529
+ content: [
530
+ {
531
+ type: 'text',
532
+ text: `❌ 标记组件失败: ${error instanceof Error ? error.message : String(error)}`,
533
+ },
534
+ ],
535
+ };
536
+ }
537
+ });
390
538
  // 启动 HTTP 服务器
391
539
  async function main() {
392
540
  // 检查是否是版本命令
@@ -760,7 +908,7 @@ async function main() {
760
908
  success: true,
761
909
  framework: frameworkName,
762
910
  configPath: frameworkPath,
763
- created: !configExists
911
+ created: !configExists,
764
912
  }, null, 2));
765
913
  }
766
914
  else {
@@ -778,7 +926,7 @@ async function main() {
778
926
  if (isJsonOutput) {
779
927
  console.log(JSON.stringify({
780
928
  success: false,
781
- error: error.toString()
929
+ error: error.toString(),
782
930
  }, null, 2));
783
931
  }
784
932
  else {
@@ -791,24 +939,8 @@ async function main() {
791
939
  try {
792
940
  // 先输出基本启动信息到控制台,确保即使 VSCode 日志失败也能看到启动过程
793
941
  console.log(`[${new Date().toISOString()}] Starting Figma transcoding interceptor MCP server...`);
794
- const port = await getAvailablePort();
795
942
  // Create config manager instance to pass to server
796
- const configManager = new ConfigManager('d2c-mcp');
797
- const isStdioMode = process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
798
- if (isStdioMode) {
799
- Logger.log(`Stdio模式启动`);
800
- await loadConfig(mcpServer, configManager);
801
- const transport = new StdioServerTransport();
802
- await mcpServer.connect(transport);
803
- // 启动 HTTP 服务器 - 仅启用web服务
804
- await startHttpServer(port, mcpServer, configManager, false);
805
- }
806
- else {
807
- // 启动 HTTP 服务器 - 这是核心功能,不依赖 VSCode 日志
808
- await startHttpServer(port, mcpServer, configManager, true);
809
- }
810
- // 服务器启动成功后再尝试使用 Logger(包含 VSCode 日志)
811
- Logger.log(`Server started successfully on port ${port}!`);
943
+ await startListening(true);
812
944
  }
813
945
  catch (error) {
814
946
  // 确保错误信息能够输出,即使 VSCode 日志服务不可用
@@ -852,6 +984,7 @@ process.on('SIGTERM', async () => {
852
984
  catch (error) {
853
985
  // 忽略日志错误,确保清理过程继续
854
986
  }
987
+ await stopSocketServer();
855
988
  cleanupTauri();
856
989
  cleanupWeb();
857
990
  cleanupVSCode();