sloth-d2c-mcp 1.0.4-beta78 → 1.0.4-beta79

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.
package/README.md CHANGED
@@ -1,100 +1,83 @@
1
- ## D2C MCP Server
1
+ # Sloth D2C MCP
2
2
 
3
- 这是一个MCP (Model Context Protocol) 服务器,提供Figma设计转代码的功能。
3
+ ## 简介
4
4
 
5
- ## 可用工具
5
+ Sloth D2C 设计稿转代码工具,包括 Sloth D2C 设计稿转代码 Figma 插件及 Sloth D2C MCP 工具。
6
6
 
7
- ### 1\. d2c\_figma\_intercept
7
+ Sloth D2C 设计稿转代码插件,面向前端开发与设计团队的智能化工具,支持设计稿快速转前端代码,并通过协同 MCP 工具,写入生产级前端代码;同时支持文本/图片+Prompt输入AI,生成目标风格页面并转译至Figma节点。
8
8
 
9
- 使用Tauri桌面应用获取用户输入的工具。
9
+ Sloth D2C MCP 工具,获取插件推送的转码数据,通过拦截页面配置框架选择、转换倍率、颜色格式、图片存储、提示词编辑等基础设置,同时支持圈选分组、组件映射、模块标记等高级设置,以生成提示词指导 Agent 生成目标代码并写入。
10
10
 
11
- **特点:**
11
+ ## 核心能力
12
12
 
13
- * 打开原生桌面应用
14
- * 更好的用户体验
15
- * 窗口始终保持在最前面
16
- * 支持快捷键操作
13
+ <!-- ### 节点清洗 -->
14
+ <!-- 待补充 -->
17
15
 
18
- **参数:**
16
+ ### 效果预览及源码调整
19
17
 
20
- * `fileKey`: Figma文件的key (必需)
21
- * `nodeId`: 节点ID (可选)
22
- * `depth`: 遍历深度 (可选)
18
+ 插件支持生成代码页面预览,同时支持源码二次编辑。在插件转码后,提供代码编辑器编辑源码,实时预览效果。
23
19
 
24
- ### 2\. d2c\_figma\_intercept\_web
20
+ ### AI 生成页面
25
21
 
26
- 使用网页浏览器获取用户输入的工具。
22
+ 插件支持文本/图片+Prompt 输入 AI,生成目标风格页面代码并预览。同时支持转译至成 Figma 节点并插入 Figma 设计稿。
27
23
 
28
- **特点:**
24
+ ### 自研 MCP 拦截器
29
25
 
30
- * 在默认浏览器中打开网页
31
- * 兼容性更好
32
- * 无需额外的桌面应用
26
+ 设计了创新的 MCP 拦截器。利用 await 挂起 MCP 的核心请求,为用户创造了一个交互窗口。通过配置拦截及数据推送突破 RESTful API 限制,最大化还原度。
33
27
 
34
- **参数:**
28
+ ### 支持多种框架
35
29
 
36
- * `fileKey`: Figma文件的key (必需)
37
- * `nodeId`: 节点ID (可选)
38
- * `depth`: 遍历深度 (可选)
30
+ MCP 默认支持 React、Vue 代码生成,系统内置提示词默认配置。同时支持新增自定义框架,配置自定义框架生成提示词。
39
31
 
40
- ## 使用方法
32
+ ### 多种图片存储方式
41
33
 
42
- 启动MCP服务器:
34
+ 支持图片本地存储、OSS 存储及自定义图片上传方式。
43
35
 
44
- 通过MCP客户端调用工具:
36
+ ### 圈选分组与采样
45
37
 
46
- 或者使用网页版本:
38
+ 支持圈选分组与智能采样,圈选分组功能允许你在设计稿预览页中手动划分代码结构区域,每个区域分别在不同的采用请求流程进行代码生成,提高转码效率和准确性。
47
39
 
48
- ```plaintext
49
- {
50
- "method": "tools/call",
51
- "params": {
52
- "name": "d2c_figma_intercept_web",
53
- "arguments": {
54
- "fileKey": "your-figma-file-key",
55
- "nodeId": "optional-node-id"
56
- }
57
- }
58
- }
59
- ```
40
+ ### 组件映射
60
41
 
61
- ## 端口配置
42
+ 从项目中选择组件文件,将项目中的组件映射对应圈选元素,生成代码时直接引用对应组件。
62
43
 
63
- * MCP服务器: 端口3001 (如果被占用会自动尝试下一个端口)
64
- * 网页服务器: 端口3002
65
- * Tauri回调服务器: 动态端口 (从3001开始)
44
+ ### 模块标记
66
45
 
67
- ## 开发
46
+ 对圈选后的模块进行标记,方便其他设计稿类似模块的直接复用,无需重复生成。
68
47
 
69
- ```plaintext
70
- # 安装依赖
71
- npm install
48
+ ## 安装 MCP
72
49
 
73
- # 开发模式
74
- npm run dev
50
+ ```bash
51
+ # 使用 pnpm(推荐)
52
+ pnpm install sloth-d2c-mcp -g
75
53
 
76
- # 构建
77
- npm run build
54
+ # 或使用 npm
55
+ npm install sloth-d2c-mcp -g
78
56
 
79
- # 测试
80
- node test-both-tools.js
57
+ # 验证安装
58
+ sloth
81
59
  ```
82
60
 
83
- ```plaintext
84
- {
85
- "method": "tools/call",
86
- "params": {
87
- "name": "d2c_figma_intercept",
88
- "arguments": {
89
- "fileKey": "your-figma-file-key",
90
- "nodeId": "optional-node-id"
91
- }
92
- }
93
- }
61
+ ## 配置 MCP
62
+
63
+ **TME-Continue**
64
+
65
+ ```yaml
66
+ - name: sloth-d2c-mcp
67
+ command: sloth
68
+ connectionTimeout: 3000000
69
+ args:
70
+ - --stdio
94
71
  ```
95
72
 
96
- ```plaintext
97
- npm run dev
73
+ **Copilot**
74
+
75
+ ```json
76
+ "d2c-cli": {
77
+ "type": "stdio",
78
+ "command": "sloth",
79
+ "args": ["--stdio"]
80
+ }
98
81
  ```
99
82
 
100
- npx @modelcontextprotocol/_inspector_
83
+ > **提示**:推荐使用 Claude AI 模型。若需转码长设计稿,请开启 MCP 采样功能以优化转码策略(TME-Continue 和 Copilot 均支持)。
package/cli/run.js CHANGED
@@ -93,6 +93,26 @@ if (args[0] === 'config') {
93
93
  process.env.SLOTH_COMMAND = 'framework';
94
94
  // 传递子命令参数
95
95
  process.env.SLOTH_FRAMEWORK_ARGS = JSON.stringify(args.slice(1));
96
+ } else if (args[0] === 'run') {
97
+ // run 命令:后台运行 sloth --stdio
98
+ const { spawn } = await import('node:child_process');
99
+ const { fileURLToPath } = await import('node:url');
100
+
101
+ // 获取当前脚本路径(兼容 Node.js 18)
102
+ const scriptPath = fileURLToPath(import.meta.url);
103
+
104
+ // 后台启动子进程
105
+ const child = spawn(process.execPath, [scriptPath, '--stdio'], {
106
+ detached: true,
107
+ stdio: 'ignore',
108
+ env: { ...process.env, NODE_ENV: 'cli' }
109
+ });
110
+
111
+ // 解除父进程对子进程的引用,允许父进程退出
112
+ child.unref();
113
+
114
+ console.log(`[sloth] 已在后台启动,PID: ${child.pid}`);
115
+ process.exit(0);
96
116
  } else {
97
117
  // 设置环境变量为CLI模式
98
118
  process.env.NODE_ENV = "cli";
@@ -209,3 +209,97 @@ export function buildPlaceholderPrompt(placeholders) {
209
209
  prompt += '\n将 HTML 标签转换为真实组件调用,但不需要添加对应的 import 语句,保持其在布局中的位置。\n';
210
210
  return prompt;
211
211
  }
212
+ /**
213
+ * 构建完整的 AI 代码修改提示词
214
+ * 用于用户确认变更后,生成可直接发送给 AI 的完整提示词
215
+ * @param confirmedChanges 用户确认的变更点列表
216
+ * @param options 配置选项
217
+ * @returns 完整的 AI 提示词
218
+ */
219
+ export function buildFullUpdatePromptForAI(confirmedChanges, options = {}) {
220
+ const { fileKey, nodeId, htmlDiff } = options;
221
+ if (!confirmedChanges || confirmedChanges.length === 0) {
222
+ return '没有需要修改的变更点。';
223
+ }
224
+ const typeLabels = {
225
+ layout: '布局变更',
226
+ style: '样式变更',
227
+ content: '内容变更',
228
+ structure: '结构变更',
229
+ };
230
+ let prompt = `# 设计稿变更 - 代码修改任务
231
+
232
+ ## 任务背景
233
+
234
+ 用户的 UI 设计稿发生了变更,需要根据以下变更点修改现有代码。这是一个**增量修改任务**,不是全新创建。
235
+
236
+ ## 前置要求
237
+
238
+ **请先确认当前上下文中是否包含需要修改的代码。**
239
+
240
+ 如果当前上下文中没有相关代码,请让用户:
241
+ 1. 提供需要修改的代码文件路径,以便读取
242
+ 2. 或直接粘贴需要修改的代码内容
243
+
244
+ ---
245
+
246
+ ## 变更信息
247
+
248
+ `;
249
+ // 添加设计稿信息(如果有)
250
+ if (fileKey) {
251
+ prompt += `- **设计稿标识**: ${fileKey}${nodeId ? ` / ${nodeId}` : ''}\n`;
252
+ }
253
+ prompt += `- **变更数量**: ${confirmedChanges.length} 个\n\n`;
254
+ // 变更点详情
255
+ prompt += `## 变更点详情\n\n`;
256
+ confirmedChanges.forEach((change, index) => {
257
+ prompt += `### ${index + 1}. ${change.title}\n\n`;
258
+ prompt += `- **类型**: ${typeLabels[change.type] || change.type}\n`;
259
+ prompt += `- **描述**: ${change.description}\n`;
260
+ prompt += `- **建议操作**: ${change.suggestedAction}\n`;
261
+ if (change.userPrompt) {
262
+ prompt += `- **用户补充**: ${change.userPrompt}\n`;
263
+ }
264
+ prompt += `\n`;
265
+ });
266
+ // 添加 HTML Diff(如果有)
267
+ if (htmlDiff && htmlDiff.trim()) {
268
+ prompt += `## 设计稿 HTML Diff
269
+
270
+ 以下是新旧设计稿的 HTML 差异,可帮助你精确定位需要修改的样式和布局:
271
+
272
+ \`\`\`diff
273
+ ${htmlDiff}
274
+ \`\`\`
275
+
276
+ **说明**:
277
+ - \`---\` 开头的行表示被删除的内容(旧设计稿)
278
+ - \`+++\` 开头的行表示新增的内容(新设计稿)
279
+ - HTML 使用 Tailwind CSS 类名
280
+
281
+ `;
282
+ }
283
+ // 修改指南
284
+ prompt += `## 修改要求
285
+
286
+ ### 核心原则
287
+
288
+ 1. **基于现有实现**: 分析当前项目的技术栈和代码实现方式,在此基础上进行修改
289
+ 2. **最小化变更**: 只修改与变更点相关的代码,保持其他部分不变
290
+ 3. **保持一致性**: 遵循现有代码的命名规范、代码风格和架构模式
291
+ 4. **增量修改**: 优先通过修改现有代码实现,避免大规模重构
292
+
293
+ ### 修改流程
294
+
295
+ 1. **分析现有代码**: 理解当前代码的技术栈、组件结构、样式方案
296
+ 2. **定位修改位置**: 根据变更点找到需要修改的具体代码位置
297
+ 3. **实施修改**: 按照现有代码风格进行修改
298
+ 4. **验证完整性**: 确保修改后代码可正常运行
299
+
300
+ ---
301
+
302
+ **请开始分析代码并执行修改。如果需要更多信息或代码上下文,请告诉我。**
303
+ `;
304
+ return prompt;
305
+ }
@@ -13,7 +13,7 @@ import { updateImageMapIfNeeded } from './utils/update.js';
13
13
  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
- import { buildComponentMappingPrompt, buildPlaceholderPrompt } from './core/prompt-builder.js';
16
+ import { buildComponentMappingPrompt, buildFullUpdatePromptForAI, buildPlaceholderPrompt } from './core/prompt-builder.js';
17
17
  // @ts-ignore
18
18
  import * as flatted from 'flatted';
19
19
  import { promises as fs } from 'fs';
@@ -77,11 +77,16 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
77
77
  nodeId: z.string().optional().describe('The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided'),
78
78
  depth: z.number().optional().describe('How many levels deep to traverse the node tree, only use if explicitly requested by the user'),
79
79
  local: z.boolean().optional().describe('Whether to use local data cache, default is false'),
80
+ update: z
81
+ .boolean()
82
+ .optional()
83
+ .default(false)
84
+ .describe('Set to true only when user explicitly wants to modify/update previously generated code. Default is false (create new code).'),
80
85
  }, async (args) => {
81
86
  try {
82
87
  trackToolCall('d2c_figma');
83
88
  Logger.log(`收到工具调用参数:`, JSON.stringify(args, null, 2));
84
- const { fileKey, nodeId, depth, local } = args;
89
+ const { fileKey, nodeId, depth, local, update = false } = args;
85
90
  let config = await configManager.load();
86
91
  let hasLaunchWebview = false;
87
92
  // 检测 VSCode 扩展是否可用
@@ -99,19 +104,21 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
99
104
  Logger.log('获取根目录时出错:', error);
100
105
  }
101
106
  await startListening();
102
- // 没有配置figmaApiKey,无法预览,直接唤起配置页面
103
- if (!config.mcp?.figmaApiKey) {
107
+ let userConfirmedChanges;
108
+ let userHtmlDiff; // 存储 HTML diff 文本
109
+ // 没有配置figmaApiKey,非数据推送模式,无法预览,直接唤起配置页面
110
+ if (!config.mcp?.figmaApiKey && !local) {
104
111
  hasLaunchWebview = true;
105
112
  let configDataString;
106
113
  if (isVSCodePluginAvailable) {
107
114
  // 优先使用 VSCode 扩展
108
115
  Logger.log('使用 VSCode 扩展获取用户输入');
109
- configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId });
116
+ configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
110
117
  }
111
118
  else {
112
119
  // 降级到网页浏览器
113
120
  Logger.log('VSCode 扩展不可用,降级到网页浏览器');
114
- configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId });
121
+ configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
115
122
  }
116
123
  console.log('getting configDataString', configDataString);
117
124
  if (!configDataString || configDataString.trim() === '') {
@@ -119,7 +126,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
119
126
  }
120
127
  else {
121
128
  const configData = JSON.parse(configDataString);
122
- const { mcp, groupsData, ...rest } = configData;
129
+ const { mcp, groupsData, confirmedChanges, htmlDiff, ...rest } = configData;
123
130
  config = {
124
131
  ...config,
125
132
  mcp,
@@ -128,6 +135,8 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
128
135
  [fileKey]: rest,
129
136
  },
130
137
  };
138
+ userConfirmedChanges = confirmedChanges;
139
+ userHtmlDiff = htmlDiff; // 保存 HTML diff
131
140
  mcpServer.setConfig(config.mcp);
132
141
  configManager.save(config);
133
142
  if (rest) {
@@ -160,12 +169,12 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
160
169
  await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', flatted.stringify(d2cNodeList));
161
170
  await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
162
171
  // 保存figma绝对定位代码
163
- await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml);
172
+ await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml);
164
173
  }
165
174
  else {
166
175
  d2cNodeList = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'nodeList.json'));
167
176
  imageMap = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'imageMap.json'));
168
- absoluteHtml = await fileManager.loadFile(fileKey, nodeId, 'absolute.html');
177
+ absoluteHtml = await fileManager.loadAbsoluteHtml(fileKey, nodeId);
169
178
  console.log('cache load data');
170
179
  }
171
180
  console.log('d2cNodeList', d2cNodeList);
@@ -177,10 +186,10 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
177
186
  Logger.log(`收到网页工具调用参数:`, JSON.stringify(args, null, 2));
178
187
  let configDataString;
179
188
  if (isVSCodePluginAvailable) {
180
- configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId });
189
+ configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
181
190
  }
182
191
  else {
183
- configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId });
192
+ configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId, mode: update ? 'update' : 'create' });
184
193
  }
185
194
  console.log('收到网页提交数据', configDataString);
186
195
  if (!configDataString || configDataString.trim() === '') {
@@ -188,7 +197,9 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
188
197
  }
189
198
  else {
190
199
  const configData = JSON.parse(configDataString);
191
- const { mcp, groupsData, ...rest } = configData;
200
+ const { mcp, groupsData, confirmedChanges, htmlDiff, ...rest } = configData;
201
+ userConfirmedChanges = confirmedChanges;
202
+ userHtmlDiff = htmlDiff; // 保存 HTML diff
192
203
  // 保存旧配置用于比较
193
204
  const oldFileConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
194
205
  config = {
@@ -227,6 +238,22 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
227
238
  }
228
239
  }
229
240
  }
241
+ // Update 模式:直接返回提示词,不需要复杂的采样逻辑
242
+ if (update) {
243
+ // 保存图片文件
244
+ saveImageFile({ imageMap, root });
245
+ Logger.log(`Update 模式: 收到已构建的提示词,直接返回`);
246
+ Logger.log(`Update 模式: 变更点数量 - ${userConfirmedChanges?.length || 0} 个`);
247
+ Logger.log(`Update 模式: HTML Diff 长度 - ${userHtmlDiff?.length || 0} 字符`);
248
+ return {
249
+ content: [
250
+ {
251
+ type: 'text',
252
+ text: buildFullUpdatePromptForAI(userConfirmedChanges, { htmlDiff: userHtmlDiff }),
253
+ },
254
+ ],
255
+ };
256
+ }
230
257
  // 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
231
258
  const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
232
259
  const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
@@ -385,10 +412,10 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
385
412
  const markedComponents = [];
386
413
  if (groupsData && groupsData.length > 0) {
387
414
  groupsData.forEach((group) => {
388
- if (group.marked && group.screenshot?.hash && group.name) {
415
+ if (group.marked && group.screenshot && group.name) {
389
416
  markedComponents.push({
390
417
  name: group.name,
391
- signature: group.screenshot.hash,
418
+ signature: group.screenshot,
392
419
  });
393
420
  }
394
421
  });
@@ -520,6 +547,9 @@ mcpServer.tool('mark_components', 'Mark and save components to project component
520
547
  super: comp.super,
521
548
  description: comp.description,
522
549
  };
550
+ if (comp.signature) {
551
+ storedComponent.signature = comp.signature;
552
+ }
523
553
  if (existingIndex !== undefined) {
524
554
  // 更新已存在的组件(保留原始导入时间)
525
555
  const originalImportedAt = existingComponents[existingIndex].importedAt;