sloth-d2c-mcp 1.0.4-beta75 → 1.0.4-beta77

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,163 @@
1
+ import * as net from 'net';
2
+ import { Logger } from './utils/logger.js';
3
+ /**
4
+ * Socket 客户端类
5
+ * 用于子进程连接到主进程的 Socket 服务器,接收认证结果
6
+ */
7
+ export class SocketClient {
8
+ socket = null;
9
+ host;
10
+ port;
11
+ connected = false;
12
+ messageBuffer = '';
13
+ messageHandlers = new Map();
14
+ constructor(host = 'localhost', port) {
15
+ this.host = host;
16
+ this.port = port;
17
+ }
18
+ /**
19
+ * 连接到 Socket 服务器
20
+ */
21
+ connect() {
22
+ return new Promise((resolve, reject) => {
23
+ this.socket = new net.Socket();
24
+ this.socket.setEncoding('utf8');
25
+ // 连接成功
26
+ this.socket.on('connect', () => {
27
+ Logger.log(`Socket 客户端已连接到 ${this.host}:${this.port}`);
28
+ this.connected = true;
29
+ resolve();
30
+ });
31
+ // 接收数据
32
+ this.socket.on('data', (data) => {
33
+ this.handleData(data);
34
+ });
35
+ // 处理错误
36
+ this.socket.on('error', (error) => {
37
+ Logger.error('Socket 客户端错误:', error);
38
+ this.connected = false;
39
+ if (!this.socket?.connecting) {
40
+ reject(error);
41
+ }
42
+ });
43
+ // 处理断开连接
44
+ this.socket.on('close', () => {
45
+ Logger.log('Socket 客户端已断开连接');
46
+ this.connected = false;
47
+ });
48
+ // 开始连接
49
+ this.socket.connect(this.port, this.host);
50
+ });
51
+ }
52
+ /**
53
+ * 处理接收到的数据(支持粘包处理)
54
+ */
55
+ handleData(data) {
56
+ // 将数据添加到缓冲区
57
+ this.messageBuffer += data;
58
+ // 按换行符分割消息
59
+ const messages = this.messageBuffer.split('\n');
60
+ // 保留最后一个不完整的消息
61
+ this.messageBuffer = messages.pop() || '';
62
+ // 处理每个完整的消息
63
+ for (const messageStr of messages) {
64
+ if (!messageStr.trim())
65
+ continue;
66
+ try {
67
+ const message = JSON.parse(messageStr);
68
+ this.handleMessage(message);
69
+ }
70
+ catch (error) {
71
+ Logger.error('解析 Socket 消息失败:', error, '原始数据:', messageStr);
72
+ }
73
+ }
74
+ }
75
+ /**
76
+ * 处理解析后的消息
77
+ */
78
+ handleMessage(message) {
79
+ Logger.log('收到 Socket 消息:', message.type);
80
+ // 如果是认证响应,调用对应的处理器
81
+ if (message.type === 'submit-response' && message.token) {
82
+ const handler = this.messageHandlers.get(message.token);
83
+ if (handler) {
84
+ handler(message.data);
85
+ this.messageHandlers.delete(message.token);
86
+ }
87
+ }
88
+ // 处理其他消息类型
89
+ switch (message.type) {
90
+ case 'welcome':
91
+ Logger.log('收到欢迎消息:', message.message);
92
+ break;
93
+ case 'pong':
94
+ Logger.log('收到 pong 响应');
95
+ break;
96
+ case 'error':
97
+ Logger.error('服务器错误:', message.message);
98
+ break;
99
+ }
100
+ }
101
+ /**
102
+ * 发送消息到服务器
103
+ */
104
+ send(message) {
105
+ if (!this.socket || !this.connected) {
106
+ throw new Error('Socket 客户端未连接');
107
+ }
108
+ try {
109
+ this.socket.write(JSON.stringify(message) + '\n');
110
+ }
111
+ catch (error) {
112
+ Logger.error('发送 Socket 消息失败:', error);
113
+ throw error;
114
+ }
115
+ }
116
+ /**
117
+ * 注册 token,等待认证响应
118
+ */
119
+ registerToken(token) {
120
+ return new Promise((resolve, reject) => {
121
+ // 设置超时(可选,根据需求调整)
122
+ const timeout = setTimeout(() => {
123
+ this.messageHandlers.delete(token);
124
+ reject(new Error('等待认证响应超时'));
125
+ }, 5 * 60 * 1000); // 5分钟超时
126
+ // 注册消息处理器
127
+ this.messageHandlers.set(token, (data) => {
128
+ clearTimeout(timeout);
129
+ resolve(JSON.stringify(data));
130
+ });
131
+ // 发送注册消息到服务器
132
+ this.send({
133
+ type: 'register-token',
134
+ token,
135
+ timestamp: Date.now(),
136
+ });
137
+ Logger.log(`已注册 token: ${token},等待认证响应...`);
138
+ });
139
+ }
140
+ /**
141
+ * 发送 ping 消息
142
+ */
143
+ ping() {
144
+ this.send({ type: 'ping', timestamp: Date.now() });
145
+ }
146
+ /**
147
+ * 断开连接
148
+ */
149
+ disconnect() {
150
+ if (this.socket) {
151
+ this.socket.destroy();
152
+ this.socket = null;
153
+ this.connected = false;
154
+ this.messageHandlers.clear();
155
+ }
156
+ }
157
+ /**
158
+ * 检查是否已连接
159
+ */
160
+ isConnected() {
161
+ return this.connected;
162
+ }
163
+ }
@@ -0,0 +1,233 @@
1
+ import * as net from 'net';
2
+ import { Logger } from './utils/logger.js';
3
+ /**
4
+ * Socket 服务器类
5
+ * 使用原生 net 模块实现 TCP Socket 通信
6
+ */
7
+ export class SocketServer {
8
+ server = null;
9
+ connections = new Set();
10
+ tokenSockets = new Map(); // token -> socket 映射
11
+ messageBuffers = new Map(); // socket -> 消息缓冲区
12
+ port = 0;
13
+ constructor() {
14
+ this.server = net.createServer(this.handleConnection.bind(this));
15
+ this.setupServerEvents();
16
+ }
17
+ /**
18
+ * 处理新的 Socket 连接
19
+ */
20
+ handleConnection(socket) {
21
+ const clientId = `${socket.remoteAddress}:${socket.remotePort}`;
22
+ Logger.log(`Socket 客户端已连接: ${clientId}`);
23
+ // 添加到活跃连接集合
24
+ this.connections.add(socket);
25
+ this.messageBuffers.set(socket, '');
26
+ // 设置编码
27
+ socket.setEncoding('utf8');
28
+ // 接收数据
29
+ socket.on('data', (data) => {
30
+ this.handleData(socket, clientId, data);
31
+ });
32
+ // 处理错误
33
+ socket.on('error', (error) => {
34
+ Logger.error(`Socket 客户端 ${clientId} 错误:`, error);
35
+ });
36
+ // 处理断开连接
37
+ socket.on('close', () => {
38
+ Logger.log(`Socket 客户端已断开: ${clientId}`);
39
+ this.connections.delete(socket);
40
+ this.messageBuffers.delete(socket);
41
+ // 清理该 socket 关联的所有 token
42
+ for (const [token, sock] of this.tokenSockets.entries()) {
43
+ if (sock === socket) {
44
+ this.tokenSockets.delete(token);
45
+ Logger.log(`清理 token: ${token}`);
46
+ }
47
+ }
48
+ });
49
+ // 发送欢迎消息
50
+ this.sendMessage(socket, {
51
+ type: 'welcome',
52
+ message: 'Socket 服务器已连接',
53
+ timestamp: Date.now(),
54
+ });
55
+ }
56
+ /**
57
+ * 处理接收到的数据(支持粘包处理)
58
+ */
59
+ handleData(socket, clientId, data) {
60
+ // 将数据添加到缓冲区
61
+ const buffer = this.messageBuffers.get(socket) || '';
62
+ this.messageBuffers.set(socket, buffer + data);
63
+ // 按换行符分割消息
64
+ const messages = this.messageBuffers.get(socket).split('\n');
65
+ // 保留最后一个不完整的消息
66
+ this.messageBuffers.set(socket, messages.pop() || '');
67
+ // 处理每个完整的消息
68
+ for (const messageStr of messages) {
69
+ if (!messageStr.trim())
70
+ continue;
71
+ try {
72
+ const message = JSON.parse(messageStr);
73
+ this.handleMessage(socket, clientId, message);
74
+ }
75
+ catch (error) {
76
+ Logger.error(`解析 Socket 消息失败: ${error}`, '原始数据:', messageStr);
77
+ this.sendMessage(socket, { type: 'error', message: '无效的 JSON 格式' });
78
+ }
79
+ }
80
+ }
81
+ /**
82
+ * 处理解析后的消息
83
+ */
84
+ handleMessage(socket, clientId, message) {
85
+ Logger.log(`收到来自 ${clientId} 的消息类型: ${message.type}`);
86
+ // 处理不同类型的消息
87
+ switch (message.type) {
88
+ case 'ping':
89
+ this.sendMessage(socket, { type: 'pong', timestamp: Date.now() });
90
+ break;
91
+ case 'echo':
92
+ this.sendMessage(socket, { type: 'echo', data: message.data });
93
+ break;
94
+ case 'register-token':
95
+ // 注册 token,建立 token -> socket 映射
96
+ if (message.token) {
97
+ this.tokenSockets.set(message.token, socket);
98
+ Logger.log(`已注册 token: ${message.token} -> ${clientId}`);
99
+ this.sendMessage(socket, {
100
+ type: 'token-registered',
101
+ token: message.token,
102
+ timestamp: Date.now()
103
+ });
104
+ }
105
+ else {
106
+ this.sendMessage(socket, { type: 'error', message: '缺少 token' });
107
+ }
108
+ break;
109
+ default:
110
+ this.sendMessage(socket, { type: 'error', message: '未知的消息类型' });
111
+ }
112
+ }
113
+ /**
114
+ * 发送消息到客户端
115
+ */
116
+ sendMessage(socket, message) {
117
+ try {
118
+ socket.write(JSON.stringify(message) + '\n');
119
+ }
120
+ catch (error) {
121
+ Logger.error('发送 Socket 消息失败:', error);
122
+ }
123
+ }
124
+ /**
125
+ * 广播消息到所有连接的客户端
126
+ */
127
+ broadcast(message) {
128
+ const data = JSON.stringify(message) + '\n';
129
+ for (const socket of this.connections) {
130
+ try {
131
+ socket.write(data);
132
+ }
133
+ catch (error) {
134
+ Logger.error('广播消息失败:', error);
135
+ }
136
+ }
137
+ }
138
+ /**
139
+ * 设置服务器事件监听
140
+ */
141
+ setupServerEvents() {
142
+ if (!this.server)
143
+ return;
144
+ this.server.on('error', (error) => {
145
+ Logger.error('Socket 服务器错误:', error);
146
+ });
147
+ this.server.on('close', () => {
148
+ Logger.log('Socket 服务器已关闭');
149
+ });
150
+ }
151
+ /**
152
+ * 启动 Socket 服务器
153
+ */
154
+ start(port) {
155
+ return new Promise((resolve, reject) => {
156
+ if (!this.server) {
157
+ reject(new Error('Socket 服务器未初始化'));
158
+ return;
159
+ }
160
+ this.port = port;
161
+ this.server.listen(port, () => {
162
+ Logger.log(`Socket server listening on port ${port}`);
163
+ resolve();
164
+ });
165
+ this.server.once('error', (error) => {
166
+ reject(error);
167
+ });
168
+ });
169
+ }
170
+ /**
171
+ * 发送认证响应到指定 token 的客户端
172
+ */
173
+ sendSubmitResponse(token, data) {
174
+ const socket = this.tokenSockets.get(token);
175
+ if (!socket) {
176
+ Logger.warn(`未找到 token 对应的 socket: ${token}`);
177
+ return false;
178
+ }
179
+ try {
180
+ this.sendMessage(socket, {
181
+ type: 'submit-response',
182
+ token,
183
+ data,
184
+ timestamp: Date.now(),
185
+ });
186
+ // 发送后清理 token 映射
187
+ this.tokenSockets.delete(token);
188
+ Logger.log(`已发送认证响应到 token: ${token}`);
189
+ return true;
190
+ }
191
+ catch (error) {
192
+ Logger.error(`发送认证响应失败: ${error}`);
193
+ return false;
194
+ }
195
+ }
196
+ /**
197
+ * 停止 Socket 服务器
198
+ */
199
+ stop() {
200
+ return new Promise((resolve) => {
201
+ // 关闭所有活跃连接
202
+ for (const socket of this.connections) {
203
+ socket.destroy();
204
+ }
205
+ this.connections.clear();
206
+ this.tokenSockets.clear();
207
+ this.messageBuffers.clear();
208
+ // 关闭服务器
209
+ if (this.server) {
210
+ this.server.close(() => {
211
+ Logger.log('Socket 服务器已停止');
212
+ this.server = null;
213
+ resolve();
214
+ });
215
+ }
216
+ else {
217
+ resolve();
218
+ }
219
+ });
220
+ }
221
+ /**
222
+ * 获取当前连接数
223
+ */
224
+ getConnectionCount() {
225
+ return this.connections.size;
226
+ }
227
+ /**
228
+ * 获取服务器端口
229
+ */
230
+ getPort() {
231
+ return this.port;
232
+ }
233
+ }
@@ -70,25 +70,26 @@ export function extractJson(text) {
70
70
  /**
71
71
  * 从 markdown 文本中提取代码块
72
72
  * 遵循 CommonMark 规范,使用三个反引号包围的代码块格式
73
+ * 支持从代码块 info string 中提取组件名,格式:```language:ComponentName
73
74
  * @param text 包含 markdown 代码块的文本
74
75
  * @returns 提取到的代码块数组
75
76
  */
76
77
  export function extractCodeBlocks(text) {
77
78
  const trimmedText = text.trim();
78
- // 匹配 markdown 代码块的正则表达式
79
- // 兼容以下情况:
80
- // - 结尾处没有换行直接关闭 ```
81
- // - Windows 换行 \r\n
82
- // - 语言标记包含短横线(如 "c++" 之类极少数场景,放宽为 [\w-]
83
- // 格式: ```language\ncode\n?```
84
- const codeBlockRegex = /```([\w-]+)?\s*\r?\n([\s\S]*?)\r?\n?\s*```/g;
79
+ // 匹配带有组件名的代码块
80
+ // 格式: ```language:ComponentName\ncode\n```
81
+ // 组件名是可选的,通过冒号分隔
82
+ // 示例: ```tsx:UserCard ```vue:ProductList
83
+ const codeBlockRegex = /```([\w-]+)(?::([A-Z][a-zA-Z0-9]*))?\s*\r?\n([\s\S]*?)\r?\n?\s*```/g;
85
84
  const codeBlocks = [];
86
85
  let match;
87
86
  while ((match = codeBlockRegex.exec(trimmedText)) !== null) {
88
87
  const language = match[1];
89
- const code = match[2].trim();
88
+ const declaredComponentName = match[2]; // 从 info string 中提取的组件名(可能为 undefined)
89
+ const code = match[3].trim();
90
90
  if (code) {
91
- const componentName = extractComponentName(code, language);
91
+ // 优先使用声明的组件名,否则降级到正则提取
92
+ const componentName = declaredComponentName || extractComponentName(code, language);
92
93
  codeBlocks.push({
93
94
  language,
94
95
  code,
@@ -99,6 +100,7 @@ export function extractCodeBlocks(text) {
99
100
  return codeBlocks;
100
101
  }
101
102
  /**
103
+ * @deprecated: 为支持跨框架已废弃,改为从模型返回中提取
102
104
  * 从代码中提取组件名
103
105
  * 支持多种常见的组件定义模式
104
106
  * @param code 代码内容
@@ -46,7 +46,8 @@ export class FileManager {
46
46
  * @param filename - 文件名
47
47
  * @returns 完整的文件路径
48
48
  */
49
- getWorkspaceFilePath(fileKey, nodeId, filename) {
49
+ getWorkspaceFilePath(fileKey, nodeId, filename, opts = { skipParsePath: false }) {
50
+ const { skipParsePath = false } = opts;
50
51
  if (!this.workspaceRoot) {
51
52
  console.error('工作目录根路径未设置,使用默认目录:', this.baseDir);
52
53
  return this.getFilePath(fileKey, nodeId, filename);
@@ -55,6 +56,9 @@ export class FileManager {
55
56
  const cleanFileKey = fileKey.replace(/[^a-zA-Z0-9-_]/g, '_');
56
57
  const cleanNodeId = nodeId ? nodeId.replace(/[^a-zA-Z0-9-_:]/g, '_') : 'root';
57
58
  const cleanFilename = filename.replace(/[^a-zA-Z0-9-_.]/g, '_');
59
+ if (skipParsePath) {
60
+ return path.join(this.workspaceRoot, '.sloth', fileKey, nodeId || 'root', cleanFilename);
61
+ }
58
62
  return path.join(this.workspaceRoot, '.sloth', cleanFileKey, cleanNodeId, cleanFilename);
59
63
  }
60
64
  /**
@@ -76,9 +80,10 @@ export class FileManager {
76
80
  * @param content - 文件内容
77
81
  * @param useWorkspaceDir - 是否使用工作目录
78
82
  */
79
- async saveFile(fileKey, nodeId, filename, content, useWorkspaceDir = false) {
83
+ async saveFile(fileKey, nodeId, filename, content, opts = { useWorkspaceDir: false, skipParsePath: false }) {
84
+ const { useWorkspaceDir = false, ...restOpts } = opts;
80
85
  try {
81
- const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename) : this.getFilePath(fileKey, nodeId, filename);
86
+ const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename, restOpts) : this.getFilePath(fileKey, nodeId, filename);
82
87
  // 确保目录存在
83
88
  await fs.mkdir(path.dirname(filePath), { recursive: true });
84
89
  // 保存文件内容
@@ -98,9 +103,10 @@ export class FileManager {
98
103
  * @param useWorkspaceDir - 是否使用工作目录
99
104
  * @returns Promise<string> - 文件内容,如果文件不存在则返回空字符串
100
105
  */
101
- async loadFile(fileKey, nodeId, filename, useWorkspaceDir = false) {
106
+ async loadFile(fileKey, nodeId, filename, opts = { useWorkspaceDir: false, skipParsePath: false }) {
107
+ const { useWorkspaceDir = false, ...restOpts } = opts;
102
108
  try {
103
- const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename) : this.getFilePath(fileKey, nodeId, filename);
109
+ const filePath = useWorkspaceDir ? this.getWorkspaceFilePath(fileKey, nodeId, filename, restOpts) : this.getFilePath(fileKey, nodeId, filename);
104
110
  Logger.log(`加载文件: ${filePath}, workspaceRoot: ${this.workspaceRoot}`);
105
111
  const content = await fs.readFile(filePath, 'utf-8');
106
112
  return content;
@@ -238,7 +244,7 @@ export class FileManager {
238
244
  * @param groupsData - 分组数据
239
245
  */
240
246
  async saveGroupsData(fileKey, nodeId, groupsData) {
241
- await this.saveFile(fileKey, nodeId, 'groupsData.json', JSON.stringify(groupsData, null, 2), true);
247
+ await this.saveFile(fileKey, nodeId, 'groupsData.json', JSON.stringify(groupsData, null, 2), { useWorkspaceDir: true, skipParsePath: false });
242
248
  }
243
249
  /**
244
250
  * 加载 groupsData 从特定的 nodeId 文件
@@ -248,7 +254,7 @@ export class FileManager {
248
254
  */
249
255
  async loadGroupsData(fileKey, nodeId) {
250
256
  try {
251
- const content = await this.loadFile(fileKey, nodeId, 'groupsData.json', true);
257
+ const content = await this.loadFile(fileKey, nodeId, 'groupsData.json', { useWorkspaceDir: true, skipParsePath: false });
252
258
  return content ? JSON.parse(content) : [];
253
259
  }
254
260
  catch (error) {
@@ -263,7 +269,9 @@ export class FileManager {
263
269
  * @param promptSetting - 提示词设置
264
270
  */
265
271
  async savePromptSetting(fileKey, nodeId, promptSetting) {
266
- await this.saveFile(fileKey, nodeId, 'promptSetting.json', JSON.stringify(promptSetting, null, 2), true);
272
+ fileKey = '.';
273
+ nodeId = '.';
274
+ await this.saveFile(fileKey, nodeId, 'promptSetting.json', JSON.stringify(promptSetting, null, 2), { useWorkspaceDir: true, skipParsePath: true });
267
275
  }
268
276
  /**
269
277
  * 加载 promptSetting 从特定的 nodeId 文件
@@ -272,8 +280,10 @@ export class FileManager {
272
280
  * @returns Promise<any> - 提示词设置,如果文件不存在则返回 null
273
281
  */
274
282
  async loadPromptSetting(fileKey, nodeId) {
283
+ fileKey = '.';
284
+ nodeId = '.';
275
285
  try {
276
- const content = await this.loadFile(fileKey, nodeId, 'promptSetting.json', true);
286
+ const content = await this.loadFile(fileKey, nodeId, 'promptSetting.json', { useWorkspaceDir: true, skipParsePath: true });
277
287
  return content ? JSON.parse(content) : null;
278
288
  }
279
289
  catch (error) {
@@ -288,7 +298,9 @@ export class FileManager {
288
298
  * @param configSetting - 配置设置
289
299
  */
290
300
  async saveConfigSetting(fileKey, nodeId, configSetting) {
291
- await this.saveFile(fileKey, nodeId, 'configSetting.json', JSON.stringify(configSetting, null, 2), true);
301
+ fileKey = '.';
302
+ nodeId = '.';
303
+ await this.saveFile(fileKey, nodeId, 'configSetting.json', JSON.stringify(configSetting, null, 2), { useWorkspaceDir: true, skipParsePath: true });
292
304
  }
293
305
  /**
294
306
  * 加载 configSetting 从特定的 nodeId 文件
@@ -297,8 +309,10 @@ export class FileManager {
297
309
  * @returns Promise<any> - 配置设置,如果文件不存在则返回 null
298
310
  */
299
311
  async loadConfigSetting(fileKey, nodeId) {
312
+ fileKey = '.';
313
+ nodeId = '.';
300
314
  try {
301
- const content = await this.loadFile(fileKey, nodeId, 'configSetting.json', true);
315
+ const content = await this.loadFile(fileKey, nodeId, 'configSetting.json', { useWorkspaceDir: true, skipParsePath: true });
302
316
  return content ? JSON.parse(content) : null;
303
317
  }
304
318
  catch (error) {
@@ -598,5 +612,60 @@ export class FileManager {
598
612
  throw new Error(`无法写入文件: ${componentsPath}`);
599
613
  }
600
614
  }
615
+ /**
616
+ * 根据截图 hash 在整个 .sloth 目录下搜索截图文件
617
+ * @param hash - 截图文件的哈希值
618
+ * @returns Promise<string | null> - 截图文件的完整路径,如果未找到则返回 null
619
+ */
620
+ async findScreenshotByHash(hash) {
621
+ const workspaceRoot = this.getWorkspaceRoot();
622
+ if (!workspaceRoot) {
623
+ Logger.warn('工作目录根路径未设置,无法搜索截图');
624
+ return null;
625
+ }
626
+ const slothDir = path.join(workspaceRoot, '.sloth');
627
+ try {
628
+ await fs.access(slothDir);
629
+ }
630
+ catch {
631
+ Logger.warn('.sloth 目录不存在');
632
+ return null;
633
+ }
634
+ const targetFilename = `${hash}.png`;
635
+ // 递归搜索 .sloth 目录下所有 screenshots 文件夹
636
+ const searchDir = async (dir) => {
637
+ try {
638
+ const entries = await fs.readdir(dir, { withFileTypes: true });
639
+ for (const entry of entries) {
640
+ const fullPath = path.join(dir, entry.name);
641
+ if (entry.isDirectory()) {
642
+ // 如果是 screenshots 目录,检查是否有目标文件
643
+ if (entry.name === 'screenshots') {
644
+ const screenshotPath = path.join(fullPath, targetFilename);
645
+ try {
646
+ await fs.access(screenshotPath);
647
+ return screenshotPath;
648
+ }
649
+ catch {
650
+ // 文件不存在,继续搜索
651
+ }
652
+ }
653
+ else {
654
+ // 递归搜索子目录
655
+ const result = await searchDir(fullPath);
656
+ if (result)
657
+ return result;
658
+ }
659
+ }
660
+ }
661
+ return null;
662
+ }
663
+ catch (error) {
664
+ Logger.error(`搜索目录失败 ${dir}:`, error);
665
+ return null;
666
+ }
667
+ };
668
+ return searchDir(slothDir);
669
+ }
601
670
  }
602
671
  export default FileManager;