sloth-d2c-mcp 1.0.4-beta77 → 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.
@@ -434,7 +434,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
434
434
  app.get('/getHtml', async (req, res) => {
435
435
  // 加载现有配置
436
436
  // const fileManager = new FileManager('d2c-mcp')
437
- const html = await fileManager.loadFile(req.query.fileKey, req.query.nodeId, 'absolute.html');
437
+ const html = await fileManager.loadAbsoluteHtml(req.query.fileKey, req.query.nodeId);
438
438
  res.json({
439
439
  success: true,
440
440
  data: html,
@@ -682,7 +682,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
682
682
  }
683
683
  // 保存绝对布局HTML
684
684
  if (absoluteHtml) {
685
- await fileManager.saveFile(fileKey, nodeId, 'absolute.html', absoluteHtml);
685
+ await fileManager.saveAbsoluteHtml(fileKey, nodeId, absoluteHtml);
686
686
  }
687
687
  Logger.log(`成功保存节点数据: fileKey=${fileKey}, nodeId=${nodeId}`);
688
688
  res.json({
@@ -979,7 +979,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
979
979
  },
980
980
  },
981
981
  ].filter(Boolean),
982
- maxTokens: 8000,
982
+ maxTokens: 48000,
983
983
  }, { timeout: 2 * 60 * 1000 });
984
984
  // 解析 AI 返回的 JSON
985
985
  const extractResult = extractJson(text);
@@ -1299,6 +1299,334 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
1299
1299
  });
1300
1300
  }
1301
1301
  });
1302
+ // 列出设计稿快照接口(用于 update 模式选择旧设计稿)
1303
+ app.get('/listDesignSnapshots', async (req, res) => {
1304
+ try {
1305
+ Logger.log('获取设计稿快照列表');
1306
+ const workspaceRoot = getProjectRoot();
1307
+ Logger.log('workspaceRoot:', workspaceRoot);
1308
+ if (!workspaceRoot) {
1309
+ res.json({
1310
+ success: true,
1311
+ snapshots: [],
1312
+ });
1313
+ return;
1314
+ }
1315
+ const slothDir = path.join(workspaceRoot, '.sloth');
1316
+ const snapshots = [];
1317
+ try {
1318
+ const fileKeys = await fs.promises.readdir(slothDir);
1319
+ for (const fileKey of fileKeys) {
1320
+ // 跳过特殊文件
1321
+ if (fileKey === '.' || fileKey === 'components.json' || fileKey.startsWith('.'))
1322
+ continue;
1323
+ const fileKeyDir = path.join(slothDir, fileKey);
1324
+ const stat = await fs.promises.stat(fileKeyDir);
1325
+ if (!stat.isDirectory())
1326
+ continue;
1327
+ const nodeIds = await fs.promises.readdir(fileKeyDir);
1328
+ for (const nodeId of nodeIds) {
1329
+ const nodeDir = path.join(fileKeyDir, nodeId);
1330
+ const nodeStat = await fs.promises.stat(nodeDir);
1331
+ if (!nodeStat.isDirectory())
1332
+ continue;
1333
+ // 查找截图并转换为 base64
1334
+ let screenshotBase64 = '';
1335
+ const screenshotsDir = path.join(nodeDir, 'screenshots');
1336
+ try {
1337
+ const indexScreenshotPath = path.join(screenshotsDir, 'index.png');
1338
+ await fs.promises.access(indexScreenshotPath);
1339
+ // 读取截图文件并转换为 base64
1340
+ const imageBuffer = await fs.promises.readFile(indexScreenshotPath);
1341
+ screenshotBase64 = `data:image/png;base64,${imageBuffer.toString('base64')}`;
1342
+ }
1343
+ catch {
1344
+ // 没有 index.png,尝试找其他截图
1345
+ try {
1346
+ const screenshots = await fs.promises.readdir(screenshotsDir);
1347
+ if (screenshots.length > 0) {
1348
+ const firstScreenshot = screenshots[0];
1349
+ const screenshotPath = path.join(screenshotsDir, firstScreenshot);
1350
+ const imageBuffer = await fs.promises.readFile(screenshotPath);
1351
+ screenshotBase64 = `data:image/png;base64,${imageBuffer.toString('base64')}`;
1352
+ }
1353
+ }
1354
+ catch {
1355
+ // 没有截图目录
1356
+ }
1357
+ }
1358
+ // 加载 groupsData 获取名称和分组数量
1359
+ let name = `${fileKey}/${nodeId}`;
1360
+ snapshots.push({
1361
+ fileKey,
1362
+ nodeId,
1363
+ screenshotBase64,
1364
+ timestamp: nodeStat.mtime.toISOString(),
1365
+ name,
1366
+ });
1367
+ }
1368
+ }
1369
+ // 按时间倒序排列
1370
+ snapshots.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1371
+ Logger.log(`找到 ${snapshots.length} 个设计稿快照`);
1372
+ res.json({
1373
+ success: true,
1374
+ snapshots,
1375
+ });
1376
+ }
1377
+ catch (error) {
1378
+ Logger.error('扫描设计稿快照失败:', error);
1379
+ res.json({
1380
+ success: true,
1381
+ snapshots: [],
1382
+ });
1383
+ }
1384
+ }
1385
+ catch (error) {
1386
+ Logger.error('获取设计稿快照列表失败:', error);
1387
+ res.status(500).json({
1388
+ success: false,
1389
+ message: '获取设计稿快照列表失败',
1390
+ error: error instanceof Error ? error.message : String(error),
1391
+ });
1392
+ }
1393
+ });
1394
+ // 分析设计稿变更接口(用于 update 模式)
1395
+ app.use('/analyzeChange', express.json());
1396
+ app.post('/analyzeChange', async (req, res) => {
1397
+ try {
1398
+ const { oldFileKey, oldNodeId, newFileKey, newNodeId } = req.body;
1399
+ if (!oldFileKey || !oldNodeId || !newFileKey || !newNodeId) {
1400
+ res.status(400).json({
1401
+ success: false,
1402
+ message: '缺少必要参数: oldFileKey, oldNodeId, newFileKey, newNodeId',
1403
+ });
1404
+ return;
1405
+ }
1406
+ Logger.log(`分析设计稿变更: old=${oldFileKey}/${oldNodeId}, new=${newFileKey}/${newNodeId}`);
1407
+ // 1. 加载新旧 HTML 和 imageMap
1408
+ let oldHtml = await fileManager.loadAbsoluteHtml(oldFileKey, oldNodeId);
1409
+ let newHtml = await fileManager.loadAbsoluteHtml(newFileKey, newNodeId);
1410
+ Logger.log(`[analyzeChange] 加载 HTML 完成: oldHtml=${oldHtml?.length || 0}字符, newHtml=${newHtml?.length || 0}字符`);
1411
+ // 加载 imageMap 用于替换图片路径(flatted 格式)
1412
+ let oldImageMap = {};
1413
+ let newImageMap = {};
1414
+ try {
1415
+ const oldImageMapStr = await fileManager.loadFile(oldFileKey, oldNodeId, 'imageMap.json');
1416
+ if (oldImageMapStr) {
1417
+ oldImageMap = flatted.parse(oldImageMapStr);
1418
+ Logger.log(`[analyzeChange] 加载旧 imageMap 成功, 条目数: ${Object.keys(oldImageMap).length}`);
1419
+ }
1420
+ else {
1421
+ Logger.log(`[analyzeChange] 旧 imageMap 为空`);
1422
+ }
1423
+ }
1424
+ catch (e) {
1425
+ Logger.log(`[analyzeChange] 加载旧 imageMap 失败: ${e}`);
1426
+ }
1427
+ try {
1428
+ const newImageMapStr = await fileManager.loadFile(newFileKey, newNodeId, 'imageMap.json');
1429
+ if (newImageMapStr) {
1430
+ newImageMap = flatted.parse(newImageMapStr);
1431
+ Logger.log(`[analyzeChange] 加载新 imageMap 成功, 条目数: ${Object.keys(newImageMap).length}`);
1432
+ }
1433
+ else {
1434
+ Logger.log(`[analyzeChange] 新 imageMap 为空`);
1435
+ }
1436
+ }
1437
+ catch (e) {
1438
+ Logger.log(`[analyzeChange] 加载新 imageMap 失败: ${e}`);
1439
+ }
1440
+ // 构建 base64 到 path 的映射表
1441
+ const buildBase64ToPathMap = (imageMap, label) => {
1442
+ const map = new Map();
1443
+ for (const [key, value] of Object.entries(imageMap)) {
1444
+ if (value?.base64 && value?.path) {
1445
+ map.set(value.base64, value.path);
1446
+ Logger.log(`[analyzeChange] ${label} 映射: key=${key}, path=${value.path}, base64长度=${value.base64?.length || 0}`);
1447
+ }
1448
+ }
1449
+ Logger.log(`[analyzeChange] ${label} 构建映射表完成, 有效映射数: ${map.size}`);
1450
+ return map;
1451
+ };
1452
+ // 处理 HTML 用于 diff 比较
1453
+ const processHtmlForDiff = (html, imageMap, label) => {
1454
+ // 构建 base64 到 path 的映射
1455
+ const base64ToPath = buildBase64ToPathMap(imageMap, label);
1456
+ // 替换图片 base64 为 path
1457
+ let processedHtml = html;
1458
+ let replaceCount = 0;
1459
+ for (const [base64, imgPath] of base64ToPath) {
1460
+ // 转义特殊字符用于正则匹配
1461
+ const escapedBase64 = base64.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1462
+ const beforeLength = processedHtml.length;
1463
+ processedHtml = processedHtml.replace(new RegExp(escapedBase64, 'g'), imgPath);
1464
+ if (processedHtml.length !== beforeLength) {
1465
+ replaceCount++;
1466
+ Logger.log(`[analyzeChange] ${label} 替换成功: ${imgPath}, 长度变化: ${beforeLength} -> ${processedHtml.length}`);
1467
+ }
1468
+ }
1469
+ Logger.log(`[analyzeChange] ${label} 图片替换完成, 替换数: ${replaceCount}, HTML长度: ${html.length} -> ${processedHtml.length}`);
1470
+ return processedHtml
1471
+ // 去除 data-id、data-name、data-type 属性(不参与 diff)
1472
+ .replace(/\s*data-id="[^"]*"/g, '')
1473
+ .replace(/\s*data-name="[^"]*"/g, '')
1474
+ .replace(/\s*data-type="[^"]*"/g, '')
1475
+ // 去除 z-index 相关的 Tailwind 类名
1476
+ .replace(/\bz-\[\d+\]/g, '')
1477
+ // 清理 class 中可能产生的多余空格
1478
+ .replace(/class="([^"]*)"/g, (_, classes) => {
1479
+ const cleaned = classes.replace(/\s+/g, ' ').trim();
1480
+ return `class="${cleaned}"`;
1481
+ });
1482
+ };
1483
+ oldHtml = processHtmlForDiff(oldHtml, oldImageMap, 'old');
1484
+ newHtml = processHtmlForDiff(newHtml, newImageMap, 'new');
1485
+ Logger.log(`[analyzeChange] HTML 处理完成: oldHtml=${oldHtml?.length || 0}字符, newHtml=${newHtml?.length || 0}字符`);
1486
+ if (!oldHtml) {
1487
+ res.status(404).json({
1488
+ success: false,
1489
+ message: '旧设计稿 HTML 不存在',
1490
+ });
1491
+ return;
1492
+ }
1493
+ if (!newHtml) {
1494
+ res.status(404).json({
1495
+ success: false,
1496
+ message: '新设计稿 HTML 不存在',
1497
+ });
1498
+ return;
1499
+ }
1500
+ // 2. 加载新旧 groupsData
1501
+ const oldGroupsData = (await fileManager.loadGroupsData(oldFileKey, oldNodeId)) || [];
1502
+ const newGroupsData = (await fileManager.loadGroupsData(newFileKey, newNodeId)) || [];
1503
+ // 3. 执行 HTML 差异分析
1504
+ // @ts-ignore
1505
+ const { diffLines } = await import('diff');
1506
+ const diffResult = diffLines(oldHtml, newHtml);
1507
+ // 统计变更并生成 diff 文本
1508
+ let addedLines = 0;
1509
+ let removedLines = 0;
1510
+ let unchangedLines = 0;
1511
+ const diffParts = [];
1512
+ for (const part of diffResult) {
1513
+ const lines = part.value.split('\n').filter((l) => l.trim()).length;
1514
+ if (part.added) {
1515
+ addedLines += lines;
1516
+ // 标记新增内容
1517
+ diffParts.push(`+++ ${part.value.trim()}`);
1518
+ }
1519
+ else if (part.removed) {
1520
+ removedLines += lines;
1521
+ // 标记删除内容
1522
+ diffParts.push(`--- ${part.value.trim()}`);
1523
+ }
1524
+ else {
1525
+ unchangedLines += lines;
1526
+ // 不输出未变更的内容,减少 token
1527
+ }
1528
+ }
1529
+ // 生成精简的 diff 文本(只包含变更部分)
1530
+ const diffText = diffParts.join('\n');
1531
+ // 4. 生成变更摘要
1532
+ const changeSummary = {
1533
+ totalChanges: addedLines + removedLines,
1534
+ addedLines,
1535
+ removedLines,
1536
+ unchangedLines,
1537
+ changeRatio: ((addedLines + removedLines) / (addedLines + removedLines + unchangedLines) * 100).toFixed(1),
1538
+ };
1539
+ // 5. 尝试使用 AI 总结变更(如果 MCP 服务器可用)
1540
+ let aiSummary = [];
1541
+ if (mcpServer && changeSummary.totalChanges > 0) {
1542
+ try {
1543
+ // 限制 diff 文本长度,避免 token 过大
1544
+ const maxDiffLength = 48000;
1545
+ const truncatedDiff = diffText.length > maxDiffLength
1546
+ ? diffText.substring(0, maxDiffLength) + '\n... (diff 内容已截断)'
1547
+ : diffText;
1548
+ const prompt = `
1549
+ 你是一个专业的前端开发专家。请分析以下设计稿 HTML 的 diff 变更,总结出具体的变更点。
1550
+
1551
+ ## Diff 变更内容
1552
+ \`\`\`diff
1553
+ ${truncatedDiff}
1554
+ \`\`\`
1555
+
1556
+ 说明:
1557
+ - \`---\` 开头的行表示被删除的内容
1558
+ - \`+++\` 开头的行表示新增的内容
1559
+ - HTML 使用 Tailwind CSS 类名
1560
+
1561
+ 请按以下 JSON 格式输出变更点列表:
1562
+ \`\`\`json
1563
+ [
1564
+ {
1565
+ "id": "change_1",
1566
+ "type": "layout|style|content|structure",
1567
+ "title": "变更标题(简短)",
1568
+ "description": "详细描述变更内容",
1569
+ "suggestedAction": "建议的代码修改方向"
1570
+ }
1571
+ ]
1572
+ \`\`\`
1573
+
1574
+ 请确保:
1575
+ 1. 每个变更点都是独立的、可操作的
1576
+ 2. 描述要具体,包含位置、样式、尺寸等信息
1577
+ 3. 建议的操作要明确
1578
+ `;
1579
+ Logger.log('=== AI 分析变更提示词 ===');
1580
+ Logger.log(prompt);
1581
+ Logger.log('=== 提示词结束 ===');
1582
+ const { content: { text } } = await mcpServer.server.createMessage({
1583
+ messages: [{
1584
+ role: 'user',
1585
+ content: { type: 'text', text: prompt }
1586
+ }],
1587
+ maxTokens: 48000
1588
+ }, { timeout: 2 * 60 * 1000 });
1589
+ // 解析 AI 返回的 JSON
1590
+ const jsonMatch = text.match(/```json\s*([\s\S]*?)\s*```/);
1591
+ if (jsonMatch) {
1592
+ aiSummary = JSON.parse(jsonMatch[1]);
1593
+ }
1594
+ }
1595
+ catch (aiError) {
1596
+ Logger.warn('AI 分析变更失败,使用基础分析:', aiError);
1597
+ }
1598
+ }
1599
+ // 6. 如果 AI 分析失败,生成基础摘要
1600
+ if (aiSummary.length === 0 && changeSummary.totalChanges > 0) {
1601
+ aiSummary = [{
1602
+ id: 'change_1',
1603
+ type: 'structure',
1604
+ title: '设计稿结构变更',
1605
+ description: `检测到 ${addedLines} 行新增内容和 ${removedLines} 行删除内容,变更比例约 ${changeSummary.changeRatio}%`,
1606
+ suggestedAction: '请检查新设计稿的布局和样式变化,更新相应的代码'
1607
+ }];
1608
+ }
1609
+ Logger.log(`变更分析完成: ${aiSummary.length} 个变更点`);
1610
+ res.json({
1611
+ success: true,
1612
+ data: {
1613
+ changeSummary,
1614
+ aiSummary,
1615
+ oldGroupsData,
1616
+ newGroupsData,
1617
+ diffText, // 返回 HTML diff 文本,用于 AI 代码修改
1618
+ },
1619
+ });
1620
+ }
1621
+ catch (error) {
1622
+ Logger.error('分析设计稿变更失败:', error);
1623
+ res.status(500).json({
1624
+ success: false,
1625
+ message: '分析设计稿变更失败',
1626
+ error: error instanceof Error ? error.message : String(error),
1627
+ });
1628
+ }
1629
+ });
1302
1630
  // 启动 HTTP 服务器,监听端口
1303
1631
  httpServer = app.listen(port, async (err) => {
1304
1632
  if (err) {
@@ -1359,7 +1687,7 @@ export async function getUserInput(payload) {
1359
1687
  return new Promise(async (resolve, reject) => {
1360
1688
  const token = uuidv4();
1361
1689
  const port = getPort();
1362
- const authUrl = `http://localhost:${port}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}`;
1690
+ const authUrl = `http://localhost:${port}/auth-page?token=${token}&fileKey=${payload.fileKey}&nodeId=${payload.nodeId}&mode=${payload.mode}`;
1363
1691
  Logger.log('authUrl', authUrl);
1364
1692
  // 判断是主进程还是子进程
1365
1693
  const isMainProcess = httpServer !== null;
@@ -34,9 +34,9 @@ export class FileManager {
34
34
  */
35
35
  getFilePath(fileKey, nodeId, filename) {
36
36
  // 清理文件名中的特殊字符
37
- const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
38
- const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_:]/g, '_') : 'root';
39
- const cleanFilename = filename.replace(/[^a-zA-Z0-9-_.]/g, '_');
37
+ const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_');
38
+ const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_:]/g, '_') : 'root';
39
+ const cleanFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_.]/g, '_');
40
40
  return path.join(this.baseDir, cleanFileKey, cleanNodeId, cleanFilename);
41
41
  }
42
42
  /**
@@ -53,9 +53,9 @@ export class FileManager {
53
53
  return this.getFilePath(fileKey, nodeId, filename);
54
54
  }
55
55
  // 清理文件名中的特殊字符
56
- const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
57
- const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_:]/g, '_') : 'root';
58
- const cleanFilename = filename.replace(/[^a-zA-Z0-9-_.]/g, '_');
56
+ const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_');
57
+ const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_:]/g, '_') : 'root';
58
+ const cleanFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_.]/g, '_');
59
59
  if (skipParsePath) {
60
60
  return path.join(this.workspaceRoot, '.sloth', fileKey, nodeId || 'root', cleanFilename);
61
61
  }
@@ -667,5 +667,127 @@ export class FileManager {
667
667
  };
668
668
  return searchDir(slothDir);
669
669
  }
670
+ /**
671
+ * 保存 absolute.html 到 .sloth 目录,支持版本管理
672
+ * 如果内容不一致,将当前版本移动到带时间戳的文件夹中
673
+ * @param fileKey - Figma文件的key
674
+ * @param nodeId - 节点ID(可选)
675
+ * @param content - 文件内容
676
+ */
677
+ async saveAbsoluteHtml(fileKey, nodeId, content) {
678
+ const filename = 'absolute.html';
679
+ // 先尝试加载现有文件
680
+ const existingContent = await this.loadFile(fileKey, nodeId, filename, { useWorkspaceDir: false });
681
+ // 如果存在且内容不一致,先归档旧版本
682
+ if (existingContent && existingContent.trim() !== content.trim()) {
683
+ await this.archiveAbsoluteHtml(fileKey, nodeId, existingContent);
684
+ Logger.log(`检测到 absolute.html 内容变化,已归档旧版本`);
685
+ }
686
+ // 保存新内容
687
+ await this.saveFile(fileKey, nodeId, filename, content, { useWorkspaceDir: false });
688
+ }
689
+ /**
690
+ * 归档 absolute.html 到带时间戳的文件夹
691
+ * @param fileKey - Figma文件的key
692
+ * @param nodeId - 节点ID(可选)
693
+ * @param content - 要归档的内容
694
+ */
695
+ async archiveAbsoluteHtml(fileKey, nodeId, content) {
696
+ if (!this.workspaceRoot) {
697
+ Logger.warn('工作目录根路径未设置,无法归档');
698
+ return;
699
+ }
700
+ // 生成时间戳文件夹名:YYYYMMDD_HHmmss
701
+ const now = new Date();
702
+ const timestamp = now.getFullYear().toString() +
703
+ (now.getMonth() + 1).toString().padStart(2, '0') +
704
+ now.getDate().toString().padStart(2, '0') + '_' +
705
+ now.getHours().toString().padStart(2, '0') +
706
+ now.getMinutes().toString().padStart(2, '0') +
707
+ now.getSeconds().toString().padStart(2, '0');
708
+ const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
709
+ const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_:]/g, '_') : 'root';
710
+ // 归档路径:.sloth/{fileKey}/{nodeId}/history/{timestamp}/absolute.html
711
+ const archiveDir = path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, 'history', timestamp);
712
+ try {
713
+ await fs.mkdir(archiveDir, { recursive: true });
714
+ const archivePath = path.join(archiveDir, 'absolute.html');
715
+ await fs.writeFile(archivePath, content, 'utf-8');
716
+ // 同时复制 index.png 截图(如果存在)
717
+ const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
718
+ const indexScreenshotPath = path.join(screenshotsDir, 'index.png');
719
+ try {
720
+ await fs.access(indexScreenshotPath);
721
+ const archiveScreenshotPath = path.join(archiveDir, 'index.png');
722
+ await fs.copyFile(indexScreenshotPath, archiveScreenshotPath);
723
+ Logger.log(`截图已归档: ${archiveScreenshotPath}`);
724
+ }
725
+ catch {
726
+ // 截图不存在,跳过
727
+ }
728
+ Logger.log(`absolute.html 已归档到: ${archivePath}`);
729
+ }
730
+ catch (error) {
731
+ Logger.error(`归档 absolute.html 失败: ${error}`);
732
+ }
733
+ }
734
+ /**
735
+ * 加载 absolute.html
736
+ * @param fileKey - Figma文件的key
737
+ * @param nodeId - 节点ID(可选)
738
+ * @returns Promise<string> - 文件内容
739
+ */
740
+ async loadAbsoluteHtml(fileKey, nodeId) {
741
+ return this.loadFile(fileKey, nodeId, 'absolute.html', { useWorkspaceDir: false });
742
+ }
743
+ /**
744
+ * 列出 absolute.html 的所有历史版本
745
+ * @param fileKey - Figma文件的key
746
+ * @param nodeId - 节点ID(可选)
747
+ * @returns Promise<Array<{ timestamp: string; path: string }>> - 历史版本列表
748
+ */
749
+ async listAbsoluteHtmlHistory(fileKey, nodeId) {
750
+ if (!this.workspaceRoot) {
751
+ return [];
752
+ }
753
+ const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
754
+ const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_:]/g, '_') : 'root';
755
+ const historyDir = path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, 'history');
756
+ try {
757
+ const entries = await fs.readdir(historyDir, { withFileTypes: true });
758
+ const versions = [];
759
+ for (const entry of entries) {
760
+ if (entry.isDirectory()) {
761
+ const htmlPath = path.join(historyDir, entry.name, 'absolute.html');
762
+ const screenshotPath = path.join(historyDir, entry.name, 'index.png');
763
+ try {
764
+ await fs.access(htmlPath);
765
+ let screenshot;
766
+ try {
767
+ await fs.access(screenshotPath);
768
+ screenshot = screenshotPath;
769
+ }
770
+ catch {
771
+ // 截图不存在
772
+ }
773
+ versions.push({
774
+ timestamp: entry.name,
775
+ path: htmlPath,
776
+ screenshotPath: screenshot,
777
+ });
778
+ }
779
+ catch {
780
+ // html 文件不存在,跳过
781
+ }
782
+ }
783
+ }
784
+ // 按时间戳倒序排列(最新的在前)
785
+ versions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
786
+ return versions;
787
+ }
788
+ catch {
789
+ return [];
790
+ }
791
+ }
670
792
  }
671
793
  export default FileManager;
@@ -1,18 +1,18 @@
1
1
  {
2
- "buildTime": "2025-12-10T09:06:44.786Z",
2
+ "buildTime": "2025-12-21T09:28:39.194Z",
3
3
  "mode": "build",
4
4
  "pages": {
5
5
  "main": {
6
6
  "file": "index.html",
7
- "size": 1604168,
8
- "sizeFormatted": "1.53 MB"
7
+ "size": 1628632,
8
+ "sizeFormatted": "1.55 MB"
9
9
  },
10
10
  "detail": {
11
11
  "file": "detail.html",
12
- "size": 280964,
13
- "sizeFormatted": "274.38 KB"
12
+ "size": 281671,
13
+ "sizeFormatted": "275.07 KB"
14
14
  }
15
15
  },
16
- "totalSize": 1885132,
17
- "totalSizeFormatted": "1.8 MB"
16
+ "totalSize": 1910303,
17
+ "totalSizeFormatted": "1.82 MB"
18
18
  }