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,868 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import envPaths from 'env-paths';
|
|
4
|
+
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { httpServer, socketServer } from '../server.js';
|
|
6
|
+
/**
|
|
7
|
+
* 通用文件管理器
|
|
8
|
+
* 可以保存任何类型的文件,使用fileKey和nodeId组织目录结构
|
|
9
|
+
*/
|
|
10
|
+
export class FileManager {
|
|
11
|
+
paths; // 应用路径配置
|
|
12
|
+
baseDir; // 基础存储目录
|
|
13
|
+
_workspaceRoot = null; // MCP 工作目录根路径
|
|
14
|
+
_uuid = '';
|
|
15
|
+
constructor(appName) {
|
|
16
|
+
this.paths = envPaths(appName);
|
|
17
|
+
this.baseDir = path.join(this.paths.data, 'files');
|
|
18
|
+
}
|
|
19
|
+
get workspaceRoot() {
|
|
20
|
+
const isMainProcess = httpServer !== null;
|
|
21
|
+
if (isMainProcess && this._uuid) {
|
|
22
|
+
const workspaceRoot = socketServer?.getTokenExtraField(this._uuid, 'workspaceRoot');
|
|
23
|
+
return workspaceRoot || this._workspaceRoot;
|
|
24
|
+
}
|
|
25
|
+
return this._workspaceRoot;
|
|
26
|
+
}
|
|
27
|
+
set workspaceRoot(rootPath) {
|
|
28
|
+
this._workspaceRoot = rootPath;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* 设置工作目录根路径(用于 MCP 工作项目)
|
|
32
|
+
* @param rootPath - 工作目录根路径
|
|
33
|
+
*/
|
|
34
|
+
setWorkspaceRoot(rootPath) {
|
|
35
|
+
this.workspaceRoot = rootPath;
|
|
36
|
+
Logger.log(`已设置工作目录根路径: ${rootPath}`);
|
|
37
|
+
}
|
|
38
|
+
getWorkspaceRoot() {
|
|
39
|
+
return this.workspaceRoot || './';
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 生成文件路径
|
|
43
|
+
* @param fileKey - Figma文件的key
|
|
44
|
+
* @param nodeId - 节点ID(可选)
|
|
45
|
+
* @param filename - 文件名
|
|
46
|
+
* @returns 完整的文件路径
|
|
47
|
+
*/
|
|
48
|
+
getFilePath(fileKey, nodeId, filename) {
|
|
49
|
+
// 清理文件名中的特殊字符
|
|
50
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_');
|
|
51
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_') : 'root';
|
|
52
|
+
const cleanFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_.]/g, '_');
|
|
53
|
+
return path.join(this.baseDir, cleanFileKey, cleanNodeId, cleanFilename);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* 生成工作目录中的文件路径(保存到 .sloth 文件夹)
|
|
57
|
+
* @param fileKey - Figma文件的key
|
|
58
|
+
* @param nodeId - 节点ID(可选)
|
|
59
|
+
* @param filename - 文件名
|
|
60
|
+
* @returns 完整的文件路径
|
|
61
|
+
*/
|
|
62
|
+
getWorkspaceFilePath(fileKey, nodeId, filename, opts = { skipParsePath: false }) {
|
|
63
|
+
const { skipParsePath = false } = opts;
|
|
64
|
+
if (!this.workspaceRoot) {
|
|
65
|
+
console.error('工作目录根路径未设置,使用默认目录:', this.baseDir);
|
|
66
|
+
return this.getFilePath(fileKey, nodeId, filename);
|
|
67
|
+
}
|
|
68
|
+
// 清理文件名中的特殊字符
|
|
69
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_');
|
|
70
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_]/g, '_') : 'root';
|
|
71
|
+
const cleanFilename = filename.replace(/[^a-zA-Z0-9\u4e00-\u9fff\u3400-\u4dbf-_.]/g, '_');
|
|
72
|
+
if (skipParsePath) {
|
|
73
|
+
return path.join(this.workspaceRoot, '.sloth', fileKey, nodeId || 'root', cleanFilename);
|
|
74
|
+
}
|
|
75
|
+
return path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, cleanFilename);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 获取工作目录下特定 fileKey 的目录路径
|
|
79
|
+
* @param fileKey - Figma 文件的 key
|
|
80
|
+
* @returns string | null
|
|
81
|
+
*/
|
|
82
|
+
getWorkspaceFileKeyDirPath(fileKey) {
|
|
83
|
+
if (!this.workspaceRoot)
|
|
84
|
+
return null;
|
|
85
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
86
|
+
return path.join(this.workspaceRoot, '.sloth', cleanFileKey);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* 保存文件内容
|
|
90
|
+
* @param fileKey - Figma文件的key
|
|
91
|
+
* @param nodeId - 节点ID(可选)
|
|
92
|
+
* @param filename - 文件名
|
|
93
|
+
* @param content - 文件内容
|
|
94
|
+
* @param useWorkspaceDir - 是否使用工作目录
|
|
95
|
+
*/
|
|
96
|
+
async saveFile(fileKey, nodeId, filename, content, opts = { useWorkspaceDir: false, skipParsePath: false }) {
|
|
97
|
+
const { useWorkspaceDir = false, ...restOpts } = opts;
|
|
98
|
+
try {
|
|
99
|
+
const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename, restOpts) : this.getFilePath(fileKey, nodeId, filename);
|
|
100
|
+
// 确保目录存在
|
|
101
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
102
|
+
// 保存文件内容
|
|
103
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
104
|
+
Logger.log(`文件已保存: ${filePath}`);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
Logger.error(`保存文件失败: ${error}`);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 读取文件内容
|
|
113
|
+
* @param fileKey - Figma文件的key
|
|
114
|
+
* @param nodeId - 节点ID(可选)
|
|
115
|
+
* @param filename - 文件名
|
|
116
|
+
* @param useWorkspaceDir - 是否使用工作目录
|
|
117
|
+
* @returns Promise<string> - 文件内容,如果文件不存在则返回空字符串
|
|
118
|
+
*/
|
|
119
|
+
async loadFile(fileKey, nodeId, filename, opts = { useWorkspaceDir: false, skipParsePath: false }) {
|
|
120
|
+
const { useWorkspaceDir = false, ...restOpts } = opts;
|
|
121
|
+
try {
|
|
122
|
+
const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename, restOpts) : this.getFilePath(fileKey, nodeId, filename);
|
|
123
|
+
Logger.log(`加载文件: ${filePath}, workspaceRoot: ${this.workspaceRoot}`);
|
|
124
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
125
|
+
return content;
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
if (err.code === 'ENOENT') {
|
|
129
|
+
Logger.log(`文件不存在: ${filename}`);
|
|
130
|
+
return '';
|
|
131
|
+
}
|
|
132
|
+
Logger.error(`读取文件失败: ${err.toString()}`);
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* 检查文件是否存在
|
|
138
|
+
* @param fileKey - Figma文件的key
|
|
139
|
+
* @param nodeId - 节点ID(可选)
|
|
140
|
+
* @param filename - 文件名
|
|
141
|
+
* @returns Promise<boolean> - 如果文件存在则返回true
|
|
142
|
+
*/
|
|
143
|
+
async exists(fileKey, nodeId, filename) {
|
|
144
|
+
try {
|
|
145
|
+
const filePath = this.getFilePath(fileKey, nodeId, filename);
|
|
146
|
+
await fs.access(filePath);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* 删除文件
|
|
155
|
+
* @param fileKey - Figma文件的key
|
|
156
|
+
* @param nodeId - 节点ID(可选)
|
|
157
|
+
* @param filename - 文件名
|
|
158
|
+
*/
|
|
159
|
+
async deleteFile(fileKey, nodeId, filename) {
|
|
160
|
+
try {
|
|
161
|
+
const filePath = this.getFilePath(fileKey, nodeId, filename);
|
|
162
|
+
await fs.unlink(filePath);
|
|
163
|
+
Logger.log(`文件已删除: ${filePath}`);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (err.code !== 'ENOENT') {
|
|
167
|
+
Logger.error(`删除文件失败: ${err.toString()}`);
|
|
168
|
+
throw err; // 如果不是文件不存在错误,则抛出异常
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 清理特定fileKey的所有文件
|
|
174
|
+
* @param fileKey - Figma文件的key
|
|
175
|
+
*/
|
|
176
|
+
async cleanupFileKey(fileKey) {
|
|
177
|
+
try {
|
|
178
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
179
|
+
const fileKeyDir = path.join(this.baseDir, cleanFileKey);
|
|
180
|
+
await fs.rm(fileKeyDir, { recursive: true, force: true });
|
|
181
|
+
Logger.log(`文件Key目录已清理: ${fileKeyDir}`);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
Logger.error(`清理文件Key目录失败: ${error}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* 清理特定fileKey和nodeId的所有文件
|
|
189
|
+
* @param fileKey - Figma文件的key
|
|
190
|
+
* @param nodeId - 节点ID(可选)
|
|
191
|
+
*/
|
|
192
|
+
async cleanupNode(fileKey, nodeId) {
|
|
193
|
+
try {
|
|
194
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
195
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_]/g, '_') : 'root';
|
|
196
|
+
const nodeDir = path.join(this.baseDir, cleanFileKey, cleanNodeId);
|
|
197
|
+
await fs.rm(nodeDir, { recursive: true, force: true });
|
|
198
|
+
Logger.log(`节点目录已清理: ${nodeDir}`);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
Logger.error(`清理节点目录失败: ${error}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* 清理整个存储目录
|
|
206
|
+
*/
|
|
207
|
+
async cleanup() {
|
|
208
|
+
try {
|
|
209
|
+
await fs.rm(this.baseDir, { recursive: true, force: true });
|
|
210
|
+
Logger.log(`存储目录已清理: ${this.baseDir}`);
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
Logger.error(`清理存储目录失败: ${error}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* 获取文件的完整路径(公共方法)
|
|
218
|
+
* @param fileKey - Figma文件的key
|
|
219
|
+
* @param nodeId - 节点ID(可选)
|
|
220
|
+
* @param filename - 文件名
|
|
221
|
+
* @returns string - 文件的完整路径
|
|
222
|
+
*/
|
|
223
|
+
getFilePathPublic(fileKey, nodeId, filename) {
|
|
224
|
+
return this.getFilePath(fileKey, nodeId, filename);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* 获取基础存储目录路径
|
|
228
|
+
* @returns string - 基础存储目录的完整路径
|
|
229
|
+
*/
|
|
230
|
+
getBaseDir() {
|
|
231
|
+
return this.baseDir;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 获取特定fileKey的目录路径
|
|
235
|
+
* @param fileKey - Figma文件的key
|
|
236
|
+
* @returns string - fileKey目录的完整路径
|
|
237
|
+
*/
|
|
238
|
+
getFileKeyDir(fileKey) {
|
|
239
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
240
|
+
return path.join(this.baseDir, cleanFileKey);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* 获取特定fileKey和nodeId的目录路径
|
|
244
|
+
* @param fileKey - Figma文件的key
|
|
245
|
+
* @param nodeId - 节点ID(可选)
|
|
246
|
+
* @returns string - 节点目录的完整路径
|
|
247
|
+
*/
|
|
248
|
+
getNodeDir(fileKey, nodeId) {
|
|
249
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
250
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_]/g, '_') : 'root';
|
|
251
|
+
return path.join(this.baseDir, cleanFileKey, cleanNodeId);
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* 保存 groupsData 到特定的 nodeId 文件(保存到工作目录的 .sloth 文件夹)
|
|
255
|
+
* @param fileKey - Figma文件的key
|
|
256
|
+
* @param nodeId - 节点ID(可选)
|
|
257
|
+
* @param groupsData - 分组数据
|
|
258
|
+
*/
|
|
259
|
+
async saveGroupsData(fileKey, nodeId, groupsData) {
|
|
260
|
+
await this.saveFile(fileKey, nodeId, 'groupsData.json', JSON.stringify(groupsData, null, 2), { useWorkspaceDir: true, skipParsePath: false });
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* 加载 groupsData 从特定的 nodeId 文件
|
|
264
|
+
* @param fileKey - Figma文件的key
|
|
265
|
+
* @param nodeId - 节点ID(可选)
|
|
266
|
+
* @returns Promise<any[]> - 分组数据,如果文件不存在则返回空数组
|
|
267
|
+
*/
|
|
268
|
+
async loadGroupsData(fileKey, nodeId) {
|
|
269
|
+
try {
|
|
270
|
+
const content = await this.loadFile(fileKey, nodeId, 'groupsData.json', { useWorkspaceDir: true, skipParsePath: false });
|
|
271
|
+
return content ? JSON.parse(content) : [];
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
Logger.log(`加载 groupsData 失败,返回空数组: ${error}`);
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* 保存 promptSetting 到特定的 nodeId 文件(保存到工作目录的 .sloth 文件夹)
|
|
280
|
+
* @param fileKey - Figma文件的key
|
|
281
|
+
* @param nodeId - 节点ID(可选)
|
|
282
|
+
* @param promptSetting - 提示词设置对象,格式为 { [key: string]: string }
|
|
283
|
+
*/
|
|
284
|
+
/**
|
|
285
|
+
* 将 camelCase 转换为 kebab-case
|
|
286
|
+
*/
|
|
287
|
+
camelToKebab(str) {
|
|
288
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* 将 kebab-case 转换为 camelCase
|
|
292
|
+
*/
|
|
293
|
+
kebabToCamel(str) {
|
|
294
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
295
|
+
}
|
|
296
|
+
async savePromptSetting(fileKey, nodeId, promptSetting) {
|
|
297
|
+
fileKey = '.';
|
|
298
|
+
nodeId = '.';
|
|
299
|
+
// 按 key 存储为独立的 md 文件
|
|
300
|
+
if (promptSetting && typeof promptSetting === 'object') {
|
|
301
|
+
for (const [key, value] of Object.entries(promptSetting)) {
|
|
302
|
+
if (typeof value === 'string' && value) {
|
|
303
|
+
// 将 camelCase key 转换为 kebab-case 文件名
|
|
304
|
+
const fileName = this.camelToKebab(key);
|
|
305
|
+
Logger.log(`fileName: ${fileName}`, 'this.workspaceRoot: ', this.workspaceRoot);
|
|
306
|
+
// 直接构建完整路径到 .sloth/prompt/xx.md
|
|
307
|
+
const filePath = path.join(this.workspaceRoot, '.sloth', 'd2c-prompt', `${fileName}.md`);
|
|
308
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
309
|
+
await fs.writeFile(filePath, value, 'utf-8');
|
|
310
|
+
Logger.log(`Prompt 文件已保存: ${filePath}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* 加载 promptSetting 从特定的 nodeId 文件
|
|
317
|
+
* @param fileKey - Figma文件的key
|
|
318
|
+
* @param nodeId - 节点ID(可选)
|
|
319
|
+
* @returns Promise<any> - 提示词设置对象,如果文件不存在则返回 null
|
|
320
|
+
*/
|
|
321
|
+
async loadPromptSetting(fileKey, nodeId) {
|
|
322
|
+
fileKey = '.';
|
|
323
|
+
nodeId = '.';
|
|
324
|
+
try {
|
|
325
|
+
// 兼容老格式:尝试读取 promptSetting.json
|
|
326
|
+
const oldContent = await this.loadFile(fileKey, nodeId, 'promptSetting.json', { useWorkspaceDir: true, skipParsePath: true });
|
|
327
|
+
if (oldContent) {
|
|
328
|
+
Logger.log('检测到老格式 promptSetting.json,准备删除');
|
|
329
|
+
// 删除老文件
|
|
330
|
+
const filePath = this.getWorkspaceFilePath(fileKey, nodeId, 'promptSetting.json', { skipParsePath: true });
|
|
331
|
+
await fs.unlink(filePath).catch((err) => Logger.log(`删除老文件失败: ${err}`));
|
|
332
|
+
return JSON.parse(oldContent);
|
|
333
|
+
}
|
|
334
|
+
// 读取新格式:所有 .md 文件,直接从 .sloth/d2c-prompt/ 目录读取
|
|
335
|
+
const dirPath = path.join(this.workspaceRoot, '.sloth', 'd2c-prompt');
|
|
336
|
+
const files = await fs.readdir(dirPath).catch(() => []);
|
|
337
|
+
const result = {};
|
|
338
|
+
for (const file of files) {
|
|
339
|
+
if (file.endsWith('.md')) {
|
|
340
|
+
// 将 kebab-case 文件名转换回 camelCase key
|
|
341
|
+
const kebabKey = file.replace(/\.md$/, '');
|
|
342
|
+
const key = this.kebabToCamel(kebabKey);
|
|
343
|
+
const filePath = path.join(dirPath, file);
|
|
344
|
+
const content = await fs.readFile(filePath, 'utf-8').catch(() => '');
|
|
345
|
+
if (content) {
|
|
346
|
+
result[key] = content;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
Logger.log(`加载 promptSetting 失败,返回 null: ${error}`);
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* 保存 configSetting 到特定的 nodeId 文件(保存到工作目录的 .sloth 文件夹)
|
|
359
|
+
* @param fileKey - Figma文件的key
|
|
360
|
+
* @param nodeId - 节点ID(可选)
|
|
361
|
+
* @param configSetting - 配置设置
|
|
362
|
+
*/
|
|
363
|
+
async saveConfigSetting(fileKey, nodeId, configSetting) {
|
|
364
|
+
fileKey = '.';
|
|
365
|
+
nodeId = '.';
|
|
366
|
+
await this.saveFile(fileKey, nodeId, 'configSetting.json', JSON.stringify(configSetting, null, 2), { useWorkspaceDir: true, skipParsePath: true });
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* 加载 configSetting 从特定的 nodeId 文件
|
|
370
|
+
* @param fileKey - Figma文件的key
|
|
371
|
+
* @param nodeId - 节点ID(可选)
|
|
372
|
+
* @returns Promise<any> - 配置设置,如果文件不存在则返回 null
|
|
373
|
+
*/
|
|
374
|
+
async loadConfigSetting(fileKey, nodeId) {
|
|
375
|
+
fileKey = '.';
|
|
376
|
+
nodeId = '.';
|
|
377
|
+
try {
|
|
378
|
+
const content = await this.loadFile(fileKey, nodeId, 'configSetting.json', { useWorkspaceDir: true, skipParsePath: true });
|
|
379
|
+
return content ? JSON.parse(content) : null;
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
Logger.log(`加载 configSetting 失败,返回 null: ${error}`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* 列出指定 fileKey 下的所有 nodeId(目录名)
|
|
388
|
+
* @param fileKey - Figma 文件的 key
|
|
389
|
+
*/
|
|
390
|
+
async listNodeIds(fileKey) {
|
|
391
|
+
const workspaceDir = this.getWorkspaceFileKeyDirPath(fileKey);
|
|
392
|
+
const targetDir = workspaceDir || this.getFileKeyDir(fileKey);
|
|
393
|
+
try {
|
|
394
|
+
const dirents = await fs.readdir(targetDir, { withFileTypes: true });
|
|
395
|
+
return dirents.filter((d) => d.isDirectory()).map((d) => d.name);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* 列出所有 fileKey
|
|
403
|
+
* @returns Promise<string[]> - 所有 fileKey 的列表
|
|
404
|
+
*/
|
|
405
|
+
async listAllFileKeys() {
|
|
406
|
+
try {
|
|
407
|
+
if (!this.workspaceRoot) {
|
|
408
|
+
Logger.log('workspaceRoot 未设置,无法列出所有 fileKey');
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
const slothDir = path.join(this.workspaceRoot, '.sloth');
|
|
412
|
+
const entries = await fs.readdir(slothDir, { withFileTypes: true });
|
|
413
|
+
const fileKeys = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
414
|
+
Logger.log(`找到 ${fileKeys.length} 个 fileKey:`, fileKeys);
|
|
415
|
+
return fileKeys;
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* 加载指定 fileKey 下所有 nodeId 的 groupsData
|
|
423
|
+
* @param fileKey - Figma 文件的 key
|
|
424
|
+
*/
|
|
425
|
+
async loadAllGroupsData(fileKey) {
|
|
426
|
+
const nodeIds = await this.listNodeIds(fileKey);
|
|
427
|
+
const results = [];
|
|
428
|
+
Logger.log(`加载指定 fileKey 下所有 nodeId 的 groupsData: ${fileKey}, nodeIds: ${nodeIds}`);
|
|
429
|
+
if (nodeIds.length === 0) {
|
|
430
|
+
const groups = await this.loadGroupsData(fileKey, undefined);
|
|
431
|
+
if (groups.length > 0) {
|
|
432
|
+
results.push({ nodeId: 'root', groups });
|
|
433
|
+
}
|
|
434
|
+
return results;
|
|
435
|
+
}
|
|
436
|
+
for (const nodeId of nodeIds) {
|
|
437
|
+
const normalizedNodeId = nodeId === 'root' ? undefined : nodeId;
|
|
438
|
+
Logger.log(`加载指定 fileKey 下 nodeId 的 groupsData: ${fileKey}, nodeId: ${nodeId}`);
|
|
439
|
+
const groups = await this.loadGroupsData(fileKey, normalizedNodeId);
|
|
440
|
+
if (groups && groups.length > 0) {
|
|
441
|
+
results.push({ nodeId, groups });
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return results;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 加载整个项目所有 fileKey 的所有 groupsData(用于跨文件组件匹配)
|
|
448
|
+
* @returns Promise<Array<{ fileKey: string; nodeId: string; groups: any[] }>>
|
|
449
|
+
*/
|
|
450
|
+
async loadAllProjectGroupsData() {
|
|
451
|
+
const fileKeys = await this.listAllFileKeys();
|
|
452
|
+
const results = [];
|
|
453
|
+
Logger.log(`开始加载整个项目的 groupsData,共 ${fileKeys.length} 个 fileKey`);
|
|
454
|
+
for (const fileKey of fileKeys) {
|
|
455
|
+
const groupsDataByNode = await this.loadAllGroupsData(fileKey);
|
|
456
|
+
for (const { nodeId, groups } of groupsDataByNode) {
|
|
457
|
+
if (groups && groups.length > 0) {
|
|
458
|
+
results.push({ fileKey, nodeId, groups });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
Logger.log(`项目 groupsData 加载完成,共 ${results.length} 个节点有分组数据`);
|
|
463
|
+
return results;
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 获取 screenshots 目录路径
|
|
467
|
+
* @param fileKey - Figma文件的key
|
|
468
|
+
* @param nodeId - 节点ID(可选)
|
|
469
|
+
* @returns string - screenshots 目录的完整路径
|
|
470
|
+
*/
|
|
471
|
+
getScreenshotsDir(fileKey, nodeId) {
|
|
472
|
+
const nodeDir = this.getWorkspaceFilePath(fileKey, nodeId, '');
|
|
473
|
+
return path.join(nodeDir, 'screenshots');
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* 保存截图文件
|
|
477
|
+
* @param fileKey - Figma文件的key
|
|
478
|
+
* @param nodeId - 节点ID(可选)
|
|
479
|
+
* @param hash - 截图文件的哈希值(作为文件名)
|
|
480
|
+
* @param buffer - 截图文件的二进制数据
|
|
481
|
+
*/
|
|
482
|
+
async saveScreenshot(fileKey, nodeId, hash, buffer) {
|
|
483
|
+
try {
|
|
484
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
485
|
+
// 确保目录存在
|
|
486
|
+
await fs.mkdir(screenshotsDir, { recursive: true });
|
|
487
|
+
const filePath = path.join(screenshotsDir, `${hash}.png`);
|
|
488
|
+
await fs.writeFile(filePath, buffer);
|
|
489
|
+
Logger.log(`截图已保存: ${filePath}`);
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
Logger.error(`保存截图失败: ${error}`);
|
|
493
|
+
throw error;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 加载截图文件
|
|
498
|
+
* @param fileKey - Figma文件的key
|
|
499
|
+
* @param nodeId - 节点ID(可选)
|
|
500
|
+
* @param hash - 截图文件的哈希值
|
|
501
|
+
* @returns Promise<Buffer> - 截图文件的二进制数据
|
|
502
|
+
*/
|
|
503
|
+
async loadScreenshot(fileKey, nodeId, hash) {
|
|
504
|
+
try {
|
|
505
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
506
|
+
const filePath = path.join(screenshotsDir, `${hash}.png`);
|
|
507
|
+
const buffer = await fs.readFile(filePath);
|
|
508
|
+
return buffer;
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
if (err.code === 'ENOENT') {
|
|
512
|
+
Logger.log(`截图文件不存在: ${hash}.png`);
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
Logger.error(`加载截图失败: ${err.toString()}`);
|
|
516
|
+
throw err;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* 检查截图文件是否存在
|
|
521
|
+
* @param fileKey - Figma文件的key
|
|
522
|
+
* @param nodeId - 节点ID(可选)
|
|
523
|
+
* @param hash - 截图文件的哈希值
|
|
524
|
+
* @returns Promise<boolean> - 如果文件存在则返回 true
|
|
525
|
+
*/
|
|
526
|
+
async screenshotExists(fileKey, nodeId, hash) {
|
|
527
|
+
try {
|
|
528
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
529
|
+
const filePath = path.join(screenshotsDir, `${hash}.png`);
|
|
530
|
+
await fs.access(filePath);
|
|
531
|
+
return true;
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* 获取截图文件的完整路径
|
|
539
|
+
* @param fileKey - Figma文件的key
|
|
540
|
+
* @param nodeId - 节点ID(可选)
|
|
541
|
+
* @param hash - 截图文件的哈希值
|
|
542
|
+
* @returns string - 截图文件的完整路径
|
|
543
|
+
*/
|
|
544
|
+
getScreenshotPath(fileKey, nodeId, hash) {
|
|
545
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
546
|
+
return path.join(screenshotsDir, `${hash}.png`);
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* 删除截图文件
|
|
550
|
+
* @param fileKey - Figma文件的key
|
|
551
|
+
* @param nodeId - 节点ID(可选)
|
|
552
|
+
* @param hash - 截图文件的哈希值
|
|
553
|
+
*/
|
|
554
|
+
async deleteScreenshot(fileKey, nodeId, hash) {
|
|
555
|
+
try {
|
|
556
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
557
|
+
const filePath = path.join(screenshotsDir, `${hash}.png`);
|
|
558
|
+
await fs.unlink(filePath);
|
|
559
|
+
Logger.log(`截图已删除: ${filePath}`);
|
|
560
|
+
}
|
|
561
|
+
catch (err) {
|
|
562
|
+
if (err.code !== 'ENOENT') {
|
|
563
|
+
Logger.error(`删除截图失败: ${err.toString()}`);
|
|
564
|
+
throw err;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* 清理旧的截图文件(根据时间戳)
|
|
570
|
+
* @param fileKey - Figma文件的key
|
|
571
|
+
* @param nodeId - 节点ID(可选)
|
|
572
|
+
* @param maxAgeDays - 最大保留天数
|
|
573
|
+
*/
|
|
574
|
+
async cleanupOldScreenshots(fileKey, nodeId, maxAgeDays = 30) {
|
|
575
|
+
try {
|
|
576
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
577
|
+
// 检查目录是否存在
|
|
578
|
+
try {
|
|
579
|
+
await fs.access(screenshotsDir);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
// 目录不存在,直接返回
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const files = await fs.readdir(screenshotsDir);
|
|
586
|
+
const now = Date.now();
|
|
587
|
+
const maxAge = maxAgeDays * 24 * 60 * 60 * 1000;
|
|
588
|
+
let deletedCount = 0;
|
|
589
|
+
for (const file of files) {
|
|
590
|
+
const filePath = path.join(screenshotsDir, file);
|
|
591
|
+
const stats = await fs.stat(filePath);
|
|
592
|
+
const age = now - stats.mtimeMs;
|
|
593
|
+
if (age > maxAge) {
|
|
594
|
+
await fs.unlink(filePath);
|
|
595
|
+
deletedCount++;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
if (deletedCount > 0) {
|
|
599
|
+
Logger.log(`清理了 ${deletedCount} 个旧截图文件(超过 ${maxAgeDays} 天)`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch (error) {
|
|
603
|
+
Logger.error(`清理旧截图失败: ${error}`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* 读取组件数据库
|
|
608
|
+
* @returns Promise<StoredComponent[]> - 组件列表
|
|
609
|
+
*/
|
|
610
|
+
async loadComponentsDatabase() {
|
|
611
|
+
const workspaceRoot = this.getWorkspaceRoot();
|
|
612
|
+
if (!workspaceRoot) {
|
|
613
|
+
Logger.warn('工作目录根路径未设置,无法加载组件数据库');
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
const componentsPath = path.join(workspaceRoot, '.sloth', 'components.json');
|
|
617
|
+
try {
|
|
618
|
+
const content = await fs.readFile(componentsPath, 'utf-8');
|
|
619
|
+
// 验证文件内容不为空
|
|
620
|
+
if (!content || !content.trim()) {
|
|
621
|
+
Logger.warn('components.json 文件为空,返回空数组');
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
const components = JSON.parse(content);
|
|
625
|
+
// 验证数据格式
|
|
626
|
+
if (!Array.isArray(components)) {
|
|
627
|
+
Logger.warn('components.json 格式错误(不是数组),返回空数组');
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
Logger.log(`成功加载 ${components.length} 个组件`);
|
|
631
|
+
return components;
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
const err = error;
|
|
635
|
+
if (err.code === 'ENOENT') {
|
|
636
|
+
// 文件不存在,返回空数组
|
|
637
|
+
Logger.log('components.json 不存在,将创建新文件');
|
|
638
|
+
return [];
|
|
639
|
+
}
|
|
640
|
+
if (err instanceof SyntaxError) {
|
|
641
|
+
Logger.error('components.json JSON 解析失败:', err.message);
|
|
642
|
+
return [];
|
|
643
|
+
}
|
|
644
|
+
Logger.error('读取组件数据库失败:', err);
|
|
645
|
+
throw error;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* 保存组件数据库
|
|
650
|
+
* @param components - 要保存的组件列表
|
|
651
|
+
*/
|
|
652
|
+
async saveComponentsDatabase(components) {
|
|
653
|
+
const workspaceRoot = this.getWorkspaceRoot();
|
|
654
|
+
const slothDir = path.join(workspaceRoot, '.sloth');
|
|
655
|
+
const componentsPath = path.join(slothDir, 'components.json');
|
|
656
|
+
// 确保目录存在
|
|
657
|
+
try {
|
|
658
|
+
await fs.mkdir(slothDir, { recursive: true });
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
Logger.error('创建 .sloth 目录失败:', error);
|
|
662
|
+
throw new Error(`无法创建目录: ${slothDir}`);
|
|
663
|
+
}
|
|
664
|
+
// 写入数据
|
|
665
|
+
try {
|
|
666
|
+
const jsonContent = JSON.stringify(components, null, 2);
|
|
667
|
+
await fs.writeFile(componentsPath, jsonContent, 'utf-8');
|
|
668
|
+
Logger.log(`✅ 已保存 ${components.length} 个组件到 components.json`);
|
|
669
|
+
}
|
|
670
|
+
catch (error) {
|
|
671
|
+
Logger.error('写入组件数据库失败:', error);
|
|
672
|
+
throw new Error(`无法写入文件: ${componentsPath}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* 根据截图 hash 在整个 .sloth 目录下搜索截图文件
|
|
677
|
+
* @param hash - 截图文件的哈希值
|
|
678
|
+
* @returns Promise<string | null> - 截图文件的完整路径,如果未找到则返回 null
|
|
679
|
+
*/
|
|
680
|
+
async findScreenshotByHash(hash) {
|
|
681
|
+
const workspaceRoot = this.getWorkspaceRoot();
|
|
682
|
+
if (!workspaceRoot) {
|
|
683
|
+
Logger.warn('工作目录根路径未设置,无法搜索截图');
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
const slothDir = path.join(workspaceRoot, '.sloth');
|
|
687
|
+
try {
|
|
688
|
+
await fs.access(slothDir);
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
Logger.warn('.sloth 目录不存在');
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
const targetFilename = `${hash}.png`;
|
|
695
|
+
// 递归搜索 .sloth 目录下所有 screenshots 文件夹
|
|
696
|
+
const searchDir = async (dir) => {
|
|
697
|
+
try {
|
|
698
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
699
|
+
for (const entry of entries) {
|
|
700
|
+
const fullPath = path.join(dir, entry.name);
|
|
701
|
+
if (entry.isDirectory()) {
|
|
702
|
+
// 如果是 screenshots 目录,检查是否有目标文件
|
|
703
|
+
if (entry.name === 'screenshots') {
|
|
704
|
+
const screenshotPath = path.join(fullPath, targetFilename);
|
|
705
|
+
try {
|
|
706
|
+
await fs.access(screenshotPath);
|
|
707
|
+
return screenshotPath;
|
|
708
|
+
}
|
|
709
|
+
catch {
|
|
710
|
+
// 文件不存在,继续搜索
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
else {
|
|
714
|
+
// 递归搜索子目录
|
|
715
|
+
const result = await searchDir(fullPath);
|
|
716
|
+
if (result)
|
|
717
|
+
return result;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
Logger.error(`搜索目录失败 ${dir}:`, error);
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
return searchDir(slothDir);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 保存 absolute.html 到 .sloth 目录,支持版本管理
|
|
732
|
+
* 如果内容不一致,将当前版本移动到带时间戳的文件夹中
|
|
733
|
+
* @param fileKey - Figma文件的key
|
|
734
|
+
* @param nodeId - 节点ID(可选)
|
|
735
|
+
* @param content - 文件内容
|
|
736
|
+
*/
|
|
737
|
+
async saveAbsoluteHtml(fileKey, nodeId, content, useWorkspaceDir = false) {
|
|
738
|
+
const filename = 'absolute.html';
|
|
739
|
+
// 先尝试加载现有文件
|
|
740
|
+
const existingContent = await this.loadFile(fileKey, nodeId, filename, { useWorkspaceDir });
|
|
741
|
+
// 如果存在且内容不一致,先归档旧版本
|
|
742
|
+
if (existingContent && existingContent.trim() !== content.trim()) {
|
|
743
|
+
await this.archiveAbsoluteHtml(fileKey, nodeId, existingContent);
|
|
744
|
+
Logger.log(`检测到 absolute.html 内容变化,已归档旧版本`);
|
|
745
|
+
}
|
|
746
|
+
// 保存新内容
|
|
747
|
+
await this.saveFile(fileKey, nodeId, filename, content, { useWorkspaceDir });
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* 归档 absolute.html 到带时间戳的文件夹
|
|
751
|
+
* @param fileKey - Figma文件的key
|
|
752
|
+
* @param nodeId - 节点ID(可选)
|
|
753
|
+
* @param content - 要归档的内容
|
|
754
|
+
*/
|
|
755
|
+
async archiveAbsoluteHtml(fileKey, nodeId, content) {
|
|
756
|
+
if (!this.workspaceRoot) {
|
|
757
|
+
Logger.warn('工作目录根路径未设置,无法归档');
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// 生成时间戳文件夹名:YYYYMMDD_HHmmss
|
|
761
|
+
const now = new Date();
|
|
762
|
+
const timestamp = now.getFullYear().toString() +
|
|
763
|
+
(now.getMonth() + 1).toString().padStart(2, '0') +
|
|
764
|
+
now.getDate().toString().padStart(2, '0') +
|
|
765
|
+
'_' +
|
|
766
|
+
now.getHours().toString().padStart(2, '0') +
|
|
767
|
+
now.getMinutes().toString().padStart(2, '0') +
|
|
768
|
+
now.getSeconds().toString().padStart(2, '0');
|
|
769
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
770
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_]/g, '_') : 'root';
|
|
771
|
+
// 归档路径:.sloth/{fileKey}/{nodeId}/history/{timestamp}/absolute.html
|
|
772
|
+
const archiveDir = path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, 'history', timestamp);
|
|
773
|
+
try {
|
|
774
|
+
await fs.mkdir(archiveDir, { recursive: true });
|
|
775
|
+
const archivePath = path.join(archiveDir, 'absolute.html');
|
|
776
|
+
await fs.writeFile(archivePath, content, 'utf-8');
|
|
777
|
+
// 同时复制 index.png 截图(如果存在)
|
|
778
|
+
const screenshotsDir = this.getScreenshotsDir(fileKey, nodeId);
|
|
779
|
+
const indexScreenshotPath = path.join(screenshotsDir, 'index.png');
|
|
780
|
+
try {
|
|
781
|
+
await fs.access(indexScreenshotPath);
|
|
782
|
+
const archiveScreenshotPath = path.join(archiveDir, 'index.png');
|
|
783
|
+
await fs.copyFile(indexScreenshotPath, archiveScreenshotPath);
|
|
784
|
+
Logger.log(`截图已归档: ${archiveScreenshotPath}`);
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
// 截图不存在,跳过
|
|
788
|
+
}
|
|
789
|
+
Logger.log(`absolute.html 已归档到: ${archivePath}`);
|
|
790
|
+
}
|
|
791
|
+
catch (error) {
|
|
792
|
+
Logger.error(`归档 absolute.html 失败: ${error}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* 加载 absolute.html
|
|
797
|
+
* @param fileKey - Figma文件的key
|
|
798
|
+
* @param nodeId - 节点ID(可选)
|
|
799
|
+
* @returns Promise<string> - 文件内容
|
|
800
|
+
*/
|
|
801
|
+
async loadAbsoluteHtml(fileKey, nodeId, useWorkspaceDir = false) {
|
|
802
|
+
return this.loadFile(fileKey, nodeId, 'absolute.html', { useWorkspaceDir });
|
|
803
|
+
}
|
|
804
|
+
/**
|
|
805
|
+
* 列出 absolute.html 的所有历史版本
|
|
806
|
+
* @param fileKey - Figma文件的key
|
|
807
|
+
* @param nodeId - 节点ID(可选)
|
|
808
|
+
* @returns Promise<Array<{ timestamp: string; path: string }>> - 历史版本列表
|
|
809
|
+
*/
|
|
810
|
+
async listAbsoluteHtmlHistory(fileKey, nodeId) {
|
|
811
|
+
if (!this.workspaceRoot) {
|
|
812
|
+
return [];
|
|
813
|
+
}
|
|
814
|
+
const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
|
|
815
|
+
const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_]/g, '_') : 'root';
|
|
816
|
+
const historyDir = path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, 'history');
|
|
817
|
+
try {
|
|
818
|
+
const entries = await fs.readdir(historyDir, { withFileTypes: true });
|
|
819
|
+
const versions = [];
|
|
820
|
+
for (const entry of entries) {
|
|
821
|
+
if (entry.isDirectory()) {
|
|
822
|
+
const htmlPath = path.join(historyDir, entry.name, 'absolute.html');
|
|
823
|
+
const screenshotPath = path.join(historyDir, entry.name, 'index.png');
|
|
824
|
+
try {
|
|
825
|
+
await fs.access(htmlPath);
|
|
826
|
+
let screenshot;
|
|
827
|
+
try {
|
|
828
|
+
await fs.access(screenshotPath);
|
|
829
|
+
screenshot = screenshotPath;
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// 截图不存在
|
|
833
|
+
}
|
|
834
|
+
versions.push({
|
|
835
|
+
timestamp: entry.name,
|
|
836
|
+
path: htmlPath,
|
|
837
|
+
screenshotPath: screenshot,
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
catch {
|
|
841
|
+
// html 文件不存在,跳过
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// 按时间戳倒序排列(最新的在前)
|
|
846
|
+
versions.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
847
|
+
return versions;
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
return [];
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* 创建一个临时的 FileManager 代理,仅在本次链式调用中使用指定的 uuid
|
|
855
|
+
* 不会影响原始实例的 _uuid 值
|
|
856
|
+
* @param uuid - 临时使用的 uuid
|
|
857
|
+
* @returns 带有临时 uuid 的 FileManager 代理对象
|
|
858
|
+
*/
|
|
859
|
+
withUUID(uuid) {
|
|
860
|
+
if (!uuid) {
|
|
861
|
+
return this;
|
|
862
|
+
}
|
|
863
|
+
const proxy = Object.create(this);
|
|
864
|
+
proxy._uuid = uuid;
|
|
865
|
+
return proxy;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
export default FileManager;
|