sloth-d2c-mcp 1.0.4-beta100
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 +83 -0
- package/cli/run.js +328 -0
- package/cli/sloth-server.log +1622 -0
- package/dist/build/config-manager/index.js +240 -0
- package/dist/build/core/prompt-builder.js +366 -0
- package/dist/build/core/sampling.js +375 -0
- package/dist/build/core/types.js +1 -0
- package/dist/build/index.js +852 -0
- package/dist/build/interceptor/client.js +142 -0
- package/dist/build/interceptor/vscode.js +143 -0
- package/dist/build/interceptor/web.js +28 -0
- package/dist/build/plugin/index.js +4 -0
- package/dist/build/plugin/loader.js +349 -0
- package/dist/build/plugin/manager.js +129 -0
- package/dist/build/plugin/types.js +6 -0
- package/dist/build/server.js +2116 -0
- package/dist/build/socket-client.js +166 -0
- package/dist/build/socket-server.js +260 -0
- package/dist/build/utils/client-capabilities.js +143 -0
- package/dist/build/utils/extract.js +168 -0
- package/dist/build/utils/file-manager.js +868 -0
- package/dist/build/utils/image-matcher.js +154 -0
- package/dist/build/utils/logger.js +90 -0
- package/dist/build/utils/opencv-loader.js +70 -0
- package/dist/build/utils/prompt-parser.js +46 -0
- package/dist/build/utils/tj.js +139 -0
- package/dist/build/utils/update.js +100 -0
- package/dist/build/utils/utils.js +184 -0
- package/dist/build/utils/vscode-logger.js +133 -0
- package/dist/build/utils/webpack-substitutions.js +196 -0
- package/dist/interceptor-web/dist/build-report.json +18 -0
- package/dist/interceptor-web/dist/detail.html +1 -0
- package/dist/interceptor-web/dist/index.html +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { convertFigmaToD2CNode, generateAbsoluteHtml, getBoundingBox, getCode, chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, noSamplingAggregationPrompt, } from 'sloth-d2c-node/convert';
|
|
4
|
+
import axios from 'axios';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { ConfigManager, defaultConfigData, } from './config-manager/index.js';
|
|
7
|
+
import { cleanup as cleanupTauri } from './interceptor/client.js';
|
|
8
|
+
import { cleanup as cleanupVSCode, getUserInputFromVSCode, isVSCodeAvailable } from './interceptor/vscode.js';
|
|
9
|
+
import { cleanup as cleanupWeb, getUserInput } from './interceptor/web.js';
|
|
10
|
+
import { loadConfig, startHttpServer, stopHttpServer, generateComponentId } from './server.js';
|
|
11
|
+
import { FileManager } from './utils/file-manager.js';
|
|
12
|
+
import { updateImageMapIfNeeded } from './utils/update.js';
|
|
13
|
+
import { Logger } from './utils/logger.js';
|
|
14
|
+
import { getAvailablePort, saveImageFile, replaceImageSrc, formatListRoots } from './utils/utils.js';
|
|
15
|
+
import { processSampling, buildNestingStructure } from './core/sampling.js';
|
|
16
|
+
import { buildComponentMappingPrompt, buildFullUpdatePromptForAI, buildPlaceholderPrompt } from './core/prompt-builder.js';
|
|
17
|
+
import { pluginManager } from './plugin/manager.js';
|
|
18
|
+
import { detectClientApiSupport, clearCapabilitiesCache } from './utils/client-capabilities.js';
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
import * as flatted from 'flatted';
|
|
21
|
+
import { promises as fs } from 'fs';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { extractCodeAndComponents } from './utils/extract.js';
|
|
25
|
+
import { trackToolCall } from './utils/tj.js';
|
|
26
|
+
const spawnServerProcess = async () => {
|
|
27
|
+
const { spawn } = await import('node:child_process');
|
|
28
|
+
// 直接调用 sloth CLI 命令
|
|
29
|
+
const child = spawn('sloth', ['server', 'start'], {
|
|
30
|
+
detached: true,
|
|
31
|
+
stdio: 'ignore',
|
|
32
|
+
shell: true, // 需要 shell 来解析命令
|
|
33
|
+
env: { ...process.env }
|
|
34
|
+
});
|
|
35
|
+
child.unref();
|
|
36
|
+
console.log(`[sloth] MCP触发:已在后台启动,PID: ${child.pid}`);
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 启动http & socket监听
|
|
40
|
+
* @param init 是否是初始化,初始化时需要连接stdio transport
|
|
41
|
+
*/
|
|
42
|
+
export async function startListening(init = false) {
|
|
43
|
+
const isStdioMode = process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
|
|
44
|
+
const configManager = new ConfigManager('d2c-mcp');
|
|
45
|
+
const port = await getAvailablePort();
|
|
46
|
+
if (isStdioMode) {
|
|
47
|
+
Logger.log(`Stdio模式启动`);
|
|
48
|
+
await loadConfig(mcpServer, configManager);
|
|
49
|
+
if (init) {
|
|
50
|
+
const transport = new StdioServerTransport();
|
|
51
|
+
await mcpServer.connect(transport);
|
|
52
|
+
// 单独启动httpServer进程,用于网页访问
|
|
53
|
+
spawnServerProcess();
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// 启动 HTTP 服务器 - 仅启用web服务
|
|
57
|
+
await startHttpServer(port, mcpServer, configManager, false);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// 启动 HTTP 服务器 - 这是核心功能,不依赖 VSCode 日志
|
|
62
|
+
await startHttpServer(port, mcpServer, configManager, true);
|
|
63
|
+
}
|
|
64
|
+
// 服务器启动成功后再尝试使用 Logger(包含 VSCode 日志)
|
|
65
|
+
Logger.log(`Server started successfully on port ${port}!`);
|
|
66
|
+
}
|
|
67
|
+
export class D2CMcpServer extends McpServer {
|
|
68
|
+
figmaApiKey = '';
|
|
69
|
+
baseURL = '';
|
|
70
|
+
constructor(serverInfo, options) {
|
|
71
|
+
super(serverInfo, options);
|
|
72
|
+
}
|
|
73
|
+
setConfig(config) {
|
|
74
|
+
this.figmaApiKey = config?.figmaApiKey || '';
|
|
75
|
+
this.baseURL = config?.baseURL || '';
|
|
76
|
+
console.log('setConfig', this.figmaApiKey, this.baseURL);
|
|
77
|
+
axios.defaults.baseURL = this.baseURL;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// 创建 MCP 服务器实例
|
|
81
|
+
const mcpServer = new D2CMcpServer({
|
|
82
|
+
name: 'transcoding-interceptor-server',
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
}, {
|
|
85
|
+
capabilities: {
|
|
86
|
+
resources: {},
|
|
87
|
+
tools: {},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const configManager = new ConfigManager('d2c-mcp');
|
|
91
|
+
export const fileManager = new FileManager('d2c-mcp');
|
|
92
|
+
// 注册 Figma 转码拦截工具
|
|
93
|
+
mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
|
|
94
|
+
fileKey: z.string().describe('The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...'),
|
|
95
|
+
nodeId: z.string().optional().describe('The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided'),
|
|
96
|
+
depth: z.number().optional().describe('How many levels deep to traverse the node tree, only use if explicitly requested by the user'),
|
|
97
|
+
local: z.boolean().optional().describe('Whether to use local data cache, default is false'),
|
|
98
|
+
update: z
|
|
99
|
+
.boolean()
|
|
100
|
+
.optional()
|
|
101
|
+
.default(false)
|
|
102
|
+
.describe('Set to true only when user explicitly wants to modify/update previously generated code. Default is false (create new code).'),
|
|
103
|
+
}, async (args) => {
|
|
104
|
+
try {
|
|
105
|
+
trackToolCall('d2c_figma');
|
|
106
|
+
Logger.log(`收到工具调用参数:`, JSON.stringify(args, null, 2));
|
|
107
|
+
const { fileKey, nodeId, depth, local, update = false } = args;
|
|
108
|
+
let config = await configManager.load();
|
|
109
|
+
let hasLaunchWebview = false;
|
|
110
|
+
// 检测客户端 API 支持程度
|
|
111
|
+
const clientApiSupport = await detectClientApiSupport(mcpServer);
|
|
112
|
+
// 检测 VSCode 扩展是否可用
|
|
113
|
+
const isVSCodePluginAvailable = await isVSCodeAvailable();
|
|
114
|
+
Logger.log(`VSCode 扩展检测结果: ${isVSCodePluginAvailable ? '可用' : '不可用'}`);
|
|
115
|
+
let root = './';
|
|
116
|
+
try {
|
|
117
|
+
const rootRes = await mcpServer.server.listRoots();
|
|
118
|
+
Logger.log('获取根目录:', rootRes);
|
|
119
|
+
root = formatListRoots(rootRes);
|
|
120
|
+
Logger.log('标准化根目录:', root);
|
|
121
|
+
// 设置 fileManager 的工作目录根路径
|
|
122
|
+
fileManager.setWorkspaceRoot(root);
|
|
123
|
+
// 设置插件管理器的工作区根目录并初始化
|
|
124
|
+
pluginManager.setWorkspaceRoot(root);
|
|
125
|
+
await pluginManager.initialize();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
Logger.log('获取根目录时出错:', error);
|
|
129
|
+
}
|
|
130
|
+
// await startListening()
|
|
131
|
+
let userConfirmedChanges;
|
|
132
|
+
let userHtmlDiff; // 存储 HTML diff 文本
|
|
133
|
+
// 没有配置figmaApiKey,非数据推送模式,无法预览,直接唤起配置页面
|
|
134
|
+
if (!config.mcp?.figmaApiKey && !local) {
|
|
135
|
+
hasLaunchWebview = true;
|
|
136
|
+
let configDataString;
|
|
137
|
+
if (isVSCodePluginAvailable) {
|
|
138
|
+
// 优先使用 VSCode 扩展
|
|
139
|
+
Logger.log('使用 VSCode 扩展获取用户输入');
|
|
140
|
+
configDataString = await getUserInputFromVSCode({
|
|
141
|
+
fileKey: fileKey,
|
|
142
|
+
nodeId: nodeId,
|
|
143
|
+
mode: update ? 'update' : 'create',
|
|
144
|
+
clientApiSupport,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// 降级到网页浏览器
|
|
149
|
+
Logger.log('VSCode 扩展不可用,降级到网页浏览器');
|
|
150
|
+
configDataString = await getUserInput({
|
|
151
|
+
fileKey: fileKey,
|
|
152
|
+
nodeId: nodeId,
|
|
153
|
+
mode: update ? 'update' : 'create',
|
|
154
|
+
clientApiSupport,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
console.log('getting configDataString', configDataString);
|
|
158
|
+
if (!configDataString || configDataString.trim() === '') {
|
|
159
|
+
throw new Error('未提供有效的转码配置');
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const configData = JSON.parse(configDataString);
|
|
163
|
+
const { mcp, groupsData, confirmedChanges, htmlDiff, ...rest } = configData;
|
|
164
|
+
config = {
|
|
165
|
+
...config,
|
|
166
|
+
mcp,
|
|
167
|
+
fileConfigs: {
|
|
168
|
+
...config.fileConfigs,
|
|
169
|
+
[fileKey]: rest,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
userConfirmedChanges = confirmedChanges;
|
|
173
|
+
userHtmlDiff = htmlDiff; // 保存 HTML diff
|
|
174
|
+
mcpServer.setConfig(config.mcp);
|
|
175
|
+
configManager.save(config);
|
|
176
|
+
if (rest) {
|
|
177
|
+
const { promptSetting, ...restConfig } = rest;
|
|
178
|
+
await fileManager.saveConfigSetting(fileKey, nodeId, restConfig);
|
|
179
|
+
Logger.log(`已保存 configSetting 到 fileKey: ${fileKey}, nodeId: ${nodeId}`);
|
|
180
|
+
}
|
|
181
|
+
// 将 groupsData 保存到 fileManager 按 nodeId 存储
|
|
182
|
+
if (groupsData && groupsData.length > 0) {
|
|
183
|
+
await fileManager.saveGroupsData(fileKey, nodeId, groupsData);
|
|
184
|
+
Logger.log(`已保存 groupsData 到 nodeId: ${nodeId}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// 调用 get_ai_group_layout
|
|
189
|
+
Logger.log(`Fetching AI group layout for ${nodeId ? `node ${nodeId} from file` : `full file`} ${fileKey}`);
|
|
190
|
+
let d2cNodeList, imageMap, absoluteHtml;
|
|
191
|
+
if (!local) {
|
|
192
|
+
const { d2cNodeList: nodes, imageMap: imgMap } = await convertFigmaToD2CNode({
|
|
193
|
+
fileKey,
|
|
194
|
+
nodeId,
|
|
195
|
+
depth,
|
|
196
|
+
config: config.fileConfigs?.[fileKey] || defaultConfigData,
|
|
197
|
+
figmaToken: mcpServer.figmaApiKey,
|
|
198
|
+
});
|
|
199
|
+
d2cNodeList = nodes;
|
|
200
|
+
imageMap = imgMap;
|
|
201
|
+
absoluteHtml = await generateAbsoluteHtml(d2cNodeList);
|
|
202
|
+
// 保存nodeList
|
|
203
|
+
await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', flatted.stringify(d2cNodeList));
|
|
204
|
+
await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
|
|
205
|
+
// 保存figma绝对定位代码
|
|
206
|
+
await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml);
|
|
207
|
+
await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml, true);
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
try {
|
|
211
|
+
d2cNodeList = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'nodeList.json'));
|
|
212
|
+
imageMap = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'imageMap.json'));
|
|
213
|
+
absoluteHtml = await fileManager.loadAbsoluteHtml(fileKey, nodeId);
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
throw new Error('加载本地数据失败, 请检查文件路径:' + fileManager.getFilePathPublic(fileKey, nodeId, 'nodeList.json'));
|
|
217
|
+
}
|
|
218
|
+
await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml, true);
|
|
219
|
+
console.log('cache load data');
|
|
220
|
+
}
|
|
221
|
+
console.log('d2cNodeList', d2cNodeList);
|
|
222
|
+
console.log('imageMap', imageMap);
|
|
223
|
+
console.log('absoluteHtml', absoluteHtml);
|
|
224
|
+
pluginManager.executeHook('onFetchImage', { imageMap });
|
|
225
|
+
console.log('pluginManager executeHook onFetchImage', imageMap);
|
|
226
|
+
// 如果配置了figmaApiKey,则可以预览,传入absoluteHtml
|
|
227
|
+
if (!hasLaunchWebview) {
|
|
228
|
+
// await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml)
|
|
229
|
+
Logger.log(`收到网页工具调用参数:`, JSON.stringify(args, null, 2));
|
|
230
|
+
let configDataString;
|
|
231
|
+
if (isVSCodePluginAvailable) {
|
|
232
|
+
configDataString = await getUserInputFromVSCode({
|
|
233
|
+
fileKey: fileKey,
|
|
234
|
+
nodeId: nodeId,
|
|
235
|
+
mode: update ? 'update' : 'create',
|
|
236
|
+
clientApiSupport,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
configDataString = await getUserInput({
|
|
241
|
+
fileKey: fileKey,
|
|
242
|
+
nodeId: nodeId,
|
|
243
|
+
mode: update ? 'update' : 'create',
|
|
244
|
+
clientApiSupport,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
console.log('收到网页提交数据', configDataString);
|
|
248
|
+
if (!configDataString || configDataString.trim() === '') {
|
|
249
|
+
throw new Error('未提供有效的转码配置');
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
const configData = JSON.parse(configDataString);
|
|
253
|
+
const { mcp, groupsData, confirmedChanges, htmlDiff, ...rest } = configData;
|
|
254
|
+
pluginManager.executeHook('afterSubmitConfigData', {
|
|
255
|
+
configData,
|
|
256
|
+
});
|
|
257
|
+
userConfirmedChanges = confirmedChanges;
|
|
258
|
+
userHtmlDiff = htmlDiff; // 保存 HTML diff
|
|
259
|
+
// 保存旧配置用于比较
|
|
260
|
+
const oldFileConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
|
|
261
|
+
config = {
|
|
262
|
+
...config,
|
|
263
|
+
mcp,
|
|
264
|
+
fileConfigs: {
|
|
265
|
+
...config.fileConfigs,
|
|
266
|
+
[fileKey]: rest,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
mcpServer.setConfig(config.mcp);
|
|
270
|
+
configManager.save(config);
|
|
271
|
+
if (rest) {
|
|
272
|
+
const { promptSetting, ...restConfig } = rest;
|
|
273
|
+
await fileManager.saveConfigSetting(fileKey, nodeId, restConfig);
|
|
274
|
+
Logger.log(`已保存 configSetting 到 fileKey: ${fileKey}, nodeId: ${nodeId}`);
|
|
275
|
+
}
|
|
276
|
+
// 将 groupsData 保存到 fileManager 按 nodeId 存储
|
|
277
|
+
if (groupsData && groupsData.length > 0) {
|
|
278
|
+
await fileManager.saveGroupsData(fileKey, nodeId, groupsData);
|
|
279
|
+
Logger.log(`已保存 groupsData 到 nodeId: ${nodeId}`);
|
|
280
|
+
}
|
|
281
|
+
// 检查是否需要更新imageMap
|
|
282
|
+
try {
|
|
283
|
+
const imageNodeList = d2cNodeList?.filter((node) => ['IMG', 'ICON'].includes(node.type)) || [];
|
|
284
|
+
const { imageMap: updatedImageMap, updated } = await updateImageMapIfNeeded(imageMap, imageNodeList, oldFileConfig, rest, local);
|
|
285
|
+
if (updated) {
|
|
286
|
+
// 更新imageMap并重新保存
|
|
287
|
+
Object.assign(imageMap, updatedImageMap);
|
|
288
|
+
await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
|
|
289
|
+
Logger.log('已更新并保存新的imageMap');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
Logger.log('更新imageMap时出错:', error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Update 模式:直接返回提示词,不需要复杂的采样逻辑
|
|
298
|
+
if (update) {
|
|
299
|
+
// 保存图片文件
|
|
300
|
+
saveImageFile({ imageMap, root });
|
|
301
|
+
Logger.log(`Update 模式: 收到已构建的提示词,直接返回`);
|
|
302
|
+
Logger.log(`Update 模式: 变更点数量 - ${userConfirmedChanges?.length || 0} 个`);
|
|
303
|
+
Logger.log(`Update 模式: HTML Diff 长度 - ${userHtmlDiff?.length || 0} 字符`);
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{
|
|
307
|
+
type: 'text',
|
|
308
|
+
text: buildFullUpdatePromptForAI(userConfirmedChanges, { htmlDiff: userHtmlDiff }),
|
|
309
|
+
},
|
|
310
|
+
],
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
// 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
|
|
314
|
+
const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
|
|
315
|
+
const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
|
|
316
|
+
const convertConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
|
|
317
|
+
// 获取提示词,优先使用用户保存的提示词,否则使用默认提示词
|
|
318
|
+
const frameworkPrompt = savedPromptSetting?.frameworkGuidePrompt || '';
|
|
319
|
+
const chunkPrompt = savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt;
|
|
320
|
+
const aggregationPrompt = savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt;
|
|
321
|
+
const finalPrompt = savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt;
|
|
322
|
+
const noSamplingPrompt = savedPromptSetting?.noSamplingAggregationPrompt || noSamplingAggregationPrompt;
|
|
323
|
+
let codeSnippets = [];
|
|
324
|
+
let isSupportSampling = clientApiSupport.sampling;
|
|
325
|
+
Logger.log(`客户端 API 支持检测: sampling=${isSupportSampling}, roots=${clientApiSupport.roots}`);
|
|
326
|
+
// 收集已映射的组件信息,用于在最终写入时添加提示词
|
|
327
|
+
const componentMappings = [];
|
|
328
|
+
// 收集所有组的组件上下文信息,用于在最终提示词中显示
|
|
329
|
+
const componentContexts = [];
|
|
330
|
+
// 构建采样配置
|
|
331
|
+
const samplingConfig = {
|
|
332
|
+
d2cNodeList,
|
|
333
|
+
imageMap,
|
|
334
|
+
convertConfig,
|
|
335
|
+
chunkPrompt,
|
|
336
|
+
frameworkPrompt,
|
|
337
|
+
mcpServer,
|
|
338
|
+
componentMappings,
|
|
339
|
+
codeSnippets,
|
|
340
|
+
componentContexts,
|
|
341
|
+
isSupportSampling,
|
|
342
|
+
isLocalMode: local,
|
|
343
|
+
};
|
|
344
|
+
// 深度遍历采样:针对每个分组生成 AI 相对布局树并转码
|
|
345
|
+
if (groupsData && groupsData?.length > 0) {
|
|
346
|
+
await processSampling(groupsData, samplingConfig);
|
|
347
|
+
// 采样完成后,更新组件名并保存到文件
|
|
348
|
+
try {
|
|
349
|
+
const groupsDataToSave = groupsData.map((group) => {
|
|
350
|
+
// 将提取的组件名更新到 componentName(仅标记的分组)
|
|
351
|
+
if (group.marked && group.name) {
|
|
352
|
+
group.componentName = group.name;
|
|
353
|
+
Logger.log(`组 ${group.groupIndex} 已标记,更新组件名: ${group.componentName}`);
|
|
354
|
+
}
|
|
355
|
+
// 去掉 nodeList 后返回
|
|
356
|
+
const { nodeList, ...groupWithoutNodeList } = group;
|
|
357
|
+
return groupWithoutNodeList;
|
|
358
|
+
});
|
|
359
|
+
await fileManager.saveGroupsData(fileKey, nodeId, groupsDataToSave);
|
|
360
|
+
Logger.log(`已保存更新后的 groupsData,包含 ${groupsData.filter((g) => g.marked).length} 个标记的组件`);
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
Logger.error(`保存 groupsData 失败:`, error);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
// 分组外元素:注入分组占位并进行整合采样
|
|
367
|
+
// 构建嵌套结构,只收集根组(第一层)的元素
|
|
368
|
+
const nestingInfo = buildNestingStructure(groupsData || []);
|
|
369
|
+
const groupedElementIds = new Set();
|
|
370
|
+
// 只收集根组的元素ID
|
|
371
|
+
nestingInfo.rootGroups.forEach((rootIndex) => {
|
|
372
|
+
const rootGroup = nestingInfo.groupMap.get(rootIndex);
|
|
373
|
+
if (rootGroup) {
|
|
374
|
+
rootGroup.elements.forEach((id) => {
|
|
375
|
+
groupedElementIds.add(id);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
Logger.log(`收集到 ${groupedElementIds.size} 个根组元素ID,根组数量: ${nestingInfo.rootGroups.length}`);
|
|
380
|
+
// 分组外元素
|
|
381
|
+
const ungroupedNodeList = d2cNodeList.filter((node) => !groupedElementIds.has(node.id));
|
|
382
|
+
if (ungroupedNodeList.length > 0) {
|
|
383
|
+
// 只注入根组占位符
|
|
384
|
+
nestingInfo.rootGroups.forEach((rootIndex) => {
|
|
385
|
+
const group = nestingInfo.groupMap.get(rootIndex);
|
|
386
|
+
if (!group)
|
|
387
|
+
return;
|
|
388
|
+
const { x, y, width, height, absoluteRenderX, absoluteRenderY, absoluteRenderWidth, absoluteRenderHeight } = getBoundingBox(group.nodeList);
|
|
389
|
+
ungroupedNodeList.push({
|
|
390
|
+
id: 'G_' + rootIndex,
|
|
391
|
+
name: group.name || `Group${group.groupIndex + 1}`,
|
|
392
|
+
x,
|
|
393
|
+
y,
|
|
394
|
+
width,
|
|
395
|
+
height,
|
|
396
|
+
debuggerInfo: {
|
|
397
|
+
originNode: {
|
|
398
|
+
absoluteBoundingBox: {
|
|
399
|
+
x: absoluteRenderX,
|
|
400
|
+
y: absoluteRenderY,
|
|
401
|
+
width: absoluteRenderWidth,
|
|
402
|
+
height: absoluteRenderHeight,
|
|
403
|
+
},
|
|
404
|
+
absoluteRenderBounds: {
|
|
405
|
+
x: absoluteRenderX,
|
|
406
|
+
y: absoluteRenderY,
|
|
407
|
+
width: absoluteRenderWidth,
|
|
408
|
+
height: absoluteRenderHeight,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
parentId: ungroupedNodeList.find((node) => group.nodeList?.find((groupNode) => groupNode.parentId === node.id))?.id,
|
|
413
|
+
type: 'COMPONENT',
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
console.log('ungroupedNodeList', ungroupedNodeList);
|
|
417
|
+
// 检测占位符元素(分组占位符)
|
|
418
|
+
const placeholders = ungroupedNodeList
|
|
419
|
+
.filter((node) => node.type === 'COMPONENT' && node.id?.startsWith('G_'))
|
|
420
|
+
.map((node) => ({
|
|
421
|
+
name: node.name || 'Unknown',
|
|
422
|
+
}));
|
|
423
|
+
const placeholderPrompt = buildPlaceholderPrompt(placeholders, isSupportSampling);
|
|
424
|
+
Logger.log(`整合采样检测到 ${placeholders.length} 个分组占位符`);
|
|
425
|
+
// 获取代码
|
|
426
|
+
const code = await getCode({
|
|
427
|
+
d2cNodeList: ungroupedNodeList,
|
|
428
|
+
config: convertConfig,
|
|
429
|
+
options: { isLocalMode: local },
|
|
430
|
+
});
|
|
431
|
+
// 使用 imageMap 的 path 替换 code 中对应的 src
|
|
432
|
+
const replacedCode = replaceImageSrc(code, imageMap);
|
|
433
|
+
// 整合采样
|
|
434
|
+
try {
|
|
435
|
+
if (!isSupportSampling)
|
|
436
|
+
throw new Error('不支持采样');
|
|
437
|
+
// 使用提示词(已包含默认值)
|
|
438
|
+
const { content: { text }, } = await mcpServer.server.createMessage({
|
|
439
|
+
messages: [
|
|
440
|
+
{
|
|
441
|
+
role: 'user',
|
|
442
|
+
content: {
|
|
443
|
+
type: 'text',
|
|
444
|
+
text: frameworkPrompt + aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') + placeholderPrompt,
|
|
445
|
+
},
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
role: 'user',
|
|
449
|
+
content: {
|
|
450
|
+
type: 'text',
|
|
451
|
+
text: replacedCode,
|
|
452
|
+
},
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
maxTokens: 48000,
|
|
456
|
+
}, {
|
|
457
|
+
timeout: 5 * 60 * 1000,
|
|
458
|
+
});
|
|
459
|
+
Logger.log('采样成功 (分组外元素)', text, '采样请求代码', replacedCode, '采样提示词', frameworkPrompt + aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || '') + placeholderPrompt);
|
|
460
|
+
const { code: pageCode } = extractCodeAndComponents(text)[0];
|
|
461
|
+
codeSnippets.unshift(pageCode);
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
Logger.log('调用分组外元素采样出错,降级到传统模式', e);
|
|
465
|
+
codeSnippets.unshift(replacedCode);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const onSamplingComplete = await pluginManager.executeHook('onSamplingComplete', {
|
|
469
|
+
codeSnippets,
|
|
470
|
+
imageMap,
|
|
471
|
+
});
|
|
472
|
+
codeSnippets = onSamplingComplete?.codeSnippets || codeSnippets;
|
|
473
|
+
imageMap = onSamplingComplete?.imageMap || imageMap;
|
|
474
|
+
console.log('executeHook onSamplingComplete', imageMap);
|
|
475
|
+
console.log('executeHook onSamplingComplete', codeSnippets);
|
|
476
|
+
// 保存图片文件
|
|
477
|
+
saveImageFile({ imageMap, root });
|
|
478
|
+
Logger.log('处理完成,生成的代码片段数量:', codeSnippets.length);
|
|
479
|
+
Logger.log('已映射的组件数量:', componentMappings.length);
|
|
480
|
+
Logger.log('组件上下文数量:', componentContexts.length);
|
|
481
|
+
// 收集标记的组件(用于提示 LLM 调用 mark_components)
|
|
482
|
+
const markedComponents = [];
|
|
483
|
+
if (groupsData && groupsData.length > 0) {
|
|
484
|
+
groupsData.forEach((group) => {
|
|
485
|
+
if (group.marked && group.screenshot && group.name) {
|
|
486
|
+
markedComponents.push({
|
|
487
|
+
name: group.name,
|
|
488
|
+
signature: group.screenshot,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
Logger.log(`收集到 ${markedComponents.length} 个标记的组件`);
|
|
493
|
+
}
|
|
494
|
+
// 加载完整的组件数据库(用于查找继承链)
|
|
495
|
+
const componentsDatabase = await fileManager.loadComponentsDatabase();
|
|
496
|
+
Logger.log(`加载组件数据库,共 ${componentsDatabase.length} 个组件`);
|
|
497
|
+
// 构建组件映射提示词(包含组件映射、组件上下文、标记组件和完整组件数据库)
|
|
498
|
+
const componentMappingPrompt = buildComponentMappingPrompt(componentMappings, componentContexts, markedComponents, componentsDatabase);
|
|
499
|
+
const { prompt: pluginFinalPrompt = '' } = await pluginManager.executeHook('beforeFinalPrompt', { configData: config.fileConfigs?.[fileKey] || '' });
|
|
500
|
+
console.log('pluginFinalPrompt', pluginFinalPrompt);
|
|
501
|
+
// 使用提示词(已包含默认值)
|
|
502
|
+
return {
|
|
503
|
+
content: [
|
|
504
|
+
isSupportSampling
|
|
505
|
+
? {
|
|
506
|
+
type: 'text',
|
|
507
|
+
text: finalPrompt + componentMappingPrompt + pluginFinalPrompt,
|
|
508
|
+
}
|
|
509
|
+
: {
|
|
510
|
+
type: 'text',
|
|
511
|
+
text: noSamplingPrompt + componentMappingPrompt + pluginFinalPrompt,
|
|
512
|
+
},
|
|
513
|
+
...codeSnippets.map((code) => {
|
|
514
|
+
return {
|
|
515
|
+
type: 'text',
|
|
516
|
+
text: code,
|
|
517
|
+
};
|
|
518
|
+
}),
|
|
519
|
+
].filter(Boolean),
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
Logger.log('网页工具处理过程中发生错误:', error);
|
|
524
|
+
return {
|
|
525
|
+
content: [
|
|
526
|
+
{
|
|
527
|
+
type: 'text',
|
|
528
|
+
text: `处理 Figma 转码时发生错误: ${error instanceof Error ? error.message : String(error)}`,
|
|
529
|
+
},
|
|
530
|
+
],
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
// 标记并保存组件到 components.json
|
|
535
|
+
mcpServer.tool('mark_components', 'Mark and save components to project components.json for future reuse and matching', {
|
|
536
|
+
components: z
|
|
537
|
+
.array(z.object({
|
|
538
|
+
name: z.string().describe('Component name in PascalCase'),
|
|
539
|
+
path: z.string().describe('Component file path relative to project root'),
|
|
540
|
+
signature: z.string().optional().describe('Component Screenshot (SHA256 hash) for matching'),
|
|
541
|
+
type: z.enum(['button', 'input', 'card', 'text', 'image', 'container', 'list', 'custom']).optional(),
|
|
542
|
+
description: z.string().optional(),
|
|
543
|
+
import: z.string().optional().describe('Import statement for the component'),
|
|
544
|
+
props: z
|
|
545
|
+
.array(z.object({
|
|
546
|
+
name: z.string(),
|
|
547
|
+
type: z.string().optional(),
|
|
548
|
+
required: z.boolean().optional(),
|
|
549
|
+
description: z.string().optional(),
|
|
550
|
+
}))
|
|
551
|
+
.optional(),
|
|
552
|
+
super: z.string().optional().describe('Parent class or base component name'),
|
|
553
|
+
functions: z
|
|
554
|
+
.array(z.object({
|
|
555
|
+
name: z.string().describe('Method or function name'),
|
|
556
|
+
params: z
|
|
557
|
+
.array(z.object({
|
|
558
|
+
name: z.string(),
|
|
559
|
+
type: z.string(),
|
|
560
|
+
}))
|
|
561
|
+
.optional()
|
|
562
|
+
.describe('Method parameters'),
|
|
563
|
+
returnType: z.string().optional().describe('Return type of the method'),
|
|
564
|
+
description: z.string().optional(),
|
|
565
|
+
}))
|
|
566
|
+
.optional()
|
|
567
|
+
.describe('Public methods or functions exposed by the component'),
|
|
568
|
+
}))
|
|
569
|
+
.describe('Array of components to mark and save'),
|
|
570
|
+
}, async (args) => {
|
|
571
|
+
try {
|
|
572
|
+
const { components } = args;
|
|
573
|
+
// 1. 设置 workspaceRoot
|
|
574
|
+
let root = './';
|
|
575
|
+
try {
|
|
576
|
+
const rootRes = await mcpServer.server.listRoots();
|
|
577
|
+
Logger.log('获取根目录:', rootRes);
|
|
578
|
+
root = formatListRoots(rootRes);
|
|
579
|
+
Logger.log('标准化根目录:', root);
|
|
580
|
+
// 设置 fileManager 的工作目录根路径
|
|
581
|
+
fileManager.setWorkspaceRoot(root);
|
|
582
|
+
}
|
|
583
|
+
catch (error) {
|
|
584
|
+
Logger.log('获取根目录时出错:', error);
|
|
585
|
+
}
|
|
586
|
+
// 2. 加载现有组件
|
|
587
|
+
const existingComponents = await fileManager.loadComponentsDatabase();
|
|
588
|
+
// 3. 构建 path 索引(用于去重,path 一致则认为是同一组件)
|
|
589
|
+
const pathIndex = new Map();
|
|
590
|
+
existingComponents.forEach((comp, index) => {
|
|
591
|
+
if (comp.path) {
|
|
592
|
+
pathIndex.set(comp.path, index);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
// 4. 处理新组件(合并逻辑)
|
|
596
|
+
const now = new Date().toISOString();
|
|
597
|
+
let addedCount = 0;
|
|
598
|
+
let updatedCount = 0;
|
|
599
|
+
for (const comp of components) {
|
|
600
|
+
// 使用 path 判断组件是否已存在(组件冲突检测)
|
|
601
|
+
const existingIndex = pathIndex.get(comp.path);
|
|
602
|
+
const storedComponent = {
|
|
603
|
+
id: generateComponentId(),
|
|
604
|
+
name: comp.name,
|
|
605
|
+
type: comp.type || 'custom',
|
|
606
|
+
path: comp.path,
|
|
607
|
+
import: comp.import || `import { ${comp.name} } from '${comp.path}'`,
|
|
608
|
+
props: (comp.props || []).map((p) => ({
|
|
609
|
+
name: p.name,
|
|
610
|
+
type: p.type || 'any',
|
|
611
|
+
required: p.required || false,
|
|
612
|
+
description: p.description,
|
|
613
|
+
})),
|
|
614
|
+
functions: (comp.functions || []).map((f) => ({
|
|
615
|
+
name: f.name,
|
|
616
|
+
params: (f.params || []).map((p) => ({ name: p.name, type: p.type })),
|
|
617
|
+
returnType: f.returnType,
|
|
618
|
+
description: f.description,
|
|
619
|
+
})),
|
|
620
|
+
super: comp.super,
|
|
621
|
+
description: comp.description,
|
|
622
|
+
};
|
|
623
|
+
if (comp.signature) {
|
|
624
|
+
storedComponent.signature = comp.signature;
|
|
625
|
+
}
|
|
626
|
+
if (existingIndex !== undefined) {
|
|
627
|
+
// 更新已存在的组件(保留原始导入时间)
|
|
628
|
+
const originalImportedAt = existingComponents[existingIndex].importedAt;
|
|
629
|
+
existingComponents[existingIndex] = {
|
|
630
|
+
...storedComponent,
|
|
631
|
+
importedAt: originalImportedAt,
|
|
632
|
+
};
|
|
633
|
+
updatedCount++;
|
|
634
|
+
}
|
|
635
|
+
else {
|
|
636
|
+
// 添加新组件
|
|
637
|
+
existingComponents.push(storedComponent);
|
|
638
|
+
pathIndex.set(comp.path, existingComponents.length - 1);
|
|
639
|
+
addedCount++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// 5. 保存到文件
|
|
643
|
+
await fileManager.saveComponentsDatabase(existingComponents);
|
|
644
|
+
// 6. 返回结果
|
|
645
|
+
Logger.log(`mark_components: 新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${existingComponents.length} 个组件`);
|
|
646
|
+
return {
|
|
647
|
+
content: [
|
|
648
|
+
{
|
|
649
|
+
type: 'text',
|
|
650
|
+
text: `✅ 组件标记完成:新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${existingComponents.length} 个组件`,
|
|
651
|
+
},
|
|
652
|
+
],
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
Logger.error('mark_components 处理失败:', error);
|
|
657
|
+
return {
|
|
658
|
+
content: [
|
|
659
|
+
{
|
|
660
|
+
type: 'text',
|
|
661
|
+
text: `❌ 标记组件失败: ${error instanceof Error ? error.message : String(error)}`,
|
|
662
|
+
},
|
|
663
|
+
],
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
// 启动 HTTP 服务器
|
|
668
|
+
async function main() {
|
|
669
|
+
// 检查是否是版本命令
|
|
670
|
+
if (process.env.SLOTH_COMMAND === 'version' || process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
671
|
+
try {
|
|
672
|
+
// 读取 package.json 获取版本信息
|
|
673
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
674
|
+
const __dirname = path.dirname(__filename);
|
|
675
|
+
// 在构建后的目录中,需要向上两级才能找到 package.json
|
|
676
|
+
// dist/build/index.js -> ../../package.json
|
|
677
|
+
const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
|
|
678
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
|
|
679
|
+
const packageJson = JSON.parse(packageJsonContent);
|
|
680
|
+
console.log(`${packageJson.name} v${packageJson.version}`);
|
|
681
|
+
process.exit(0);
|
|
682
|
+
}
|
|
683
|
+
catch (error) {
|
|
684
|
+
console.error('无法读取版本信息:', error);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
// 检查是否是帮助命令
|
|
689
|
+
if (process.env.SLOTH_COMMAND === 'help') {
|
|
690
|
+
console.log('Sloth D2C MCP Server - Figma 设计转代码工具');
|
|
691
|
+
console.log('');
|
|
692
|
+
console.log('使用方法: sloth <命令> [选项]');
|
|
693
|
+
console.log('');
|
|
694
|
+
console.log('可用命令:');
|
|
695
|
+
console.log(' cache 显示缓存目录路径');
|
|
696
|
+
console.log(' config 显示配置文件路径');
|
|
697
|
+
console.log(' server start 后台启动服务');
|
|
698
|
+
console.log(' server stop 停止服务');
|
|
699
|
+
console.log(' update 更新 sloth');
|
|
700
|
+
console.log(' version 显示版本信息');
|
|
701
|
+
console.log('');
|
|
702
|
+
console.log('全局选项:');
|
|
703
|
+
console.log(' --help, -h 显示帮助信息');
|
|
704
|
+
console.log(' --version, -v 显示版本信息');
|
|
705
|
+
console.log('');
|
|
706
|
+
console.log('示例:');
|
|
707
|
+
console.log(' sloth cache # 显示缓存目录路径');
|
|
708
|
+
console.log(' sloth config # 显示配置文件路径');
|
|
709
|
+
console.log(' sloth server start # 后台启动服务');
|
|
710
|
+
console.log(' sloth server stop # 停止服务');
|
|
711
|
+
console.log(' sloth update # 更新 sloth');
|
|
712
|
+
console.log(' sloth version # 显示版本信息');
|
|
713
|
+
process.exit(0);
|
|
714
|
+
}
|
|
715
|
+
// 检查是否是 config 命令
|
|
716
|
+
if (process.env.SLOTH_COMMAND === 'config') {
|
|
717
|
+
const configManager = new ConfigManager('d2c-mcp');
|
|
718
|
+
const configPath = configManager.getConfigPath();
|
|
719
|
+
// 获取子命令参数
|
|
720
|
+
const configArgsStr = process.env.SLOTH_CONFIG_ARGS || '[]';
|
|
721
|
+
const configArgs = JSON.parse(configArgsStr);
|
|
722
|
+
const isJsonOutput = configArgs.includes('--json');
|
|
723
|
+
// 处理帮助参数
|
|
724
|
+
if (configArgs.includes('--help') || configArgs.includes('-h')) {
|
|
725
|
+
console.log('使用方法: sloth config [选项]');
|
|
726
|
+
console.log('');
|
|
727
|
+
console.log('显示配置文件信息');
|
|
728
|
+
console.log('');
|
|
729
|
+
console.log('选项:');
|
|
730
|
+
console.log(' --help, -h 显示帮助信息');
|
|
731
|
+
console.log(' --path 仅显示配置文件路径');
|
|
732
|
+
console.log(' --json 以 JSON 格式输出');
|
|
733
|
+
process.exit(0);
|
|
734
|
+
}
|
|
735
|
+
// 处理只显示路径的参数
|
|
736
|
+
if (configArgs.includes('--path')) {
|
|
737
|
+
if (isJsonOutput) {
|
|
738
|
+
console.log(JSON.stringify({ path: configPath }));
|
|
739
|
+
}
|
|
740
|
+
else {
|
|
741
|
+
console.log(configPath);
|
|
742
|
+
}
|
|
743
|
+
process.exit(0);
|
|
744
|
+
}
|
|
745
|
+
// 检查配置文件是否存在
|
|
746
|
+
const exists = await configManager.exists();
|
|
747
|
+
let config = null;
|
|
748
|
+
let error = null;
|
|
749
|
+
if (exists) {
|
|
750
|
+
try {
|
|
751
|
+
config = await configManager.load();
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
error = err instanceof Error ? err.message : String(err);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (isJsonOutput) {
|
|
758
|
+
const result = {
|
|
759
|
+
path: configPath,
|
|
760
|
+
exists: exists,
|
|
761
|
+
config: config,
|
|
762
|
+
error: error,
|
|
763
|
+
};
|
|
764
|
+
console.log(JSON.stringify(result, null, 2));
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
console.log(`配置文件路径: ${configPath}`);
|
|
768
|
+
if (exists) {
|
|
769
|
+
console.log(`配置文件存在`);
|
|
770
|
+
if (config) {
|
|
771
|
+
console.log(`当前配置内容:`);
|
|
772
|
+
console.log(JSON.stringify(config, null, 2));
|
|
773
|
+
}
|
|
774
|
+
else if (error) {
|
|
775
|
+
console.log(`读取配置文件失败: ${error}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
console.log(`配置文件不存在`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
process.exit(0);
|
|
783
|
+
}
|
|
784
|
+
// 检查是否是 cache 命令
|
|
785
|
+
if (process.env.SLOTH_COMMAND === 'cache') {
|
|
786
|
+
console.log(fileManager.getBaseDir());
|
|
787
|
+
process.exit(0);
|
|
788
|
+
}
|
|
789
|
+
if (process.env.SLOTH_COMMAND === 'server') {
|
|
790
|
+
await startListening();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
// 先输出基本启动信息到控制台,确保即使 VSCode 日志失败也能看到启动过程
|
|
795
|
+
console.log(`[${new Date().toISOString()}] Starting Figma transcoding interceptor MCP server...`);
|
|
796
|
+
// 注意:插件管理器的初始化已移到 d2c_figma 工具中
|
|
797
|
+
// 因为需要先获取工作区根目录才能加载工作区插件
|
|
798
|
+
// Create config manager instance to pass to server
|
|
799
|
+
await startListening(true);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
// 确保错误信息能够输出,即使 VSCode 日志服务不可用
|
|
803
|
+
const timestamp = new Date().toISOString();
|
|
804
|
+
console.error(`[${timestamp}] Failed to start server:`, error);
|
|
805
|
+
// 尝试使用 Logger 输出错误,但不依赖它
|
|
806
|
+
try {
|
|
807
|
+
Logger.error('Failed to start server:', error);
|
|
808
|
+
}
|
|
809
|
+
catch (logError) {
|
|
810
|
+
console.error(`[${timestamp}] Logger also failed:`, logError);
|
|
811
|
+
}
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
// 处理进程退出时的清理
|
|
816
|
+
process.on('SIGINT', async () => {
|
|
817
|
+
Logger.log('MCP: 收到SIGINT信号');
|
|
818
|
+
const timestamp = new Date().toISOString();
|
|
819
|
+
console.log(`[${timestamp}] 正在关闭服务器...`);
|
|
820
|
+
try {
|
|
821
|
+
Logger.log('正在关闭服务器...');
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
// 忽略日志错误,确保清理过程继续
|
|
825
|
+
}
|
|
826
|
+
cleanupTauri();
|
|
827
|
+
cleanupWeb();
|
|
828
|
+
cleanupVSCode();
|
|
829
|
+
clearCapabilitiesCache(); // 清理能力检测缓存
|
|
830
|
+
Logger.cleanup(); // 清理日志连接
|
|
831
|
+
await stopHttpServer();
|
|
832
|
+
process.exit(0);
|
|
833
|
+
});
|
|
834
|
+
process.on('SIGTERM', async () => {
|
|
835
|
+
Logger.log('MCP: 收到SIGTERM信号');
|
|
836
|
+
const timestamp = new Date().toISOString();
|
|
837
|
+
console.log(`[${timestamp}] 正在关闭服务器...`);
|
|
838
|
+
try {
|
|
839
|
+
Logger.log('正在关闭服务器...');
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
// 忽略日志错误,确保清理过程继续
|
|
843
|
+
}
|
|
844
|
+
cleanupTauri();
|
|
845
|
+
cleanupWeb();
|
|
846
|
+
cleanupVSCode();
|
|
847
|
+
clearCapabilitiesCache(); // 清理能力检测缓存
|
|
848
|
+
Logger.cleanup(); // 清理日志连接
|
|
849
|
+
await stopHttpServer();
|
|
850
|
+
process.exit(0);
|
|
851
|
+
});
|
|
852
|
+
main();
|