sloth-d2c-mcp 1.0.4-beta93 → 1.0.4-beta94

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/cli/run.js CHANGED
@@ -102,7 +102,7 @@ if (args[0] === 'config') {
102
102
  const scriptPath = fileURLToPath(import.meta.url);
103
103
 
104
104
  // 后台启动子进程
105
- const child = spawn(process.execPath, [scriptPath, '--stdio'], {
105
+ const child = spawn(process.execPath, [scriptPath, '--server'], {
106
106
  detached: true,
107
107
  stdio: 'ignore',
108
108
  env: { ...process.env, NODE_ENV: 'cli' }
@@ -198,6 +198,8 @@ if (args[0] === 'config') {
198
198
  console.log(`[sloth] 共停止 ${killedCount} 个进程`);
199
199
  }
200
200
  process.exit(0);
201
+ } else if (args[0] === '--server') {
202
+ process.env.SLOTH_COMMAND = 'server';
201
203
  } else {
202
204
  // 设置环境变量为CLI模式
203
205
  process.env.NODE_ENV = "cli";
@@ -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, generateComponentId, stopSocketServer } from './server.js';
10
+ import { loadConfig, startHttpServer, stopHttpServer, generateComponentId } 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';
@@ -24,6 +24,22 @@ import * as path from 'path';
24
24
  import { fileURLToPath } from 'url';
25
25
  import { extractCodeAndComponents } from './utils/extract.js';
26
26
  import { trackToolCall } from './utils/tj.js';
27
+ const spawnServerProcess = async () => {
28
+ // run 命令:后台运行 sloth --stdio
29
+ const { spawn } = await import('node:child_process');
30
+ const { fileURLToPath } = await import('node:url');
31
+ // 获取当前脚本路径(兼容 Node.js 18)
32
+ const scriptPath = fileURLToPath(import.meta.url);
33
+ // 后台启动子进程
34
+ const child = spawn(process.execPath, [scriptPath, '--server'], {
35
+ detached: true,
36
+ stdio: 'ignore',
37
+ env: { ...process.env, NODE_ENV: 'cli' }
38
+ });
39
+ // 解除父进程对子进程的引用,允许父进程退出
40
+ child.unref();
41
+ console.log(`[sloth] 已在后台启动,PID: ${child.pid}`);
42
+ };
27
43
  /**
28
44
  * 启动http & socket监听
29
45
  * @param init 是否是初始化,初始化时需要连接stdio transport
@@ -38,9 +54,13 @@ export async function startListening(init = false) {
38
54
  if (init) {
39
55
  const transport = new StdioServerTransport();
40
56
  await mcpServer.connect(transport);
57
+ // 单独启动httpServer进程,用于网页访问
58
+ await spawnServerProcess();
59
+ }
60
+ else {
61
+ // 启动 HTTP 服务器 - 仅启用web服务
62
+ await startHttpServer(port, mcpServer, configManager, false);
41
63
  }
42
- // 启动 HTTP 服务器 - 仅启用web服务
43
- await startHttpServer(port, mcpServer, configManager, false);
44
64
  }
45
65
  else {
46
66
  // 启动 HTTP 服务器 - 这是核心功能,不依赖 VSCode 日志
@@ -112,7 +132,7 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
112
132
  catch (error) {
113
133
  Logger.log('获取根目录时出错:', error);
114
134
  }
115
- await startListening();
135
+ // await startListening()
116
136
  let userConfirmedChanges;
117
137
  let userHtmlDiff; // 存储 HTML diff 文本
118
138
  // 没有配置figmaApiKey,非数据推送模式,无法预览,直接唤起配置页面
@@ -192,9 +212,14 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
192
212
  await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml, true);
193
213
  }
194
214
  else {
195
- d2cNodeList = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'nodeList.json'));
196
- imageMap = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'imageMap.json'));
197
- absoluteHtml = await fileManager.loadAbsoluteHtml(fileKey, nodeId);
215
+ try {
216
+ d2cNodeList = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'nodeList.json'));
217
+ imageMap = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'imageMap.json'));
218
+ absoluteHtml = await fileManager.loadAbsoluteHtml(fileKey, nodeId);
219
+ }
220
+ catch (err) {
221
+ throw new Error('加载本地数据失败, 请检查文件路径:' + fileManager.getFilePathPublic(fileKey, nodeId, 'nodeList.json'));
222
+ }
198
223
  await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml, true);
199
224
  console.log('cache load data');
200
225
  }
@@ -231,6 +256,9 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
231
256
  else {
232
257
  const configData = JSON.parse(configDataString);
233
258
  const { mcp, groupsData, confirmedChanges, htmlDiff, ...rest } = configData;
259
+ pluginManager.executeHook('afterSubmitConfigData', {
260
+ configData,
261
+ });
234
262
  userConfirmedChanges = confirmedChanges;
235
263
  userHtmlDiff = htmlDiff; // 保存 HTML diff
236
264
  // 保存旧配置用于比较
@@ -473,17 +501,19 @@ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
473
501
  Logger.log(`加载组件数据库,共 ${componentsDatabase.length} 个组件`);
474
502
  // 构建组件映射提示词(包含组件映射、组件上下文、标记组件和完整组件数据库)
475
503
  const componentMappingPrompt = buildComponentMappingPrompt(componentMappings, componentContexts, markedComponents, componentsDatabase);
504
+ const { prompt: pluginFinalPrompt = '' } = await pluginManager.executeHook('beforeFinalPrompt', { configData: config.fileConfigs?.[fileKey] || '' });
505
+ console.log('pluginFinalPrompt', pluginFinalPrompt);
476
506
  // 使用提示词(已包含默认值)
477
507
  return {
478
508
  content: [
479
509
  isSupportSampling
480
510
  ? {
481
511
  type: 'text',
482
- text: finalPrompt + componentMappingPrompt,
512
+ text: finalPrompt + componentMappingPrompt + pluginFinalPrompt,
483
513
  }
484
514
  : {
485
515
  type: 'text',
486
- text: noSamplingPrompt + componentMappingPrompt,
516
+ text: noSamplingPrompt + componentMappingPrompt + pluginFinalPrompt,
487
517
  },
488
518
  ...codeSnippets.map((code) => {
489
519
  return {
@@ -1128,6 +1158,9 @@ async function main() {
1128
1158
  }
1129
1159
  process.exit(0);
1130
1160
  }
1161
+ if (process.env.SLOTH_COMMAND === 'server') {
1162
+ await startListening();
1163
+ }
1131
1164
  try {
1132
1165
  // 先输出基本启动信息到控制台,确保即使 VSCode 日志失败也能看到启动过程
1133
1166
  console.log(`[${new Date().toISOString()}] Starting Figma transcoding interceptor MCP server...`);
@@ -1179,7 +1212,6 @@ process.on('SIGTERM', async () => {
1179
1212
  catch (error) {
1180
1213
  // 忽略日志错误,确保清理过程继续
1181
1214
  }
1182
- await stopSocketServer();
1183
1215
  cleanupTauri();
1184
1216
  cleanupWeb();
1185
1217
  cleanupVSCode();
@@ -1290,6 +1322,10 @@ else if (args[0] === 'clear') {
1290
1322
  // 设置环境变量标识这是 clear 命令
1291
1323
  process.env.SLOTH_COMMAND = 'clear';
1292
1324
  }
1325
+ else if (args[0] === '--server') {
1326
+ // 设置环境变量标识这是 clear 命令
1327
+ process.env.SLOTH_COMMAND = 'server';
1328
+ }
1293
1329
  else {
1294
1330
  // 设置环境变量为CLI模式
1295
1331
  process.env.NODE_ENV = 'cli';
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
3
3
  import * as path from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import envPaths from 'env-paths';
6
+ import { createRequire } from 'module';
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = path.dirname(__filename);
8
9
  // 检测是否存在 pnpm
@@ -17,7 +18,7 @@ function hasPnpm() {
17
18
  }
18
19
  // 获取包管理器命令
19
20
  function getPackageManager() {
20
- return /* hasPnpm() ? 'pnpm' : */ 'npm';
21
+ return 'npm';
21
22
  }
22
23
  // 使用 env-paths 获取配置目录(与 config-manager 保持一致)
23
24
  const paths = envPaths('d2c-mcp');
@@ -25,7 +26,14 @@ const paths = envPaths('d2c-mcp');
25
26
  function getGlobalPackageDir() {
26
27
  // 在构建后的目录中: dist/build/plugin/loader.js
27
28
  // 需要向上三级才能找到包根目录
28
- return path.resolve(__dirname, '..', '..', '..');
29
+ // return path.resolve(__dirname, '..', '..', '..')
30
+ try {
31
+ const pm = getPackageManager();
32
+ return execSync(`${pm} root -g`, { encoding: 'utf-8' }).trim();
33
+ }
34
+ catch {
35
+ return '';
36
+ }
29
37
  }
30
38
  // 获取全局插件配置文件路径(与 config.json 在同一目录)
31
39
  function getGlobalPluginsConfigPath() {
@@ -68,6 +76,12 @@ async function loadWorkspacePluginsConfig(workspaceRoot) {
68
76
  return { plugins: [] };
69
77
  }
70
78
  }
79
+ export async function loadPluginFromGlobalDirAsync(packageName) {
80
+ const globalDir = getGlobalPackageDir();
81
+ const require = createRequire(path.join(globalDir));
82
+ const resolvedPath = require.resolve(packageName);
83
+ return await import(resolvedPath);
84
+ }
71
85
  /**
72
86
  * 解析插件配置项,统一返回 { name, config } 格式
73
87
  */
@@ -92,7 +106,7 @@ async function checkLocalPlugin(workspaceRoot, pluginName) {
92
106
  // 检查 npm 包是否已安装
93
107
  async function isNpmPackageInstalled(packageName) {
94
108
  try {
95
- await import(packageName);
109
+ await loadPluginFromGlobalDirAsync(packageName);
96
110
  return true;
97
111
  }
98
112
  catch {
@@ -125,11 +139,11 @@ async function tryInstallNpmPackage(packageName) {
125
139
  export async function installPlugin(packageName) {
126
140
  const globalDir = getGlobalPackageDir();
127
141
  console.log(`[sloth] 正在安装插件: ${packageName}`);
128
- console.log(`[sloth] 安装目录: ${globalDir}`);
142
+ // console.log(`[sloth] 安装目录: ${globalDir}`)
129
143
  try {
130
144
  const pm = getPackageManager();
131
145
  // 在全局包目录下执行安装
132
- execSync(`${pm} install ${packageName}`, {
146
+ execSync(`${pm} install -g ${packageName}`, {
133
147
  cwd: globalDir,
134
148
  stdio: 'inherit',
135
149
  });
@@ -239,15 +253,15 @@ export async function loadWorkspacePlugins(workspaceRoot) {
239
253
  if (!isInstalled) {
240
254
  // 尝试安装
241
255
  console.log(`[sloth] 插件 ${pluginName} 未安装,尝试安装...`);
242
- const installSuccess = await tryInstallNpmPackage(pluginName);
243
- if (!installSuccess) {
244
- failedPlugins.push(pluginName);
245
- continue;
246
- }
256
+ // const installSuccess = await tryInstallNpmPackage(pluginName)
257
+ // if (!installSuccess) {
258
+ // failedPlugins.push(pluginName)
259
+ // continue
260
+ // }
247
261
  }
248
262
  // 加载 npm 包
249
263
  try {
250
- const pluginModule = await import(pluginName);
264
+ const pluginModule = await loadPluginFromGlobalDirAsync(pluginName);
251
265
  console.log('pluginModule', pluginModule);
252
266
  const plugin = {
253
267
  name: pluginName,
@@ -100,6 +100,7 @@ class PluginManager {
100
100
  ...params,
101
101
  ...(lastReturnVal || {}),
102
102
  _config: context.config,
103
+ workspaceRoot: this.workspaceRoot,
103
104
  }, context);
104
105
  // 更新上一个插件信息
105
106
  lastPlugin = plugin.name;
@@ -107,6 +108,7 @@ class PluginManager {
107
108
  ...params,
108
109
  ...(lastReturnVal || {}),
109
110
  ...(result || {}),
111
+ workspaceRoot: this.workspaceRoot,
110
112
  };
111
113
  }
112
114
  catch (error) {
@@ -21,7 +21,8 @@ import { extractJson } from './utils/extract.js';
21
21
  import { componentAnalysisPrompt, componentAnalysisPromptVue, chunkOptimizeCodePromptKuikly, aggregationOptimizeCodePromptKuikly, finalOptimizeCodePromptKuikly, chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, frameworkPromptKuikly, noSamplingAggregationPrompt, noSamplingAggregationPromptVue, noSamplingAggregationPromptKuikly, chunkOptimizeCodePromptIosOc, aggregationOptimizeCodePromptIosOc, finalOptimizeCodePromptIosOc, noSamplingAggregationPromptIosOc, chunkOptimizeCodePromptIosSwift, aggregationOptimizeCodePromptIosSwift, finalOptimizeCodePromptIosSwift, noSamplingAggregationPromptIosSwift, } from 'sloth-d2c-node/convert';
22
22
  import { SocketServer } from './socket-server.js';
23
23
  import { getParam } from './utils/utils.js';
24
- import { listWorkspacePlugins } from './plugin/loader.js';
24
+ import { loadPluginFromGlobalDirAsync } from './plugin/loader.js';
25
+ import { pluginManager } from './plugin/manager.js';
25
26
  /**
26
27
  * 生成组件 ID(4位随机字符串)
27
28
  */
@@ -48,7 +49,7 @@ let configManager = null; // 配置管理器实例
48
49
  const __filename = fileURLToPath(import.meta.url);
49
50
  const __dirname = path.dirname(__filename);
50
51
  // 获取端口号
51
- let PORT = 0;
52
+ let PORT = 3100;
52
53
  // 导出获取端口号的函数
53
54
  export function getPort() {
54
55
  return PORT;
@@ -415,7 +416,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
415
416
  const fileKey = req.query.fileKey;
416
417
  const nodeId = req.query.nodeId;
417
418
  const framework = req.query.framework;
418
- const plugins = await listWorkspacePlugins(req.workspaceRoot);
419
+ // const plugins = await listWorkspacePlugins(req.workspaceRoot)
420
+ const plugins = await socketServer?.getTokenExtraField(req.uuid, 'plugins');
419
421
  if (fileKey) {
420
422
  // 如果提供了 fileKey,返回该 fileKey 的特定配置
421
423
  const globalConfig = await configManager.load();
@@ -449,7 +451,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
449
451
  defaultFramework: globalConfig.defaultFramework || 'react',
450
452
  fileKey: fileKey,
451
453
  groupsData: groupsData,
452
- plugins,
454
+ pluginSetting: plugins,
453
455
  },
454
456
  });
455
457
  }
@@ -494,7 +496,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
494
496
  imageSetting: configStorage.imageSetting || {},
495
497
  promptSetting: promptSetting,
496
498
  frameworks: frameworks,
497
- plugins,
499
+ pluginSetting: plugins,
498
500
  },
499
501
  });
500
502
  }
@@ -931,6 +933,25 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
931
933
  message: '日志重连已启动',
932
934
  });
933
935
  });
936
+ // 插件渲染器端点 - 返回JS代码字符串
937
+ app.get('/pluginRenderer', async (req, res) => {
938
+ console.log('pluginRenderer');
939
+ const pluginName = req.query.pluginName;
940
+ let pluginRenderer = null;
941
+ try {
942
+ // @ts-ignore
943
+ pluginRenderer = await loadPluginFromGlobalDirAsync(pluginName);
944
+ }
945
+ catch (error) {
946
+ console.error('加载插件渲染器失败:', error);
947
+ res.type('application/javascript; charset=utf-8').send(`alert(${pluginName}加载失败)`);
948
+ return;
949
+ }
950
+ const renderer = pluginRenderer.default.renderConfigForm;
951
+ console.log('pluginRenderer.default', pluginRenderer.default);
952
+ console.log('renderer', renderer);
953
+ res.type('application/javascript; charset=utf-8').send(renderer);
954
+ });
934
955
  // ==================== 组件映射相关接口 ====================
935
956
  /**
936
957
  * 获取项目根目录
@@ -1853,9 +1874,9 @@ ${truncatedDiff}
1853
1874
  // 启动 HTTP 服务器,监听端口
1854
1875
  httpServer = app.listen(port, async (err) => {
1855
1876
  if (err) {
1856
- Logger.error('HTTP 服务器启动失败:', err);
1857
1877
  httpServer = null;
1858
- return;
1878
+ Logger.error('HTTP 服务器启动失败:', err, '退出进程');
1879
+ process.exit(0);
1859
1880
  }
1860
1881
  Logger.log(`HTTP server listening on port ${port}`);
1861
1882
  Logger.log(`SSE endpoint available at http://localhost:${port}/sse`);
@@ -1875,7 +1896,7 @@ ${truncatedDiff}
1875
1896
  }
1876
1897
  catch (error) {
1877
1898
  Logger.error('Socket 服务器启动失败:', error);
1878
- Logger.error('进入子进程模式');
1899
+ process.exit(0);
1879
1900
  }
1880
1901
  });
1881
1902
  // 监听 SIGINT 信号,优雅关闭服务器和所有会话
@@ -1885,7 +1906,7 @@ ${truncatedDiff}
1885
1906
  // 清理等待中的认证请求
1886
1907
  pendingRequests.clear();
1887
1908
  // 关闭 Socket 服务器
1888
- await stopSocketServer();
1909
+ await stopHttpServer();
1889
1910
  // 关闭所有活跃的 SSE 和 streamable 传输,释放资源
1890
1911
  await closeTransports(transports.sse);
1891
1912
  await closeTransports(transports.streamable);
@@ -1941,7 +1962,7 @@ export async function getUserInput(payload) {
1941
1962
  Logger.log('Socket 客户端已连接');
1942
1963
  const workspaceRoot = fileManager.getWorkspaceRoot();
1943
1964
  // 注册 token 并等待响应,同时传递 extra 数据给主进程
1944
- const responsePromise = socketClient.registerToken(token, { workspaceRoot });
1965
+ const responsePromise = socketClient.registerToken(token, { workspaceRoot, plugins: pluginManager.getPlugins() });
1945
1966
  // 打开浏览器
1946
1967
  await open(authUrl);
1947
1968
  // 等待认证响应
@@ -1975,14 +1996,14 @@ export async function stopSocketServer() {
1975
1996
  }
1976
1997
  // 停止 HTTP 服务器,并关闭所有 SSE 传输
1977
1998
  export async function stopHttpServer() {
1999
+ // 关闭 Socket 服务器
2000
+ const closeSocket = stopSocketServer();
1978
2001
  if (!httpServer) {
1979
2002
  throw new Error('HTTP 服务器未运行');
1980
2003
  }
1981
2004
  return new Promise((resolve, reject) => {
1982
2005
  // 清理等待中的认证请求
1983
2006
  pendingRequests.clear();
1984
- // 关闭 Socket 服务器
1985
- const closeSocket = stopSocketServer();
1986
2007
  httpServer.close((err) => {
1987
2008
  if (err) {
1988
2009
  reject(err);
@@ -124,7 +124,7 @@ export class SocketClient {
124
124
  const timeout = setTimeout(() => {
125
125
  this.messageHandlers.delete(token);
126
126
  reject(new Error('等待认证响应超时'));
127
- }, 5 * 60 * 1000); // 5分钟超时
127
+ }, 30 * 60 * 1000); // 30分钟超时
128
128
  // 注册消息处理器
129
129
  this.messageHandlers.set(token, (data) => {
130
130
  clearTimeout(timeout);
@@ -1,10 +1,10 @@
1
1
  {
2
- "buildTime": "2026-01-19T02:41:15.253Z",
2
+ "buildTime": "2026-01-25T17:24:40.636Z",
3
3
  "mode": "build",
4
4
  "pages": {
5
5
  "main": {
6
6
  "file": "index.html",
7
- "size": 1646449,
7
+ "size": 1649150,
8
8
  "sizeFormatted": "1.57 MB"
9
9
  },
10
10
  "detail": {
@@ -13,6 +13,6 @@
13
13
  "sizeFormatted": "275.16 KB"
14
14
  }
15
15
  },
16
- "totalSize": 1928215,
16
+ "totalSize": 1930916,
17
17
  "totalSizeFormatted": "1.84 MB"
18
18
  }