sloth-d2c-mcp 1.0.4-beta65

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.
@@ -0,0 +1,839 @@
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, } 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 } 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, resetNodeListPosition, saveImageFile, replaceImageSrc } from './utils/utils.js';
15
+ // @ts-ignore
16
+ import * as flatted from 'flatted';
17
+ import { promises as fs } from 'fs';
18
+ import * as path from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { extractCodeAndComponents } from './utils/extract.js';
21
+ export class D2CMcpServer extends McpServer {
22
+ figmaApiKey = '';
23
+ baseURL = '';
24
+ constructor(serverInfo, options) {
25
+ super(serverInfo, options);
26
+ }
27
+ setConfig(config) {
28
+ this.figmaApiKey = config?.figmaApiKey || '';
29
+ this.baseURL = config?.baseURL || '';
30
+ console.log('setConfig', this.figmaApiKey, this.baseURL);
31
+ axios.defaults.baseURL = this.baseURL;
32
+ }
33
+ }
34
+ // 创建 MCP 服务器实例
35
+ const mcpServer = new D2CMcpServer({
36
+ name: 'transcoding-interceptor-server',
37
+ version: '1.0.0',
38
+ }, {
39
+ capabilities: {
40
+ resources: {},
41
+ tools: {},
42
+ },
43
+ });
44
+ const configManager = new ConfigManager('d2c-mcp');
45
+ const fileManager = new FileManager('d2c-mcp');
46
+ // 注册 Figma 转码拦截工具
47
+ mcpServer.tool('d2c_figma', 'Convert Figma Design File to Code', {
48
+ fileKey: z.string().describe('The key of the Figma file to fetch, often found in a provided URL like figma.com/(file|design)/<fileKey>/...'),
49
+ nodeId: z.string().optional().describe('The ID of the node to fetch, often found as URL parameter node-id=<nodeId>, always use if provided'),
50
+ depth: z.number().optional().describe('How many levels deep to traverse the node tree, only use if explicitly requested by the user'),
51
+ local: z.boolean().optional().describe('Whether to use local data cache, default is false'),
52
+ }, async (args) => {
53
+ try {
54
+ Logger.log(`收到工具调用参数:`, JSON.stringify(args, null, 2));
55
+ const { fileKey, nodeId, depth, local } = args;
56
+ let config = await configManager.load();
57
+ let hasLaunchWebview = false;
58
+ // 检测 VSCode 扩展是否可用
59
+ const isVSCodePluginAvailable = await isVSCodeAvailable();
60
+ Logger.log(`VSCode 扩展检测结果: ${isVSCodePluginAvailable ? '可用' : '不可用'}`);
61
+ // 没有配置figmaApiKey,无法预览,直接唤起配置页面
62
+ if (!config.mcp?.figmaApiKey) {
63
+ hasLaunchWebview = true;
64
+ let configDataString;
65
+ if (isVSCodePluginAvailable) {
66
+ // 优先使用 VSCode 扩展
67
+ Logger.log('使用 VSCode 扩展获取用户输入');
68
+ configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId });
69
+ }
70
+ else {
71
+ // 降级到网页浏览器
72
+ Logger.log('VSCode 扩展不可用,降级到网页浏览器');
73
+ configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId });
74
+ }
75
+ if (!configDataString || configDataString.trim() === '') {
76
+ throw new Error('未提供有效的转码配置');
77
+ }
78
+ else {
79
+ const configData = JSON.parse(configDataString);
80
+ const { mcp, groupsData, ...rest } = configData;
81
+ config = {
82
+ ...config,
83
+ mcp,
84
+ fileConfigs: {
85
+ ...config.fileConfigs,
86
+ [fileKey]: rest,
87
+ },
88
+ };
89
+ mcpServer.setConfig(config.mcp);
90
+ configManager.save(config);
91
+ // 将 groupsData 保存到 fileManager 按 nodeId 存储
92
+ if (groupsData && groupsData.length > 0) {
93
+ await fileManager.saveGroupsData(fileKey, nodeId, groupsData);
94
+ Logger.log(`已保存 groupsData 到 nodeId: ${nodeId}`);
95
+ }
96
+ }
97
+ }
98
+ // 调用 get_ai_group_layout
99
+ Logger.log(`Fetching AI group layout for ${nodeId ? `node ${nodeId} from file` : `full file`} ${fileKey}`);
100
+ let d2cNodeList, imageMap, absoluteHtml;
101
+ if (!local) {
102
+ const { d2cNodeList: nodes, imageMap: imgMap } = await convertFigmaToD2CNode({
103
+ fileKey,
104
+ nodeId,
105
+ depth,
106
+ config: config.fileConfigs?.[fileKey] || defaultConfigData,
107
+ figmaToken: mcpServer.figmaApiKey,
108
+ });
109
+ d2cNodeList = nodes;
110
+ imageMap = imgMap;
111
+ absoluteHtml = await generateAbsoluteHtml(d2cNodeList);
112
+ // 保存nodeList
113
+ await fileManager.saveFile(fileKey, nodeId, 'nodeList.json', flatted.stringify(d2cNodeList));
114
+ await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
115
+ // 保存figma绝对定位代码
116
+ await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml);
117
+ }
118
+ else {
119
+ d2cNodeList = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'nodeList.json'));
120
+ imageMap = flatted.parse(await fileManager.loadFile(fileKey, nodeId, 'imageMap.json'));
121
+ absoluteHtml = await fileManager.loadFile(fileKey, nodeId, 'absolute.html');
122
+ console.log('cache load data');
123
+ }
124
+ console.log('d2cNodeList', d2cNodeList);
125
+ console.log('imageMap', imageMap);
126
+ console.log('absoluteHtml', absoluteHtml);
127
+ // 如果配置了figmaApiKey,则可以预览,传入absoluteHtml
128
+ if (!hasLaunchWebview) {
129
+ // await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml)
130
+ Logger.log(`收到网页工具调用参数:`, JSON.stringify(args, null, 2));
131
+ let configDataString;
132
+ if (isVSCodePluginAvailable) {
133
+ configDataString = await getUserInputFromVSCode({ fileKey: fileKey, nodeId: nodeId });
134
+ }
135
+ else {
136
+ configDataString = await getUserInput({ fileKey: fileKey, nodeId: nodeId });
137
+ }
138
+ if (!configDataString || configDataString.trim() === '') {
139
+ throw new Error('未提供有效的转码配置');
140
+ }
141
+ else {
142
+ const configData = JSON.parse(configDataString);
143
+ const { mcp, groupsData, ...rest } = configData;
144
+ // 保存旧配置用于比较
145
+ const oldFileConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
146
+ config = {
147
+ ...config,
148
+ mcp,
149
+ fileConfigs: {
150
+ ...config.fileConfigs,
151
+ [fileKey]: rest,
152
+ },
153
+ };
154
+ mcpServer.setConfig(config.mcp);
155
+ configManager.save(config);
156
+ // 将 groupsData 保存到 fileManager 按 nodeId 存储
157
+ if (groupsData && groupsData.length > 0) {
158
+ await fileManager.saveGroupsData(fileKey, nodeId, groupsData);
159
+ Logger.log(`已保存 groupsData 到 nodeId: ${nodeId}`);
160
+ }
161
+ // 检查是否需要更新imageMap
162
+ try {
163
+ const imageNodeList = d2cNodeList?.filter((node) => ['IMG', 'ICON'].includes(node.type)) || [];
164
+ const { imageMap: updatedImageMap, updated } = await updateImageMapIfNeeded(imageMap, imageNodeList, oldFileConfig, rest);
165
+ if (updated) {
166
+ // 更新imageMap并重新保存
167
+ Object.assign(imageMap, updatedImageMap);
168
+ await fileManager.saveFile(fileKey, nodeId, 'imageMap.json', flatted.stringify(imageMap));
169
+ Logger.log('已更新并保存新的imageMap');
170
+ }
171
+ }
172
+ catch (error) {
173
+ Logger.log('更新imageMap时出错:', error);
174
+ }
175
+ }
176
+ }
177
+ // 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
178
+ const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
179
+ const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
180
+ const convertConfig = config.fileConfigs?.[fileKey] || defaultConfigData;
181
+ // 获取提示词,优先使用用户保存的提示词,否则使用默认提示词
182
+ const chunkPrompt = savedPromptSetting?.chunkOptimizePrompt || chunkOptimizeCodePrompt;
183
+ const aggregationPrompt = savedPromptSetting?.aggregationOptimizePrompt || aggregationOptimizeCodePrompt;
184
+ const finalPrompt = savedPromptSetting?.finalOptimizePrompt || finalOptimizeCodePrompt;
185
+ let root = './';
186
+ try {
187
+ const rootRes = await mcpServer.server.listRoots();
188
+ Logger.log('获取根目录:', rootRes);
189
+ root = rootRes.roots[0]?.uri?.slice(7) || './';
190
+ }
191
+ catch (error) {
192
+ Logger.log('获取根目录时出错:', error);
193
+ }
194
+ const codeSnippets = [];
195
+ let isSupportSampling = true;
196
+ // 并发采样:针对每个分组生成 AI 相对布局树并转码
197
+ if (groupsData && groupsData?.length > 0) {
198
+ // 限制并发为 2,分批执行,多个采样并发可能存在超时问题
199
+ const concurrency = 1;
200
+ for (let i = 0; i < groupsData.length; i += concurrency) {
201
+ const batch = groupsData.slice(i, i + concurrency);
202
+ const samplingPromises = batch.map(async (group) => {
203
+ const groupElements = group.elements;
204
+ let nodeList = d2cNodeList.filter((node) => groupElements.includes(node.id));
205
+ group.nodeList = nodeList;
206
+ // 重置节点位置
207
+ nodeList = resetNodeListPosition(nodeList);
208
+ // 获取代码
209
+ const code = await getCode({
210
+ d2cNodeList: nodeList,
211
+ config: convertConfig,
212
+ });
213
+ console.log('code', code);
214
+ // 使用 imageMap 的 path 替换 code 中对应的 src
215
+ let replacedCode = replaceImageSrc(code, imageMap);
216
+ const componentName = `Group${group.groupIndex + 1}`;
217
+ const codeWithCustomName = replacedCode.replace(/\bApp\b/g, componentName);
218
+ try {
219
+ const { content: { text }, } = await mcpServer.server.createMessage({
220
+ messages: [
221
+ {
222
+ role: 'user',
223
+ content: {
224
+ type: 'text',
225
+ text: chunkPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || ''),
226
+ },
227
+ },
228
+ {
229
+ role: 'user',
230
+ content: {
231
+ type: 'text',
232
+ text: codeWithCustomName + (group.userPrompt ? '\n以下是用户针对该代码的优化提出的补充说明和要求,请在优化代码时特别关注这些指导并尽可能实现:\n' + group.userPrompt : ''),
233
+ },
234
+ },
235
+ ],
236
+ maxTokens: 48000,
237
+ }, { timeout: 5 * 60 * 1000 });
238
+ Logger.log('采样成功', text);
239
+ const { code: chunkCode, componentName } = extractCodeAndComponents(text)[0];
240
+ Logger.log('采样解析成功', chunkCode, componentName);
241
+ group.name = componentName;
242
+ codeSnippets.push(chunkCode);
243
+ }
244
+ catch (e) {
245
+ Logger.log('调用采样出错', e);
246
+ codeSnippets.push(codeWithCustomName + (group.userPrompt ? '\n以下是用户针对该代码的优化提出的补充说明和要求,请在优化代码时特别关注这些指导并尽可能实现:\n' + group.userPrompt : ''));
247
+ }
248
+ });
249
+ await Promise.all(samplingPromises);
250
+ }
251
+ }
252
+ // 分组外元素:注入分组占位并进行整合采样
253
+ const groupedElementIds = new Set();
254
+ groupsData?.forEach((group) => {
255
+ group.elements.forEach((id) => {
256
+ groupedElementIds.add(id);
257
+ });
258
+ });
259
+ // 分组外元素
260
+ const ungroupedNodeList = d2cNodeList.filter((node) => !groupedElementIds.has(node.id));
261
+ if (ungroupedNodeList.length > 0) {
262
+ // 注入分组占位
263
+ groupsData?.forEach((group, index) => {
264
+ const { x, y, width, height, absoluteRenderX, absoluteRenderY, absoluteRenderWidth, absoluteRenderHeight } = getBoundingBox(group.nodeList);
265
+ ungroupedNodeList.push({
266
+ id: 'G_' + index,
267
+ name: group.name || `Group${group.groupIndex + 1}`,
268
+ x,
269
+ y,
270
+ width,
271
+ height,
272
+ debuggerInfo: {
273
+ originNode: {
274
+ absoluteBoundingBox: {
275
+ x: absoluteRenderX,
276
+ y: absoluteRenderY,
277
+ width: absoluteRenderWidth,
278
+ height: absoluteRenderHeight,
279
+ },
280
+ absoluteRenderBounds: {
281
+ x: absoluteRenderX,
282
+ y: absoluteRenderY,
283
+ width: absoluteRenderWidth,
284
+ height: absoluteRenderHeight,
285
+ },
286
+ },
287
+ },
288
+ parentId: ungroupedNodeList.find((node) => group.nodeList?.find((groupNode) => groupNode.parentId === node.id))?.id,
289
+ type: 'COMPONENT',
290
+ });
291
+ });
292
+ console.log('ungroupedNodeList', ungroupedNodeList);
293
+ // 获取代码
294
+ const code = await getCode({
295
+ d2cNodeList: ungroupedNodeList,
296
+ config: convertConfig,
297
+ });
298
+ console.log('code', code);
299
+ // 使用 imageMap 的 path 替换 code 中对应的 src
300
+ const replacedCode = replaceImageSrc(code, imageMap);
301
+ // 整合采样
302
+ try {
303
+ // 使用提示词(已包含默认值)
304
+ const { content: { text }, } = await mcpServer.server.createMessage({
305
+ messages: [
306
+ {
307
+ role: 'user',
308
+ content: {
309
+ type: 'text',
310
+ text: aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || ''),
311
+ },
312
+ },
313
+ {
314
+ role: 'user',
315
+ content: {
316
+ type: 'text',
317
+ text: replacedCode,
318
+ },
319
+ },
320
+ ],
321
+ maxTokens: 48000,
322
+ }, {
323
+ timeout: 5 * 60 * 1000,
324
+ });
325
+ Logger.log('采样成功 (分组外元素)', text);
326
+ const { code: pageCode } = extractCodeAndComponents(text)[0];
327
+ codeSnippets.unshift(pageCode);
328
+ }
329
+ catch (e) {
330
+ isSupportSampling = false;
331
+ Logger.log('调用分组外元素采样出错,降级到传统模式', e);
332
+ codeSnippets.unshift(replacedCode);
333
+ }
334
+ }
335
+ // 保存图片文件
336
+ saveImageFile({ imageMap, root });
337
+ Logger.log('处理完成,生成的代码片段数量:', codeSnippets.length);
338
+ // 使用提示词(已包含默认值)
339
+ return {
340
+ content: [
341
+ isSupportSampling
342
+ ? {
343
+ type: 'text',
344
+ text: finalPrompt,
345
+ }
346
+ : {
347
+ type: 'text',
348
+ text: aggregationPrompt.replace(/{FRAMEWORK}/g, convertConfig.convertSetting?.framework || ''),
349
+ },
350
+ ...codeSnippets.map((code) => {
351
+ return {
352
+ type: 'text',
353
+ text: code,
354
+ };
355
+ }),
356
+ ].filter(Boolean),
357
+ };
358
+ }
359
+ catch (error) {
360
+ Logger.log('网页工具处理过程中发生错误:', error);
361
+ return {
362
+ content: [
363
+ {
364
+ type: 'text',
365
+ text: `处理 Figma 转码时发生错误: ${error instanceof Error ? error.message : String(error)}`,
366
+ },
367
+ ],
368
+ };
369
+ }
370
+ });
371
+ // 启动 HTTP 服务器
372
+ async function main() {
373
+ // 检查是否是版本命令
374
+ if (process.env.SLOTH_COMMAND === 'version' || process.argv.includes('--version') || process.argv.includes('-v')) {
375
+ try {
376
+ // 读取 package.json 获取版本信息
377
+ const __filename = fileURLToPath(import.meta.url);
378
+ const __dirname = path.dirname(__filename);
379
+ // 在构建后的目录中,需要向上两级才能找到 package.json
380
+ // dist/build/index.js -> ../../package.json
381
+ const packageJsonPath = path.join(__dirname, '..', '..', 'package.json');
382
+ const packageJsonContent = await fs.readFile(packageJsonPath, 'utf8');
383
+ const packageJson = JSON.parse(packageJsonContent);
384
+ console.log(`${packageJson.name} v${packageJson.version}`);
385
+ process.exit(0);
386
+ }
387
+ catch (error) {
388
+ console.error('无法读取版本信息:', error);
389
+ process.exit(1);
390
+ }
391
+ }
392
+ // 检查是否是帮助命令
393
+ if (process.env.SLOTH_COMMAND === 'help') {
394
+ console.log('Sloth D2C MCP Server - Figma 设计转代码工具');
395
+ console.log('');
396
+ console.log('使用方法: sloth <命令> [选项]');
397
+ console.log('');
398
+ console.log('可用命令:');
399
+ console.log(' config 显示配置文件信息');
400
+ console.log(' page 显示 interceptor-web 页面地址');
401
+ console.log(' version 显示版本信息');
402
+ console.log('');
403
+ console.log('全局选项:');
404
+ console.log(' --help, -h 显示帮助信息');
405
+ console.log(' --version, -v 显示版本信息');
406
+ console.log('');
407
+ console.log('使用 "sloth <命令> --help" 获取特定命令的帮助信息');
408
+ console.log('');
409
+ console.log('示例:');
410
+ console.log(' sloth config # 显示配置信息');
411
+ console.log(' sloth config --path # 仅显示配置路径');
412
+ console.log(' sloth page # 显示页面地址');
413
+ console.log(' sloth page --list # 列出所有页面文件');
414
+ console.log(' sloth version # 显示版本信息');
415
+ console.log(' sloth --version # 显示版本信息');
416
+ process.exit(0);
417
+ }
418
+ // 检查是否是 config 命令
419
+ if (process.env.SLOTH_COMMAND === 'config') {
420
+ const configManager = new ConfigManager('d2c-mcp');
421
+ const configPath = configManager.getConfigPath();
422
+ // 获取子命令参数
423
+ const configArgsStr = process.env.SLOTH_CONFIG_ARGS || '[]';
424
+ const configArgs = JSON.parse(configArgsStr);
425
+ const isJsonOutput = configArgs.includes('--json');
426
+ // 处理帮助参数
427
+ if (configArgs.includes('--help') || configArgs.includes('-h')) {
428
+ console.log('使用方法: sloth config [选项]');
429
+ console.log('');
430
+ console.log('显示配置文件信息');
431
+ console.log('');
432
+ console.log('选项:');
433
+ console.log(' --help, -h 显示帮助信息');
434
+ console.log(' --path 仅显示配置文件路径');
435
+ console.log(' --json 以 JSON 格式输出');
436
+ process.exit(0);
437
+ }
438
+ // 处理只显示路径的参数
439
+ if (configArgs.includes('--path')) {
440
+ if (isJsonOutput) {
441
+ console.log(JSON.stringify({ path: configPath }));
442
+ }
443
+ else {
444
+ console.log(configPath);
445
+ }
446
+ process.exit(0);
447
+ }
448
+ // 检查配置文件是否存在
449
+ const exists = await configManager.exists();
450
+ let config = null;
451
+ let error = null;
452
+ if (exists) {
453
+ try {
454
+ config = await configManager.load();
455
+ }
456
+ catch (err) {
457
+ error = err.toString();
458
+ }
459
+ }
460
+ if (isJsonOutput) {
461
+ const result = {
462
+ path: configPath,
463
+ exists: exists,
464
+ config: config,
465
+ error: error,
466
+ };
467
+ console.log(JSON.stringify(result, null, 2));
468
+ }
469
+ else {
470
+ console.log(`配置文件路径: ${configPath}`);
471
+ if (exists) {
472
+ console.log(`配置文件存在`);
473
+ if (config) {
474
+ console.log(`当前配置内容:`);
475
+ console.log(JSON.stringify(config, null, 2));
476
+ }
477
+ else if (error) {
478
+ console.log(`读取配置文件失败: ${error}`);
479
+ }
480
+ }
481
+ else {
482
+ console.log(`配置文件不存在`);
483
+ }
484
+ }
485
+ process.exit(0);
486
+ }
487
+ // 检查是否是 page 命令
488
+ if (process.env.SLOTH_COMMAND === 'page') {
489
+ // 获取子命令参数
490
+ const pageArgsStr = process.env.SLOTH_PAGE_ARGS || '[]';
491
+ const pageArgs = JSON.parse(pageArgsStr);
492
+ const isJsonOutput = pageArgs.includes('--json');
493
+ // 处理帮助参数
494
+ if (pageArgs.includes('--help') || pageArgs.includes('-h')) {
495
+ console.log('使用方法: sloth page [选项]');
496
+ console.log('');
497
+ console.log('显示安装包目录下 interceptor-web 页面的地址');
498
+ console.log('');
499
+ console.log('选项:');
500
+ console.log(' --help, -h 显示帮助信息');
501
+ console.log(' --list 列出所有可用的页面文件');
502
+ console.log(' --json 以 JSON 格式输出');
503
+ process.exit(0);
504
+ }
505
+ try {
506
+ // 获取当前模块的目录
507
+ const __filename = fileURLToPath(import.meta.url);
508
+ const __dirname = path.dirname(__filename);
509
+ // 构建 interceptor-web 目录路径
510
+ // 在开发环境中路径是: dist/build/index.js -> ../interceptor-web/dist
511
+ // 在全局安装后路径是: node_modules/sloth-d2c-mcp/dist/build/index.js -> ../../interceptor-web/dist
512
+ let interceptorWebPath = path.join(__dirname, '..', 'interceptor-web', 'dist');
513
+ Logger.log('interceptorWebPath', interceptorWebPath);
514
+ // 检查目录是否存在,如果不存在可能是全局安装的情况
515
+ let pathFound = false;
516
+ try {
517
+ await fs.access(interceptorWebPath);
518
+ pathFound = true;
519
+ }
520
+ catch {
521
+ // 尝试全局安装的路径
522
+ interceptorWebPath = path.join(__dirname, '..', '..', 'interceptor-web', 'dist');
523
+ try {
524
+ await fs.access(interceptorWebPath);
525
+ pathFound = true;
526
+ }
527
+ catch {
528
+ // 路径都不存在
529
+ }
530
+ }
531
+ if (!pathFound) {
532
+ const errorInfo = {
533
+ error: 'Interceptor-web 目录不存在',
534
+ attemptedPaths: [path.join(__dirname, '..', 'interceptor-web', 'dist'), path.join(__dirname, '..', '..', 'interceptor-web', 'dist')],
535
+ suggestions: ['包未正确构建', 'interceptor-web 目录结构发生变化', '安装方式不同导致路径差异'],
536
+ };
537
+ if (isJsonOutput) {
538
+ console.log(JSON.stringify(errorInfo, null, 2));
539
+ }
540
+ else {
541
+ console.log(`Interceptor-web 目录不存在:`);
542
+ console.log(` 尝试路径1: ${errorInfo.attemptedPaths[0]}`);
543
+ console.log(` 尝试路径2: ${errorInfo.attemptedPaths[1]}`);
544
+ console.log('');
545
+ console.log('可能的原因:');
546
+ errorInfo.suggestions.forEach((suggestion, index) => {
547
+ console.log(` ${index + 1}. ${suggestion}`);
548
+ });
549
+ }
550
+ process.exit(1);
551
+ }
552
+ // 如果是 --list 参数,列出所有文件
553
+ if (pageArgs.includes('--list')) {
554
+ try {
555
+ const files = await fs.readdir(interceptorWebPath);
556
+ const fileList = [];
557
+ for (const file of files) {
558
+ const filePath = path.join(interceptorWebPath, file);
559
+ const stat = await fs.stat(filePath);
560
+ if (stat.isFile()) {
561
+ fileList.push({
562
+ name: file,
563
+ path: filePath,
564
+ });
565
+ }
566
+ }
567
+ if (isJsonOutput) {
568
+ console.log(JSON.stringify({
569
+ directory: interceptorWebPath,
570
+ files: fileList,
571
+ }, null, 2));
572
+ }
573
+ else {
574
+ console.log(`Interceptor-web 目录路径: ${interceptorWebPath}`);
575
+ console.log('可用的页面文件:');
576
+ fileList.forEach((file) => {
577
+ console.log(` ${file.name} -> ${file.path}`);
578
+ });
579
+ }
580
+ }
581
+ catch (error) {
582
+ const errorInfo = {
583
+ error: '读取目录失败',
584
+ message: error.toString(),
585
+ directory: interceptorWebPath,
586
+ };
587
+ if (isJsonOutput) {
588
+ console.log(JSON.stringify(errorInfo, null, 2));
589
+ }
590
+ else {
591
+ console.log(`读取目录失败: ${error}`);
592
+ }
593
+ process.exit(1);
594
+ }
595
+ }
596
+ else {
597
+ // 默认显示主要页面
598
+ const indexPath = path.join(interceptorWebPath, 'index.html');
599
+ const detailPath = path.join(interceptorWebPath, 'detail.html');
600
+ const pages = {
601
+ directory: interceptorWebPath,
602
+ pages: {
603
+ index: {
604
+ path: indexPath,
605
+ exists: false,
606
+ },
607
+ detail: {
608
+ path: detailPath,
609
+ exists: false,
610
+ },
611
+ },
612
+ };
613
+ // 检查 index.html
614
+ try {
615
+ await fs.access(indexPath);
616
+ pages.pages.index.exists = true;
617
+ }
618
+ catch {
619
+ // 文件不存在
620
+ }
621
+ // 检查 detail.html
622
+ try {
623
+ await fs.access(detailPath);
624
+ pages.pages.detail.exists = true;
625
+ }
626
+ catch {
627
+ // 文件不存在
628
+ }
629
+ if (isJsonOutput) {
630
+ console.log(JSON.stringify(pages, null, 2));
631
+ }
632
+ else {
633
+ console.log(`Interceptor-web 目录路径: ${interceptorWebPath}`);
634
+ console.log('主要页面文件:');
635
+ console.log(` 主页面: ${indexPath}${pages.pages.index.exists ? '' : ' (不存在)'}`);
636
+ console.log(` 详情页: ${detailPath}${pages.pages.detail.exists ? '' : ' (不存在)'}`);
637
+ }
638
+ }
639
+ }
640
+ catch (error) {
641
+ const errorInfo = {
642
+ error: '获取页面信息失败',
643
+ message: error.toString(),
644
+ };
645
+ if (isJsonOutput) {
646
+ console.log(JSON.stringify(errorInfo, null, 2));
647
+ }
648
+ else {
649
+ console.log(`获取页面信息失败: ${error}`);
650
+ }
651
+ process.exit(1);
652
+ }
653
+ process.exit(0);
654
+ }
655
+ if (process.env.SLOTH_COMMAND === 'clear') {
656
+ fileManager.cleanup();
657
+ process.exit(0);
658
+ }
659
+ try {
660
+ // 先输出基本启动信息到控制台,确保即使 VSCode 日志失败也能看到启动过程
661
+ console.log(`[${new Date().toISOString()}] Starting Figma transcoding interceptor MCP server...`);
662
+ const port = await getAvailablePort();
663
+ // Create config manager instance to pass to server
664
+ const configManager = new ConfigManager('d2c-mcp');
665
+ const isStdioMode = process.env.NODE_ENV === 'cli' || process.argv.includes('--stdio');
666
+ if (isStdioMode) {
667
+ Logger.log(`Stdio模式启动`);
668
+ await loadConfig(mcpServer, configManager);
669
+ const transport = new StdioServerTransport();
670
+ await mcpServer.connect(transport);
671
+ // 启动 HTTP 服务器 - 仅启用web服务
672
+ await startHttpServer(port, mcpServer, configManager, false);
673
+ }
674
+ else {
675
+ // 启动 HTTP 服务器 - 这是核心功能,不依赖 VSCode 日志
676
+ await startHttpServer(port, mcpServer, configManager, true);
677
+ }
678
+ // 服务器启动成功后再尝试使用 Logger(包含 VSCode 日志)
679
+ Logger.log(`Server started successfully on port ${port}!`);
680
+ }
681
+ catch (error) {
682
+ // 确保错误信息能够输出,即使 VSCode 日志服务不可用
683
+ const timestamp = new Date().toISOString();
684
+ console.error(`[${timestamp}] Failed to start server:`, error);
685
+ // 尝试使用 Logger 输出错误,但不依赖它
686
+ try {
687
+ Logger.error('Failed to start server:', error);
688
+ }
689
+ catch (logError) {
690
+ console.error(`[${timestamp}] Logger also failed:`, logError);
691
+ }
692
+ process.exit(1);
693
+ }
694
+ }
695
+ // 处理进程退出时的清理
696
+ process.on('SIGINT', async () => {
697
+ Logger.log('MCP: 收到SIGINT信号');
698
+ const timestamp = new Date().toISOString();
699
+ console.log(`[${timestamp}] 正在关闭服务器...`);
700
+ try {
701
+ Logger.log('正在关闭服务器...');
702
+ }
703
+ catch (error) {
704
+ // 忽略日志错误,确保清理过程继续
705
+ }
706
+ cleanupTauri();
707
+ cleanupWeb();
708
+ cleanupVSCode();
709
+ Logger.cleanup(); // 清理日志连接
710
+ await stopHttpServer();
711
+ process.exit(0);
712
+ });
713
+ process.on('SIGTERM', async () => {
714
+ Logger.log('MCP: 收到SIGTERM信号');
715
+ const timestamp = new Date().toISOString();
716
+ console.log(`[${timestamp}] 正在关闭服务器...`);
717
+ try {
718
+ Logger.log('正在关闭服务器...');
719
+ }
720
+ catch (error) {
721
+ // 忽略日志错误,确保清理过程继续
722
+ }
723
+ cleanupTauri();
724
+ cleanupWeb();
725
+ cleanupVSCode();
726
+ Logger.cleanup(); // 清理日志连接
727
+ await stopHttpServer();
728
+ process.exit(0);
729
+ });
730
+ main();
731
+ // 获取命令行参数
732
+ const args = process.argv.slice(2);
733
+ // 处理 --log 参数:开启文件日志(同时保留控制台输出)
734
+ const logArgIndex = args.findIndex((a) => a === '--log' || a.startsWith('--log='));
735
+ if (logArgIndex !== -1) {
736
+ let logFilePath = 'runtime.log';
737
+ const raw = args[logArgIndex];
738
+ // 移除 --log 参数,避免传递给后续程序
739
+ args.splice(logArgIndex, 1);
740
+ process.argv = [process.argv[0], process.argv[1], ...args];
741
+ try {
742
+ const { createWriteStream, mkdirSync, existsSync } = await import('node:fs');
743
+ const path = await import('node:path');
744
+ const util = await import('node:util');
745
+ if (raw.startsWith('--log=')) {
746
+ const provided = raw.slice('--log='.length).trim();
747
+ if (provided) {
748
+ logFilePath = path.isAbsolute(provided) ? provided : path.resolve(process.cwd(), provided);
749
+ }
750
+ }
751
+ else {
752
+ logFilePath = path.resolve(process.cwd(), logFilePath);
753
+ }
754
+ const logDir = path.dirname(logFilePath);
755
+ if (!existsSync(logDir)) {
756
+ mkdirSync(logDir, { recursive: true });
757
+ }
758
+ const logStream = createWriteStream(logFilePath, { flags: 'a' });
759
+ const originalConsole = {
760
+ log: console.log,
761
+ info: console.info,
762
+ warn: console.warn,
763
+ error: console.error,
764
+ };
765
+ const writeLine = (...args) => {
766
+ try {
767
+ const line = util.format(...args) + '\n';
768
+ logStream.write(line);
769
+ }
770
+ catch { }
771
+ };
772
+ console.log = (...a) => {
773
+ originalConsole.log(...a);
774
+ writeLine(...a);
775
+ };
776
+ console.info = (...a) => {
777
+ originalConsole.info(...a);
778
+ writeLine(...a);
779
+ };
780
+ console.warn = (...a) => {
781
+ originalConsole.warn(...a);
782
+ writeLine(...a);
783
+ };
784
+ console.error = (...a) => {
785
+ originalConsole.error(...a);
786
+ writeLine(...a);
787
+ };
788
+ process.env.SLOTH_LOG_FILE = logFilePath;
789
+ originalConsole.log(`[sloth] 日志已开启,写入: ${logFilePath}`);
790
+ const close = () => {
791
+ try {
792
+ logStream.end();
793
+ }
794
+ catch { }
795
+ };
796
+ process.on('exit', close);
797
+ process.on('SIGINT', close);
798
+ process.on('SIGTERM', close);
799
+ process.on('uncaughtException', close);
800
+ process.on('unhandledRejection', close);
801
+ }
802
+ catch (e) {
803
+ // 记录但不中断启动
804
+ console.warn('[sloth] 启用文件日志失败:', e);
805
+ }
806
+ }
807
+ // 检查是否是 config 命令
808
+ if (args[0] === 'config') {
809
+ // 设置环境变量标识这是 config 命令
810
+ process.env.SLOTH_COMMAND = 'config';
811
+ // 传递子命令参数
812
+ process.env.SLOTH_CONFIG_ARGS = JSON.stringify(args.slice(1));
813
+ }
814
+ else if (args[0] === 'page') {
815
+ // 设置环境变量标识这是 page 命令
816
+ process.env.SLOTH_COMMAND = 'page';
817
+ // 传递子命令参数
818
+ process.env.SLOTH_PAGE_ARGS = JSON.stringify(args.slice(1));
819
+ }
820
+ else if (args[0] === 'version' || args[0] === '--version' || args[0] === '-v') {
821
+ // 设置环境变量标识这是 version 命令
822
+ process.env.SLOTH_COMMAND = 'version';
823
+ }
824
+ else if (args[0] === '--help' || args[0] === '-h' || args.length === 0) {
825
+ // 显示主帮助信息
826
+ process.env.SLOTH_COMMAND = 'help';
827
+ }
828
+ else if (args[0] === 'clear') {
829
+ // 设置环境变量标识这是 clear 命令
830
+ process.env.SLOTH_COMMAND = 'clear';
831
+ }
832
+ else {
833
+ // 设置环境变量为CLI模式
834
+ process.env.NODE_ENV = 'cli';
835
+ // 确保--stdio参数被传递
836
+ if (!process.argv.includes('--stdio')) {
837
+ process.argv.push('--stdio');
838
+ }
839
+ }