sloth-d2c-mcp 1.0.4-beta87 → 1.0.4-beta90
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/cli/run.js +15 -0
- package/dist/build/index.js +106 -1
- package/dist/build/plugin/index.js +4 -0
- package/dist/build/plugin/loader.js +320 -0
- package/dist/build/plugin/manager.js +127 -0
- package/dist/build/plugin/types.js +1 -0
- package/dist/build/server.js +37 -23
- package/dist/build/socket-client.js +5 -2
- package/dist/build/socket-server.js +27 -0
- package/dist/build/utils/file-manager.js +28 -1
- package/dist/build/utils/utils.js +6 -0
- package/dist/interceptor-web/dist/build-report.json +4 -4
- package/dist/interceptor-web/dist/detail.html +1 -1
- package/dist/interceptor-web/dist/index.html +1 -1
- package/package.json +3 -3
package/dist/build/server.js
CHANGED
|
@@ -32,10 +32,12 @@ const upload = multer({
|
|
|
32
32
|
});
|
|
33
33
|
// 导入默认提示词
|
|
34
34
|
import { chunkOptimizeCodePrompt, aggregationOptimizeCodePrompt, finalOptimizeCodePrompt, chunkOptimizeCodePromptVue, aggregationOptimizeCodePromptVue, finalOptimizeCodePromptVue, noSamplingAggregationPrompt, noSamplingAggregationPromptVue, } from 'sloth-d2c-node/convert';
|
|
35
|
+
import { getParam } from './utils/utils.js';
|
|
36
|
+
import { listWorkspacePlugins } from './plugin/loader.js';
|
|
35
37
|
// 保存 HTTP 服务器实例
|
|
36
|
-
let httpServer = null;
|
|
38
|
+
export let httpServer = null;
|
|
37
39
|
// 保存 Socket 服务器实例
|
|
38
|
-
let socketServer = null;
|
|
40
|
+
export let socketServer = null;
|
|
39
41
|
// 管理所有活跃的传输对象,按 sessionId 分类
|
|
40
42
|
const transports = {
|
|
41
43
|
streamable: {}, // 流式 HTTP 传输
|
|
@@ -196,6 +198,14 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
196
198
|
next();
|
|
197
199
|
});
|
|
198
200
|
// app.use(express.json());
|
|
201
|
+
app.use((req, _res, next) => {
|
|
202
|
+
// 通过referer取url上面的uuid
|
|
203
|
+
const uuid = getParam(req.headers.referer || '', 'token') || '';
|
|
204
|
+
req.uuid = uuid;
|
|
205
|
+
req.fileManager = fileManager.withUUID(uuid);
|
|
206
|
+
req.workspaceRoot = req.fileManager.getWorkspaceRoot();
|
|
207
|
+
next();
|
|
208
|
+
});
|
|
199
209
|
// 处理流式 HTTP 的 POST 请求,支持会话复用和初始化
|
|
200
210
|
app.post('/mcp', async (req, res) => {
|
|
201
211
|
Logger.log('Received StreamableHTTP request');
|
|
@@ -329,15 +339,16 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
329
339
|
const fileKey = req.query.fileKey;
|
|
330
340
|
const nodeId = req.query.nodeId;
|
|
331
341
|
const framework = req.query.framework;
|
|
342
|
+
const plugins = await listWorkspacePlugins(req.workspaceRoot);
|
|
332
343
|
if (fileKey) {
|
|
333
344
|
// 如果提供了 fileKey,返回该 fileKey 的特定配置
|
|
334
345
|
const globalConfig = await configManager.load();
|
|
335
346
|
// const fileConfig = globalConfig.fileConfigs?.[fileKey] || {}
|
|
336
|
-
const fileConfig = (await fileManager.loadConfigSetting(fileKey, nodeId)) || {};
|
|
347
|
+
const fileConfig = (await req.fileManager.loadConfigSetting(fileKey, nodeId)) || {};
|
|
337
348
|
// 从 fileManager 按 nodeId 加载 groupsData 和 promptSetting
|
|
338
349
|
// const fileManager = new FileManager('d2c-mcp')
|
|
339
|
-
const groupsData = await fileManager.loadGroupsData(fileKey, nodeId);
|
|
340
|
-
const savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
|
|
350
|
+
const groupsData = await req.fileManager.loadGroupsData(fileKey, nodeId);
|
|
351
|
+
const savedPromptSetting = await req.fileManager.loadPromptSetting(fileKey, nodeId);
|
|
341
352
|
let curFramework = fileConfig?.convertSetting?.framework;
|
|
342
353
|
// 获取框架列表
|
|
343
354
|
const frameworks = await configManager.getFrameworks();
|
|
@@ -393,6 +404,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
393
404
|
defaultFramework: globalConfig.defaultFramework || 'react',
|
|
394
405
|
fileKey: fileKey,
|
|
395
406
|
groupsData: groupsData,
|
|
407
|
+
plugins,
|
|
396
408
|
},
|
|
397
409
|
});
|
|
398
410
|
}
|
|
@@ -433,6 +445,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
433
445
|
imageSetting: configStorage.imageSetting || {},
|
|
434
446
|
promptSetting: promptSetting,
|
|
435
447
|
frameworks: frameworks,
|
|
448
|
+
plugins,
|
|
436
449
|
},
|
|
437
450
|
});
|
|
438
451
|
}
|
|
@@ -681,11 +694,11 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
681
694
|
// 使用 fileManager 按 nodeId 保存 groupsData 和 promptSetting
|
|
682
695
|
// const fileManager = new FileManager('d2c-mcp')
|
|
683
696
|
if (fileKey && value.groupsData && Array.isArray(value.groupsData)) {
|
|
684
|
-
await fileManager.saveGroupsData(fileKey, nodeId, value.groupsData);
|
|
697
|
+
await req.fileManager.saveGroupsData(fileKey, nodeId, value.groupsData);
|
|
685
698
|
Logger.log(`已保存 groupsData 到 fileKey "${fileKey}", nodeId "${nodeId}":`, value.groupsData.length, '个分组');
|
|
686
699
|
}
|
|
687
700
|
if (fileKey && value.promptSetting) {
|
|
688
|
-
await fileManager.savePromptSetting(fileKey, nodeId, value.promptSetting);
|
|
701
|
+
await req.fileManager.savePromptSetting(fileKey, nodeId, value.promptSetting);
|
|
689
702
|
Logger.log(`已保存 promptSetting 到 fileKey "${fileKey}", nodeId "${nodeId}"`);
|
|
690
703
|
}
|
|
691
704
|
// 如果有 MCP 配置,更新全局配置
|
|
@@ -766,9 +779,9 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
766
779
|
/**
|
|
767
780
|
* 获取项目根目录
|
|
768
781
|
*/
|
|
769
|
-
function getProjectRoot() {
|
|
782
|
+
function getProjectRoot(fs) {
|
|
770
783
|
try {
|
|
771
|
-
const projectPath =
|
|
784
|
+
const projectPath = fs.getWorkspaceRoot();
|
|
772
785
|
return projectPath || './';
|
|
773
786
|
}
|
|
774
787
|
catch (error) {
|
|
@@ -782,7 +795,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
782
795
|
app.get('/getProjectFiles', async (req, res) => {
|
|
783
796
|
try {
|
|
784
797
|
const { directory = '' } = req.query;
|
|
785
|
-
const projectPath = getProjectRoot();
|
|
798
|
+
const projectPath = getProjectRoot(req.fileManager);
|
|
786
799
|
const targetPath = path.join(projectPath, directory);
|
|
787
800
|
Logger.log(`获取项目文件树: ${targetPath}`);
|
|
788
801
|
// 排除的文件扩展名(静态资源和配置文件)
|
|
@@ -912,12 +925,12 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
912
925
|
return;
|
|
913
926
|
}
|
|
914
927
|
Logger.log(`批量分析 ${filePaths.length} 个项目文件`);
|
|
915
|
-
const projectPath = getProjectRoot();
|
|
928
|
+
const projectPath = getProjectRoot(req.fileManager);
|
|
916
929
|
// 尝试加载保存的提示词设置
|
|
917
930
|
let savedPromptSetting = null;
|
|
918
931
|
if (fileKey && nodeId) {
|
|
919
932
|
try {
|
|
920
|
-
savedPromptSetting = await fileManager.loadPromptSetting(fileKey, nodeId);
|
|
933
|
+
savedPromptSetting = await req.fileManager.loadPromptSetting(fileKey, nodeId);
|
|
921
934
|
Logger.log(`已加载提示词设置: fileKey=${fileKey}, nodeId=${nodeId}`);
|
|
922
935
|
}
|
|
923
936
|
catch (error) {
|
|
@@ -1109,7 +1122,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1109
1122
|
}
|
|
1110
1123
|
Logger.log(`准备保存 ${components.length} 个组件`);
|
|
1111
1124
|
// 读取现有组件
|
|
1112
|
-
const existingComponents = await fileManager.loadComponentsDatabase();
|
|
1125
|
+
const existingComponents = await req.fileManager.loadComponentsDatabase();
|
|
1113
1126
|
const existingMap = new Map(existingComponents.map((c) => [c.id, c]));
|
|
1114
1127
|
let addedCount = 0;
|
|
1115
1128
|
let updatedCount = 0;
|
|
@@ -1128,7 +1141,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1128
1141
|
}
|
|
1129
1142
|
const allComponents = Array.from(existingMap.values());
|
|
1130
1143
|
// 保存到文件(带备份)
|
|
1131
|
-
await fileManager.saveComponentsDatabase(allComponents);
|
|
1144
|
+
await req.fileManager.saveComponentsDatabase(allComponents);
|
|
1132
1145
|
Logger.log(`✅ 成功保存:新增 ${addedCount} 个,更新 ${updatedCount} 个,共 ${allComponents.length} 个组件`);
|
|
1133
1146
|
res.json({
|
|
1134
1147
|
success: true,
|
|
@@ -1154,7 +1167,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1154
1167
|
app.get('/scanComponents', async (req, res) => {
|
|
1155
1168
|
try {
|
|
1156
1169
|
const { includePaths, excludePaths } = req.query;
|
|
1157
|
-
const projectPath = getProjectRoot();
|
|
1170
|
+
const projectPath = getProjectRoot(req.fileManager);
|
|
1158
1171
|
// 读取项目组件(从 .sloth/components.json)
|
|
1159
1172
|
let projectComponents = [];
|
|
1160
1173
|
const slothPath = path.join(projectPath, '.sloth', 'components.json');
|
|
@@ -1230,7 +1243,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1230
1243
|
const currentScreenshotPath = path.join(tempDir, `suggest_${Date.now()}.png`);
|
|
1231
1244
|
await fs.promises.writeFile(currentScreenshotPath, currentScreenshotFile.buffer);
|
|
1232
1245
|
// 从 components.json 加载所有组件
|
|
1233
|
-
const components = await fileManager.loadComponentsDatabase();
|
|
1246
|
+
const components = await req.fileManager.loadComponentsDatabase();
|
|
1234
1247
|
if (!components || components.length === 0) {
|
|
1235
1248
|
Logger.log('未找到已保存的组件,无法生成建议');
|
|
1236
1249
|
await fs.promises.unlink(currentScreenshotPath).catch(() => { });
|
|
@@ -1250,7 +1263,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1250
1263
|
continue;
|
|
1251
1264
|
}
|
|
1252
1265
|
// 根据 signature 搜索截图文件
|
|
1253
|
-
const screenshotPath = await fileManager.findScreenshotByHash(component.signature);
|
|
1266
|
+
const screenshotPath = await req.fileManager.findScreenshotByHash(component.signature);
|
|
1254
1267
|
if (!screenshotPath) {
|
|
1255
1268
|
Logger.log(`组件 ${component.name} 的截图未找到 (hash: ${component.signature}),跳过`);
|
|
1256
1269
|
continue;
|
|
@@ -1334,7 +1347,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1334
1347
|
}
|
|
1335
1348
|
Logger.log(`接收到截图上传请求: fileKey=${fileKey}, nodeId=${nodeId}, hash=${hash}`);
|
|
1336
1349
|
// 保存截图到 .sloth/{fileKey}/{nodeId}/screenshots/{hash}.png
|
|
1337
|
-
await fileManager.saveScreenshot(fileKey, nodeId, hash, file.buffer);
|
|
1350
|
+
await req.fileManager.saveScreenshot(fileKey, nodeId, hash, file.buffer);
|
|
1338
1351
|
Logger.log(`截图上传成功: ${hash}.png`);
|
|
1339
1352
|
res.json({
|
|
1340
1353
|
success: true,
|
|
@@ -1355,7 +1368,7 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1355
1368
|
app.get('/listDesignSnapshots', async (req, res) => {
|
|
1356
1369
|
try {
|
|
1357
1370
|
Logger.log('获取设计稿快照列表');
|
|
1358
|
-
const workspaceRoot = getProjectRoot();
|
|
1371
|
+
const workspaceRoot = getProjectRoot(req.fileManager);
|
|
1359
1372
|
Logger.log('workspaceRoot:', workspaceRoot);
|
|
1360
1373
|
if (!workspaceRoot) {
|
|
1361
1374
|
res.json({
|
|
@@ -1550,8 +1563,8 @@ export async function startHttpServer(port = PORT, mcpServer, configManagerInsta
|
|
|
1550
1563
|
return;
|
|
1551
1564
|
}
|
|
1552
1565
|
// 2. 加载新旧 groupsData
|
|
1553
|
-
const oldGroupsData = (await fileManager.loadGroupsData(oldFileKey, oldNodeId)) || [];
|
|
1554
|
-
const newGroupsData = (await fileManager.loadGroupsData(newFileKey, newNodeId)) || [];
|
|
1566
|
+
const oldGroupsData = (await req.fileManager.loadGroupsData(oldFileKey, oldNodeId)) || [];
|
|
1567
|
+
const newGroupsData = (await req.fileManager.loadGroupsData(newFileKey, newNodeId)) || [];
|
|
1555
1568
|
// 3. 执行 HTML 差异分析
|
|
1556
1569
|
// @ts-ignore
|
|
1557
1570
|
const { diffLines } = await import('diff');
|
|
@@ -1770,8 +1783,9 @@ export async function getUserInput(payload) {
|
|
|
1770
1783
|
// 连接到 Socket 服务器
|
|
1771
1784
|
await socketClient.connect();
|
|
1772
1785
|
Logger.log('Socket 客户端已连接');
|
|
1773
|
-
|
|
1774
|
-
|
|
1786
|
+
const workspaceRoot = fileManager.getWorkspaceRoot();
|
|
1787
|
+
// 注册 token 并等待响应,同时传递 extra 数据给主进程
|
|
1788
|
+
const responsePromise = socketClient.registerToken(token, { workspaceRoot });
|
|
1775
1789
|
// 打开浏览器
|
|
1776
1790
|
await open(authUrl);
|
|
1777
1791
|
// 等待认证响应
|
|
@@ -115,8 +115,10 @@ export class SocketClient {
|
|
|
115
115
|
}
|
|
116
116
|
/**
|
|
117
117
|
* 注册 token,等待认证响应
|
|
118
|
+
* @param token 认证 token
|
|
119
|
+
* @param extra 额外数据(可选),如 { workspaceRoot: string, ... }
|
|
118
120
|
*/
|
|
119
|
-
registerToken(token) {
|
|
121
|
+
registerToken(token, extra) {
|
|
120
122
|
return new Promise((resolve, reject) => {
|
|
121
123
|
// 设置超时(可选,根据需求调整)
|
|
122
124
|
const timeout = setTimeout(() => {
|
|
@@ -132,9 +134,10 @@ export class SocketClient {
|
|
|
132
134
|
this.send({
|
|
133
135
|
type: 'register-token',
|
|
134
136
|
token,
|
|
137
|
+
extra,
|
|
135
138
|
timestamp: Date.now(),
|
|
136
139
|
});
|
|
137
|
-
Logger.log(`已注册 token: ${token},等待认证响应...`);
|
|
140
|
+
Logger.log(`已注册 token: ${token},extra: ${JSON.stringify(extra)},等待认证响应...`);
|
|
138
141
|
});
|
|
139
142
|
}
|
|
140
143
|
/**
|
|
@@ -8,6 +8,7 @@ export class SocketServer {
|
|
|
8
8
|
server = null;
|
|
9
9
|
connections = new Set();
|
|
10
10
|
tokenSockets = new Map(); // token -> socket 映射
|
|
11
|
+
tokenExtras = new Map(); // token -> extra 数据映射
|
|
11
12
|
messageBuffers = new Map(); // socket -> 消息缓冲区
|
|
12
13
|
port = 0;
|
|
13
14
|
constructor() {
|
|
@@ -42,6 +43,7 @@ export class SocketServer {
|
|
|
42
43
|
for (const [token, sock] of this.tokenSockets.entries()) {
|
|
43
44
|
if (sock === socket) {
|
|
44
45
|
this.tokenSockets.delete(token);
|
|
46
|
+
this.tokenExtras.delete(token);
|
|
45
47
|
Logger.log(`清理 token: ${token}`);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
@@ -95,6 +97,11 @@ export class SocketServer {
|
|
|
95
97
|
// 注册 token,建立 token -> socket 映射
|
|
96
98
|
if (message.token) {
|
|
97
99
|
this.tokenSockets.set(message.token, socket);
|
|
100
|
+
// 保存 extra 数据(如果提供)
|
|
101
|
+
if (message.extra && typeof message.extra === 'object') {
|
|
102
|
+
this.tokenExtras.set(message.token, message.extra);
|
|
103
|
+
Logger.log(`已保存 extra 数据: ${message.token} -> ${JSON.stringify(message.extra)}`);
|
|
104
|
+
}
|
|
98
105
|
Logger.log(`已注册 token: ${message.token} -> ${clientId}`);
|
|
99
106
|
this.sendMessage(socket, {
|
|
100
107
|
type: 'token-registered',
|
|
@@ -204,6 +211,7 @@ export class SocketServer {
|
|
|
204
211
|
}
|
|
205
212
|
this.connections.clear();
|
|
206
213
|
this.tokenSockets.clear();
|
|
214
|
+
this.tokenExtras.clear();
|
|
207
215
|
this.messageBuffers.clear();
|
|
208
216
|
// 关闭服务器
|
|
209
217
|
if (this.server) {
|
|
@@ -230,4 +238,23 @@ export class SocketServer {
|
|
|
230
238
|
getPort() {
|
|
231
239
|
return this.port;
|
|
232
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* 根据 token 获取对应的 extra 数据
|
|
243
|
+
*/
|
|
244
|
+
getTokenExtra(token) {
|
|
245
|
+
return this.tokenExtras.get(token);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* 根据 token 获取 extra 中的特定字段
|
|
249
|
+
*/
|
|
250
|
+
getTokenExtraField(token, field) {
|
|
251
|
+
const extra = this.tokenExtras.get(token);
|
|
252
|
+
return extra?.[field];
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* 获取所有已注册的 token extra 映射
|
|
256
|
+
*/
|
|
257
|
+
getAllTokenExtras() {
|
|
258
|
+
return new Map(this.tokenExtras);
|
|
259
|
+
}
|
|
233
260
|
}
|
|
@@ -2,6 +2,7 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import envPaths from 'env-paths';
|
|
4
4
|
import { Logger } from '../utils/logger.js';
|
|
5
|
+
import { httpServer, socketServer } from '../server.js';
|
|
5
6
|
/**
|
|
6
7
|
* 通用文件管理器
|
|
7
8
|
* 可以保存任何类型的文件,使用fileKey和nodeId组织目录结构
|
|
@@ -9,11 +10,23 @@ import { Logger } from '../utils/logger.js';
|
|
|
9
10
|
export class FileManager {
|
|
10
11
|
paths; // 应用路径配置
|
|
11
12
|
baseDir; // 基础存储目录
|
|
12
|
-
|
|
13
|
+
_workspaceRoot = null; // MCP 工作目录根路径
|
|
14
|
+
_uuid = '';
|
|
13
15
|
constructor(appName) {
|
|
14
16
|
this.paths = envPaths(appName);
|
|
15
17
|
this.baseDir = path.join(this.paths.data, 'files');
|
|
16
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
|
+
}
|
|
17
30
|
/**
|
|
18
31
|
* 设置工作目录根路径(用于 MCP 工作项目)
|
|
19
32
|
* @param rootPath - 工作目录根路径
|
|
@@ -786,5 +799,19 @@ export class FileManager {
|
|
|
786
799
|
return [];
|
|
787
800
|
}
|
|
788
801
|
}
|
|
802
|
+
/**
|
|
803
|
+
* 创建一个临时的 FileManager 代理,仅在本次链式调用中使用指定的 uuid
|
|
804
|
+
* 不会影响原始实例的 _uuid 值
|
|
805
|
+
* @param uuid - 临时使用的 uuid
|
|
806
|
+
* @returns 带有临时 uuid 的 FileManager 代理对象
|
|
807
|
+
*/
|
|
808
|
+
withUUID(uuid) {
|
|
809
|
+
if (!uuid) {
|
|
810
|
+
return this;
|
|
811
|
+
}
|
|
812
|
+
const proxy = Object.create(this);
|
|
813
|
+
proxy._uuid = uuid;
|
|
814
|
+
return proxy;
|
|
815
|
+
}
|
|
789
816
|
}
|
|
790
817
|
export default FileManager;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"buildTime": "
|
|
2
|
+
"buildTime": "2026-01-07T02:01:53.150Z",
|
|
3
3
|
"mode": "build",
|
|
4
4
|
"pages": {
|
|
5
5
|
"main": {
|
|
6
6
|
"file": "index.html",
|
|
7
|
-
"size":
|
|
7
|
+
"size": 1643352,
|
|
8
8
|
"sizeFormatted": "1.57 MB"
|
|
9
9
|
},
|
|
10
10
|
"detail": {
|
|
@@ -13,6 +13,6 @@
|
|
|
13
13
|
"sizeFormatted": "275.19 KB"
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
|
-
"totalSize":
|
|
17
|
-
"totalSizeFormatted": "1.
|
|
16
|
+
"totalSize": 1925151,
|
|
17
|
+
"totalSizeFormatted": "1.84 MB"
|
|
18
18
|
}
|