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.
- package/dist/build/core/prompt-builder.js +124 -20
- package/dist/build/core/sampling.js +5 -23
- package/dist/build/index.js +185 -26
- package/dist/build/server.js +366 -253
- package/dist/build/socket-client.js +163 -0
- package/dist/build/socket-server.js +233 -0
- package/dist/build/utils/extract.js +11 -9
- package/dist/build/utils/file-manager.js +80 -11
- package/dist/build/utils/tj.js +139 -0
- package/dist/build/utils/utils.js +5 -5
- package/dist/interceptor-web/dist/build-report.json +7 -7
- package/dist/interceptor-web/dist/detail.html +1 -1
- package/dist/interceptor-web/dist/index.html +1 -1
- package/package.json +5 -4
- package/dist/build/component-mapping/adapter-manager.js +0 -45
- package/dist/build/component-mapping/adapters/base-adapter.js +0 -137
- package/dist/build/component-mapping/adapters/ios-adapter.js +0 -697
- package/dist/build/component-mapping/adapters/web-adapter.js +0 -536
- package/dist/build/component-mapping/index.js +0 -32
- package/dist/build/component-mapping/storage.js +0 -142
- package/dist/build/component-mapping/types.js +0 -4
|
@@ -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
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
|
|
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
|
|
88
|
+
const declaredComponentName = match[2]; // 从 info string 中提取的组件名(可能为 undefined)
|
|
89
|
+
const code = match[3].trim();
|
|
90
90
|
if (code) {
|
|
91
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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;
|