sloth-d2c-mcp 1.0.4-beta82 → 1.0.4-beta83

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.
@@ -33,8 +33,7 @@ export function buildComponentContext(group) {
33
33
  componentContextPrompt += `### ${comp.name}${libInfo}\n`;
34
34
  componentContextPrompt += `- **Props**: ${propsInfo}${descInfo}\n\n`;
35
35
  });
36
- componentContextPrompt +=
37
- '注意:在生成代码时,请根据设计稿的需求,合理使用上述组件,并正确传递 props 参数。\n';
36
+ componentContextPrompt += '注意:在生成代码时,请根据设计稿的需求,合理使用上述组件,并正确传递 props 参数。\n';
38
37
  }
39
38
  return { processedUserPrompt, componentContextPrompt };
40
39
  }
@@ -197,11 +196,11 @@ export function buildComponentMappingPrompt(componentMappings, componentContexts
197
196
  * @param placeholders 占位符信息数组
198
197
  * @returns 占位符提示词
199
198
  */
200
- export function buildPlaceholderPrompt(placeholders) {
199
+ export function buildPlaceholderPrompt(placeholders, isSupportSampling = true) {
201
200
  if (placeholders.length === 0) {
202
201
  return '';
203
202
  }
204
- let prompt = '\n\n## 子组件占位符\n\n**重要**:以下占位符代表已实现的子组件,请转换格式但不要重新实现其内部逻辑。\n\n';
203
+ let prompt = `\n\n## 子组件占位符\n\n**重要**:以下占位符代表已实现的子组件${isSupportSampling ? ',请转换格式但不要重新实现其内部逻辑。' : ''}\n\n`;
205
204
  placeholders.forEach((placeholder) => {
206
205
  const kebabName = placeholder.name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
207
206
  prompt += `- \`<${kebabName}>\`\n`;
@@ -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, frameworkPrompt, chunkPrompt, mcpServer, componentMappings, codeSnippets, componentContexts } = config;
138
+ const { d2cNodeList, imageMap, convertConfig, frameworkPrompt, chunkPrompt, mcpServer, componentMappings, codeSnippets, componentContexts, isSupportSampling } = config;
139
139
  Logger.log(`开始采样组 ${group.groupIndex}, 元素数量: ${group.elements.length}, nodeList 长度: ${nodeList.length}`);
140
140
  // 检查组件映射
141
141
  const mappingResult = handleComponentMapping(group, componentMappings);
@@ -178,6 +178,8 @@ export async function sampleSingleGroup(group, nodeList, config) {
178
178
  Logger.log(`组 ${group.groupIndex} 开始调用 AI 采样,代码长度: ${codeWithCustomName.length}, 是否有用户提示: ${!!group.userPrompt}, 是否有组件上下文: ${!!(group.componentContext && group.componentContext.length > 0)}, 占位符数量: ${placeholders.length}`);
179
179
  // 执行采样
180
180
  try {
181
+ if (!isSupportSampling)
182
+ throw new Error('不支持采样');
181
183
  const { content: { text }, } = await mcpServer.server.createMessage({
182
184
  messages: [
183
185
  {
@@ -274,11 +276,7 @@ export async function sampleGroupRecursive(groupIndex, nestingInfo, config, samp
274
276
  * @returns 是否支持采样
275
277
  */
276
278
  export async function processSampling(groupsData, config) {
277
- const { d2cNodeList, componentMappings, codeSnippets } = config;
278
- let isSupportSampling = true;
279
- if (!groupsData || groupsData.length === 0) {
280
- return isSupportSampling;
281
- }
279
+ const { d2cNodeList, componentMappings, codeSnippets, isSupportSampling } = config;
282
280
  Logger.log(`开始处理 groupsData,总数: ${groupsData.length}`);
283
281
  Logger.log(`groupsData 详情:`, groupsData.map((g) => ({ groupIndex: g.groupIndex, hasChildren: !!g.children, children: g.children })));
284
282
  // 构建嵌套结构
@@ -295,7 +293,6 @@ export async function processSampling(groupsData, config) {
295
293
  await sampleGroupRecursive(rootIndex, nestingInfo, config, samplingResults, 0);
296
294
  }
297
295
  catch (e) {
298
- isSupportSampling = false;
299
296
  Logger.log(`根组 ${rootIndex} 处理出错:`, e);
300
297
  }
301
298
  Logger.log(`========== 根组 ${rootIndex} 处理完成 ==========\n`);
@@ -327,7 +324,6 @@ export async function processSampling(groupsData, config) {
327
324
  await sampleSingleGroup(group, nodeList, config);
328
325
  }
329
326
  catch (e) {
330
- isSupportSampling = false;
331
327
  Logger.log(`独立组 ${group.groupIndex} 处理出错:`, e);
332
328
  }
333
329
  }
@@ -349,7 +345,6 @@ export async function processSampling(groupsData, config) {
349
345
  await sampleSingleGroup(group, nodeList, config);
350
346
  }
351
347
  catch (e) {
352
- isSupportSampling = false;
353
348
  Logger.log(`组 ${group.groupIndex} 处理出错:`, e);
354
349
  }
355
350
  Logger.log(`--- 组 ${group.groupIndex} 处理完成 ---\n`);
@@ -360,5 +355,4 @@ export async function processSampling(groupsData, config) {
360
355
  Logger.log(` - 已生成代码片段数: ${codeSnippets.length}`);
361
356
  Logger.log(` - 已映射组件数: ${componentMappings.length}`);
362
357
  Logger.log(` - 组件映射详情:`, componentMappings.map((m) => `组${m.groupIndex} -> ${m.groupName}`));
363
- return isSupportSampling;
364
358
  }
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { convertFigmaToD2CNode, generateAbsoluteHtml, getBoundingBox, getCode, chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, } from 'sloth-d2c-node/convert';
3
+ import { convertFigmaToD2CNode, generateAbsoluteHtml, getBoundingBox, getCode, chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, noSamplingAggregationPrompt, } from 'sloth-d2c-node/convert';
4
4
  import axios from 'axios';
5
5
  import { z } from 'zod';
6
6
  import { ConfigManager, defaultConfigData, } from './config-manager/index.js';
@@ -14,6 +14,7 @@ import { Logger } from './utils/logger.js';
14
14
  import { getAvailablePort, saveImageFile, replaceImageSrc } from './utils/utils.js';
15
15
  import { processSampling, buildNestingStructure } from './core/sampling.js';
16
16
  import { buildComponentMappingPrompt, buildFullUpdatePromptForAI, buildPlaceholderPrompt } from './core/prompt-builder.js';
17
+ import { detectClientApiSupport, clearCapabilitiesCache } from './utils/client-capabilities.js';
17
18
  // @ts-ignore
18
19
  import * as flatted from 'flatted';
19
20
  import { promises as fs } from 'fs';
@@ -89,6 +90,8 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
89
90
  const { fileKey, nodeId, depth, local, update = false } = args;
90
91
  let config = await configManager.load();
91
92
  let hasLaunchWebview = false;
93
+ // 检测客户端 API 支持程度
94
+ const clientApiSupport = await detectClientApiSupport(mcpServer);
92
95
  // 检测 VSCode 扩展是否可用
93
96
  const isVSCodePluginAvailable = await isVSCodeAvailable();
94
97
  Logger.log(`VSCode 扩展检测结果: ${isVSCodePluginAvailable ? '可用' : '不可用'}`);
@@ -113,12 +116,22 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
113
116
  if (isVSCodePluginAvailable) {
114
117
  // 优先使用 VSCode 扩展
115
118
  Logger.log('使用 VSCode 扩展获取用户输入');
116
- configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
119
+ configDataString = await getUserInputFromVSCode({
120
+ fileKey: fileKey,
121
+ nodeId: nodeId,
122
+ mode: update ? 'update' : 'create',
123
+ clientApiSupport,
124
+ });
117
125
  }
118
126
  else {
119
127
  // 降级到网页浏览器
120
128
  Logger.log('VSCode 扩展不可用,降级到网页浏览器');
121
- configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
129
+ configDataString = await getUserInput({
130
+ fileKey: fileKey,
131
+ nodeId: nodeId,
132
+ mode: update ? 'update' : 'create',
133
+ clientApiSupport,
134
+ });
122
135
  }
123
136
  console.log('getting configDataString', configDataString);
124
137
  if (!configDataString || configDataString.trim() === '') {
@@ -186,10 +199,20 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
186
199
  Logger.log(`收到网页工具调用参数:`, JSON.stringify(args, null, 2));
187
200
  let configDataString;
188
201
  if (isVSCodePluginAvailable) {
189
- configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
202
+ configDataString = await getUserInputFromVSCode({
203
+ fileKey: fileKey,
204
+ nodeId: nodeId,
205
+ mode: update ? 'update' : 'create',
206
+ clientApiSupport,
207
+ });
190
208
  }
191
209
  else {
192
- configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
210
+ configDataString = await getUserInput({
211
+ fileKey: fileKey,
212
+ nodeId: nodeId,
213
+ mode: update ? 'update' : 'create',
214
+ clientApiSupport,
215
+ });
193
216
  }
194
217
  console.log('收到网页提交数据', configDataString);
195
218
  if (!configDataString || configDataString.trim() === '') {
@@ -225,7 +248,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
225
248
  // 检查是否需要更新imageMap
226
249
  try {
227
250
  const imageNodeList = d2cNodeList?.filter((node) => ['IMG', 'ICON'].includes(node.type)) || [];
228
- const { imageMap: updatedImageMap, updated } = await updateImageMapIfNeeded(imageMap, imageNodeList, oldFileConfig, rest);
251
+ const { imageMap: updatedImageMap, updated } = await updateImageMapIfNeeded(imageMap, imageNodeList, oldFileConfig, rest, local);
229
252
  if (updated) {
230
253
  // 更新imageMap并重新保存
231
254
  Object.assign(imageMap, updatedImageMap);
@@ -263,8 +286,10 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
263
286
  const chunkPrompt = savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt;
264
287
  const aggregationPrompt = savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt;
265
288
  const finalPrompt = savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt;
289
+ const noSamplingPrompt = savedPromptSetting?.noSamplingAggregationPrompt || noSamplingAggregationPrompt;
266
290
  const codeSnippets = [];
267
- let isSupportSampling = true;
291
+ let isSupportSampling = clientApiSupport.sampling;
292
+ Logger.log(`客户端 API 支持检测: sampling=${isSupportSampling}, roots=${clientApiSupport.roots}`);
268
293
  // 收集已映射的组件信息,用于在最终写入时添加提示词
269
294
  const componentMappings = [];
270
295
  // 收集所有组的组件上下文信息,用于在最终提示词中显示
@@ -280,10 +305,11 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
280
305
  componentMappings,
281
306
  codeSnippets,
282
307
  componentContexts,
308
+ isSupportSampling,
283
309
  };
284
310
  // 深度遍历采样:针对每个分组生成 AI 相对布局树并转码
285
311
  if (groupsData && groupsData?.length > 0) {
286
- isSupportSampling = await processSampling(groupsData, samplingConfig);
312
+ await processSampling(groupsData, samplingConfig);
287
313
  // 采样完成后,更新组件名并保存到文件
288
314
  try {
289
315
  const groupsDataToSave = groupsData.map((group) => {
@@ -360,7 +386,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
360
386
  .map((node) => ({
361
387
  name: node.name || 'Unknown',
362
388
  }));
363
- const placeholderPrompt = buildPlaceholderPrompt(placeholders);
389
+ const placeholderPrompt = buildPlaceholderPrompt(placeholders, isSupportSampling);
364
390
  Logger.log(`整合采样检测到 ${placeholders.length} 个分组占位符`);
365
391
  // 获取代码
366
392
  const code = await getCode({
@@ -371,6 +397,8 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
371
397
  const replacedCode = replaceImageSrc(code, imageMap);
372
398
  // 整合采样
373
399
  try {
400
+ if (!isSupportSampling)
401
+ throw new Error('不支持采样');
374
402
  // 使用提示词(已包含默认值)
375
403
  const { content: { text }, } = await mcpServer.server.createMessage({
376
404
  messages: [
@@ -398,7 +426,6 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
398
426
  codeSnippets.unshift(pageCode);
399
427
  }
400
428
  catch (e) {
401
- isSupportSampling = false;
402
429
  Logger.log('调用分组外元素采样出错,降级到传统模式', e);
403
430
  codeSnippets.unshift(placeholderPrompt + '\n' + replacedCode);
404
431
  }
@@ -436,7 +463,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
436
463
  }
437
464
  : {
438
465
  type: 'text',
439
- text: aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') + componentMappingPrompt,
466
+ text: noSamplingPrompt + componentMappingPrompt,
440
467
  },
441
468
  ...codeSnippets.map((code) => {
442
469
  return {
@@ -1026,6 +1053,7 @@ process.on('SIGINT', async () => {
1026
1053
  cleanupTauri();
1027
1054
  cleanupWeb();
1028
1055
  cleanupVSCode();
1056
+ clearCapabilitiesCache(); // 清理能力检测缓存
1029
1057
  Logger.cleanup(); // 清理日志连接
1030
1058
  await stopHttpServer();
1031
1059
  process.exit(0);
@@ -1044,6 +1072,7 @@ process.on('SIGTERM', async () => {
1044
1072
  cleanupTauri();
1045
1073
  cleanupWeb();
1046
1074
  cleanupVSCode();
1075
+ clearCapabilitiesCache(); // 清理能力检测缓存
1047
1076
  Logger.cleanup(); // 清理日志连接
1048
1077
  await stopHttpServer();
1049
1078
  process.exit(0);
@@ -12,14 +12,14 @@ export class VSCodeCommunicator {
12
12
  /**
13
13
  * 长连接模式 - 直接等待用户输入,无超时限制
14
14
  * @param sessionId 会话ID
15
- * @param payload 传递给webview的数据
15
+ * @param payload 传递给webview的数据(包含 clientApiSupport)
16
16
  * @returns Promise<string> 返回用户输入的数据
17
17
  */
18
18
  waitForUserInputLongConnection(sessionId, payload) {
19
19
  return new Promise((resolve, reject) => {
20
20
  const client = net.connect({ port: this.port, host: this.host }, () => {
21
21
  console.log(`[MCP] 建立长连接等待用户输入,会话ID: ${sessionId}`);
22
- // 发送长连接等待命令,包含 payload 数据
22
+ // 发送长连接等待命令,包含 payload 数据(clientApiSupport 已在 payload 中)
23
23
  const requestPayload = JSON.stringify({
24
24
  cmd: 'waitForInput',
25
25
  sessionId,
@@ -67,7 +67,7 @@ export class VSCodeCommunicator {
67
67
  /**
68
68
  * 从VSCode获取用户输入 - 主要方法
69
69
  * 使用长连接模式,无超时限制
70
- * @param payload 传输参数
70
+ * @param payload 传输参数(包含 clientApiSupport)
71
71
  * @returns Promise<string> 返回用户输入的数据
72
72
  */
73
73
  async getUserInputFromVSCode(payload) {
@@ -116,7 +116,7 @@ export class VSCodeCommunicator {
116
116
  const vscodeComm = new VSCodeCommunicator();
117
117
  /**
118
118
  * 从VSCode获取用户输入的便捷函数
119
- * @param payload 传输参数
119
+ * @param payload 传输参数(包含 clientApiSupport)
120
120
  * @returns Promise<string> 返回用户输入的数据
121
121
  */
122
122
  export async function getUserInputFromVSCode(payload) {
@@ -31,7 +31,7 @@ const upload = multer({
31
31
  limits: { fileSize: 10 * 1024 * 1024 }, // 10MB
32
32
  });
33
33
  // 导入默认提示词
34
- import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, } from 'sloth-d2c-node/convert';
34
+ import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, noSamplingAggregationPrompt, noSamplingAggregationPromptVue, } from 'sloth-d2c-node/convert';
35
35
  // 保存 HTTP 服务器实例
36
36
  let httpServer = null;
37
37
  // 保存 Socket 服务器实例
@@ -104,6 +104,7 @@ export async function loadConfig(mcpServer, configManagerInstance) {
104
104
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
105
105
  finalOptimizePrompt: finalOptimizeCodePrompt,
106
106
  componentAnalysisPrompt: componentAnalysisPrompt,
107
+ noSamplingAggregationPrompt: noSamplingAggregationPrompt,
107
108
  });
108
109
  const reactConfigPath = configManager.getFrameworkConfigPath('react');
109
110
  Logger.log(`已创建默认 react 配置文件: ${reactConfigPath}`);
@@ -116,6 +117,7 @@ export async function loadConfig(mcpServer, configManagerInstance) {
116
117
  aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
117
118
  finalOptimizePrompt: finalOptimizeCodePromptVue,
118
119
  componentAnalysisPrompt: componentAnalysisPromptVue,
120
+ noSamplingAggregationPromptVue: noSamplingAggregationPromptVue,
119
121
  });
120
122
  const vueConfigPath = configManager.getFrameworkConfigPath('vue');
121
123
  Logger.log(`已创建默认 vue 配置文件: ${vueConfigPath}`);
@@ -127,6 +129,7 @@ export async function loadConfig(mcpServer, configManagerInstance) {
127
129
  aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
128
130
  finalOptimizePrompt: finalOptimizeCodePromptVue,
129
131
  componentAnalysisPrompt: componentAnalysisPromptVue,
132
+ noSamplingAggregationPrompt: noSamplingAggregationPromptVue,
130
133
  ...(vueConfig || {}),
131
134
  });
132
135
  }
@@ -347,17 +350,23 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
347
350
  chunkOptimizePrompt: chunkOptimizeCodePrompt,
348
351
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
349
352
  finalOptimizePrompt: finalOptimizeCodePrompt,
353
+ componentAnalysisPrompt: componentAnalysisPrompt,
354
+ noSamplingAggregationPrompt: noSamplingAggregationPrompt,
350
355
  }
351
356
  : {
352
357
  chunkOptimizePrompt: chunkOptimizeCodePromptVue,
353
358
  aggregationOptimizePrompt: aggregationOptimizeCodePromptVue,
354
359
  finalOptimizePrompt: finalOptimizeCodePromptVue,
360
+ componentAnalysisPrompt: componentAnalysisPromptVue,
361
+ noSamplingAggregationPrompt: noSamplingAggregationPromptVue,
355
362
  });
356
363
  // 如果指定了框架,加载框架配置的提示词
357
364
  let promptSetting = {
358
365
  chunkOptimizePrompt: savedPromptSetting?.chunkOptimizePrompt || curFrameworkDefaultConfig.chunkOptimizePrompt,
359
366
  aggregationOptimizePrompt: savedPromptSetting?.aggregationOptimizePrompt || curFrameworkDefaultConfig.aggregationOptimizePrompt,
360
367
  finalOptimizePrompt: savedPromptSetting?.finalOptimizePrompt || curFrameworkDefaultConfig.finalOptimizePrompt,
368
+ componentAnalysisPrompt: savedPromptSetting?.componentAnalysisPrompt || curFrameworkDefaultConfig.componentAnalysisPrompt,
369
+ noSamplingAggregationPrompt: savedPromptSetting?.noSamplingAggregationPrompt || curFrameworkDefaultConfig.noSamplingAggregationPrompt,
361
370
  };
362
371
  if (framework) {
363
372
  // 加载框架配置
@@ -368,6 +377,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
368
377
  chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || promptSetting.chunkOptimizePrompt,
369
378
  aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
370
379
  finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
380
+ componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || promptSetting.componentAnalysisPrompt,
381
+ noSamplingAggregationPrompt: frameworkConfig.noSamplingAggregationPrompt || promptSetting.noSamplingAggregationPrompt,
371
382
  };
372
383
  }
373
384
  }
@@ -395,6 +406,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
395
406
  chunkOptimizePrompt: chunkOptimizeCodePrompt,
396
407
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
397
408
  finalOptimizePrompt: finalOptimizeCodePrompt,
409
+ componentAnalysisPrompt: componentAnalysisPrompt,
410
+ noSamplingAggregationPrompt: noSamplingAggregationPrompt,
398
411
  };
399
412
  if (framework) {
400
413
  // 加载框架配置
@@ -405,6 +418,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
405
418
  chunkOptimizePrompt: frameworkConfig.chunkOptimizePrompt || promptSetting.chunkOptimizePrompt,
406
419
  aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
407
420
  finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
421
+ componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || promptSetting.componentAnalysisPrompt,
422
+ noSamplingAggregationPrompt: frameworkConfig.noSamplingAggregationPrompt || promptSetting.noSamplingAggregationPrompt,
408
423
  };
409
424
  }
410
425
  }
@@ -461,6 +476,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
461
476
  aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || aggregationOptimizeCodePrompt,
462
477
  finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || finalOptimizeCodePrompt,
463
478
  componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || componentAnalysisPrompt,
479
+ noSamplingAggregationPrompt: frameworkConfig.noSamplingAggregationPrompt || noSamplingAggregationPrompt,
464
480
  };
465
481
  res.json({
466
482
  success: true,
@@ -528,6 +544,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
528
544
  aggregationOptimizePrompt: aggregationOptimizeCodePrompt,
529
545
  finalOptimizePrompt: finalOptimizeCodePrompt,
530
546
  componentAnalysisPrompt: componentAnalysisPrompt,
547
+ noSamplingAggregationPrompt: noSamplingAggregationPrompt,
531
548
  };
532
549
  const frameworkConfig = await configManager.loadFrameworkConfig(framework);
533
550
  if (frameworkConfig && Object.keys(frameworkConfig).length > 0) {
@@ -537,6 +554,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
537
554
  aggregationOptimizePrompt: frameworkConfig.aggregationOptimizePrompt || promptSetting.aggregationOptimizePrompt,
538
555
  finalOptimizePrompt: frameworkConfig.finalOptimizePrompt || promptSetting.finalOptimizePrompt,
539
556
  componentAnalysisPrompt: frameworkConfig.componentAnalysisPrompt || promptSetting.componentAnalysisPrompt,
557
+ noSamplingAggregationPrompt: frameworkConfig.noSamplingAggregationPrompt || promptSetting.noSamplingAggregationPrompt,
540
558
  };
541
559
  }
542
560
  res.json({
@@ -554,6 +572,41 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
554
572
  });
555
573
  }
556
574
  });
575
+ // 获取框架默认提示词接口(不读取用户配置,直接返回代码中的原始默认值)
576
+ app.get('/getDefaultFrameworkConfig', async (req, res) => {
577
+ try {
578
+ const framework = req.query.framework;
579
+ if (!framework) {
580
+ res.status(400).json({
581
+ success: false,
582
+ message: '缺少必要参数: framework',
583
+ });
584
+ return;
585
+ }
586
+ // 根据框架返回对应的原始默认提示词
587
+ const isVue = framework.toLowerCase() === 'vue';
588
+ const promptSetting = {
589
+ chunkOptimizePrompt: isVue ? chunkOptimizeCodePromptVue : chunkOptimizeCodePrompt,
590
+ aggregationOptimizePrompt: isVue ? aggregationOptimizeCodePromptVue : aggregationOptimizeCodePrompt,
591
+ finalOptimizePrompt: isVue ? finalOptimizeCodePromptVue : finalOptimizeCodePrompt,
592
+ componentAnalysisPrompt: isVue ? componentAnalysisPromptVue : componentAnalysisPrompt,
593
+ noSamplingAggregationPrompt: isVue ? noSamplingAggregationPromptVue : noSamplingAggregationPrompt,
594
+ };
595
+ res.json({
596
+ success: true,
597
+ data: promptSetting,
598
+ message: '获取默认框架配置成功',
599
+ });
600
+ }
601
+ catch (error) {
602
+ Logger.error('获取默认框架配置失败:', error);
603
+ res.status(500).json({
604
+ success: false,
605
+ message: '获取默认框架配置失败',
606
+ error: error instanceof Error ? error.message : String(error),
607
+ });
608
+ }
609
+ });
557
610
  // 认证页面
558
611
  app.get('/auth-page', (req, res) => {
559
612
  try {
@@ -1102,7 +1155,6 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1102
1155
  try {
1103
1156
  const { includePaths, excludePaths } = req.query;
1104
1157
  const projectPath = getProjectRoot();
1105
- Logger.log('扫描项目组件:', { includePaths, excludePaths });
1106
1158
  // 读取项目组件(从 .sloth/components.json)
1107
1159
  let projectComponents = [];
1108
1160
  const slothPath = path.join(projectPath, '.sloth', 'components.json');
@@ -1467,7 +1519,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1467
1519
  }
1468
1520
  }
1469
1521
  Logger.log(`[analyzeChange] ${label} 图片替换完成, 替换数: ${replaceCount}, HTML长度: ${html.length} -> ${processedHtml.length}`);
1470
- return processedHtml
1522
+ return (processedHtml
1471
1523
  // 去除 data-id、data-name、data-type 属性(不参与 diff)
1472
1524
  .replace(/\s*data-id="[^"]*"/g, '')
1473
1525
  .replace(/\s*data-name="[^"]*"/g, '')
@@ -1478,7 +1530,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1478
1530
  .replace(/class="([^"]*)"/g, (_, classes) => {
1479
1531
  const cleaned = classes.replace(/\s+/g, ' ').trim();
1480
1532
  return `class="${cleaned}"`;
1481
- });
1533
+ }));
1482
1534
  };
1483
1535
  oldHtml = processHtmlForDiff(oldHtml, oldImageMap, 'old');
1484
1536
  newHtml = processHtmlForDiff(newHtml, newImageMap, 'new');
@@ -1534,7 +1586,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1534
1586
  addedLines,
1535
1587
  removedLines,
1536
1588
  unchangedLines,
1537
- changeRatio: ((addedLines + removedLines) / (addedLines + removedLines + unchangedLines) * 100).toFixed(1),
1589
+ changeRatio: (((addedLines + removedLines) / (addedLines + removedLines + unchangedLines)) * 100).toFixed(1),
1538
1590
  };
1539
1591
  // 5. 尝试使用 AI 总结变更(如果 MCP 服务器可用)
1540
1592
  let aiSummary = [];
@@ -1542,9 +1594,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1542
1594
  try {
1543
1595
  // 限制 diff 文本长度,避免 token 过大
1544
1596
  const maxDiffLength = 48000;
1545
- const truncatedDiff = diffText.length > maxDiffLength
1546
- ? diffText.substring(0, maxDiffLength) + '\n... (diff 内容已截断)'
1547
- : diffText;
1597
+ const truncatedDiff = diffText.length > maxDiffLength ? diffText.substring(0, maxDiffLength) + '\n... (diff 内容已截断)' : diffText;
1548
1598
  const prompt = `
1549
1599
  你是一个专业的前端开发专家。请分析以下设计稿 HTML 的 diff 变更,总结出具体的变更点。
1550
1600
 
@@ -1579,12 +1629,14 @@ ${truncatedDiff}
1579
1629
  Logger.log('=== AI 分析变更提示词 ===');
1580
1630
  Logger.log(prompt);
1581
1631
  Logger.log('=== 提示词结束 ===');
1582
- const { content: { text } } = await mcpServer.server.createMessage({
1583
- messages: [{
1632
+ const { content: { text }, } = await mcpServer.server.createMessage({
1633
+ messages: [
1634
+ {
1584
1635
  role: 'user',
1585
- content: { type: 'text', text: prompt }
1586
- }],
1587
- maxTokens: 48000
1636
+ content: { type: 'text', text: prompt },
1637
+ },
1638
+ ],
1639
+ maxTokens: 48000,
1588
1640
  }, { timeout: 2 * 60 * 1000 });
1589
1641
  // 解析 AI 返回的 JSON
1590
1642
  const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
@@ -1598,13 +1650,15 @@ ${truncatedDiff}
1598
1650
  }
1599
1651
  // 6. 如果 AI 分析失败,生成基础摘要
1600
1652
  if (aiSummary.length === 0 && changeSummary.totalChanges > 0) {
1601
- aiSummary = [{
1653
+ aiSummary = [
1654
+ {
1602
1655
  id: 'change_1',
1603
1656
  type: 'structure',
1604
1657
  title: '设计稿结构变更',
1605
1658
  description: `检测到 ${addedLines} 行新增内容和 ${removedLines} 行删除内容,变更比例约 ${changeSummary.changeRatio}%`,
1606
- suggestedAction: '请检查新设计稿的布局和样式变化,更新相应的代码'
1607
- }];
1659
+ suggestedAction: '请检查新设计稿的布局和样式变化,更新相应的代码',
1660
+ },
1661
+ ];
1608
1662
  }
1609
1663
  Logger.log(`变更分析完成: ${aiSummary.length} 个变更点`);
1610
1664
  res.json({
@@ -1687,7 +1741,11 @@ export async function getUserInput(payload) {
1687
1741
  return new Promise(async (resolve, reject) => {
1688
1742
  const token = uuidv4();
1689
1743
  const port = getPort();
1690
- const authUrl = `http://localhost:${port}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}&mode=${payload.mode}`;
1744
+ // 构建 URL,从 payload 中提取 clientApiSupport
1745
+ const clientApiSupport = payload.clientApiSupport;
1746
+ const supportSampling = clientApiSupport?.sampling ? '1' : '0';
1747
+ const supportRoots = clientApiSupport?.roots ? '1' : '0';
1748
+ const authUrl = `http://localhost:${port}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}&mode=${payload.mode}&supportSampling=${supportSampling}&supportRoots=${supportRoots}`;
1691
1749
  Logger.log('authUrl', authUrl);
1692
1750
  // 判断是主进程还是子进程
1693
1751
  const isMainProcess = httpServer !== null;
@@ -0,0 +1,143 @@
1
+ import { Logger } from './logger.js';
2
+ // 缓存检测结果,避免重复检测
3
+ let cachedCapabilities = null;
4
+ /**
5
+ * 检测 MCP Client 的 API 支持程度
6
+ * @param mcpServer MCP 服务器实例
7
+ * @param forceRecheck 是否强制重新检测(忽略缓存)
8
+ * @returns ClientApiSupport 对象
9
+ */
10
+ export async function detectClientApiSupport(mcpServer, forceRecheck = false) {
11
+ // 使用缓存
12
+ if (cachedCapabilities && !forceRecheck) {
13
+ return cachedCapabilities;
14
+ }
15
+ const support = {
16
+ sampling: false,
17
+ roots: false,
18
+ };
19
+ try {
20
+ // 方法1: 通过 capabilities 协商机制检测
21
+ // MCP SDK 在连接时会进行能力协商
22
+ const serverInstance = mcpServer.server;
23
+ // 尝试获取客户端能力
24
+ if (typeof serverInstance.getClientCapabilities === 'function') {
25
+ const clientCapabilities = serverInstance.getClientCapabilities();
26
+ Logger.log('客户端能力:', JSON.stringify(clientCapabilities, null, 2));
27
+ if (clientCapabilities) {
28
+ // 检查 sampling 能力
29
+ support.sampling = clientCapabilities.sampling !== undefined;
30
+ // 检查 roots 能力
31
+ support.roots = clientCapabilities.roots !== undefined;
32
+ }
33
+ }
34
+ // 方法2: 如果 capabilities 检测不到,通过 try-catch 探测
35
+ // 检测 roots 支持
36
+ if (!support.roots) {
37
+ support.roots = await probeRootsSupport(mcpServer);
38
+ }
39
+ // 检测 sampling 支持(仅当 capabilities 未声明时才探测)
40
+ if (!support.sampling) {
41
+ support.sampling = await probeSamplingSupport(mcpServer);
42
+ }
43
+ }
44
+ catch (error) {
45
+ Logger.log('检测客户端能力时出错:', error);
46
+ }
47
+ // 缓存结果
48
+ cachedCapabilities = support;
49
+ Logger.log('MCP Client API 支持检测结果:', support);
50
+ return support;
51
+ }
52
+ /**
53
+ * 探测 listRoots API 支持
54
+ */
55
+ async function probeRootsSupport(mcpServer) {
56
+ try {
57
+ const result = await mcpServer.server.listRoots();
58
+ // 如果调用成功,说明支持
59
+ return result !== undefined;
60
+ }
61
+ catch (error) {
62
+ // 检查错误类型
63
+ const errorMessage = error?.message || String(error);
64
+ const errorCode = error?.code;
65
+ // Method not found 或明确不支持的错误
66
+ if (errorCode === -32601 ||
67
+ errorMessage.includes('not supported') ||
68
+ errorMessage.includes('not implemented') ||
69
+ errorMessage.includes('Method not found')) {
70
+ return false;
71
+ }
72
+ // 其他错误(如超时、网络问题)可能是暂时性的,保守认为支持
73
+ Logger.log('探测 listRoots 时出现非致命错误:', errorMessage);
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * 探测 createMessage (sampling) API 支持
79
+ * 注意:这个探测可能会产生实际的 API 调用,应谨慎使用
80
+ */
81
+ async function probeSamplingSupport(mcpServer) {
82
+ try {
83
+ // 使用最小化的测试请求
84
+ // 注意:某些客户端可能会实际执行这个请求,所以使用非常小的 maxTokens
85
+ await mcpServer.server.createMessage({
86
+ messages: [
87
+ {
88
+ role: 'user',
89
+ content: {
90
+ type: 'text',
91
+ text: 'ping', // 最小化测试消息
92
+ },
93
+ },
94
+ ],
95
+ maxTokens: 1, // 最小化 token 消耗
96
+ }, {
97
+ timeout: 10000, // 10秒超时
98
+ });
99
+ // 如果调用成功,说明支持
100
+ return true;
101
+ }
102
+ catch (error) {
103
+ const errorMessage = error?.message || String(error);
104
+ const errorCode = error?.code;
105
+ // Method not found 或明确不支持的错误
106
+ if (errorCode === -32601 ||
107
+ errorMessage.includes('not supported') ||
108
+ errorMessage.includes('not implemented') ||
109
+ errorMessage.includes('Method not found') ||
110
+ errorMessage.includes('Sampling not supported')) {
111
+ Logger.log('客户端不支持 sampling API');
112
+ return false;
113
+ }
114
+ // 超时或其他错误,可能是支持但调用失败
115
+ // 保守处理:认为不支持,让业务逻辑走降级路径
116
+ Logger.log('探测 sampling 时出现错误:', errorMessage);
117
+ return false;
118
+ }
119
+ }
120
+ /**
121
+ * 清除缓存的能力检测结果
122
+ * 在重新连接时应调用此方法
123
+ */
124
+ export function clearCapabilitiesCache() {
125
+ cachedCapabilities = null;
126
+ Logger.log('已清除客户端能力缓存');
127
+ }
128
+ /**
129
+ * 检查是否支持 sampling
130
+ * 便捷方法,直接返回 boolean
131
+ */
132
+ export async function isSamplingSupported(mcpServer) {
133
+ const support = await detectClientApiSupport(mcpServer);
134
+ return support.sampling;
135
+ }
136
+ /**
137
+ * 检查是否支持 roots
138
+ * 便捷方法,直接返回 boolean
139
+ */
140
+ export async function isRootsSupported(mcpServer) {
141
+ const support = await detectClientApiSupport(mcpServer);
142
+ return support.roots;
143
+ }