cicy-desktop 1.0.8

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.
Files changed (66) hide show
  1. package/.github/workflows/build.yml +85 -0
  2. package/.kiro/steering/dev-workflow.md +166 -0
  3. package/AGENTS.md +247 -0
  4. package/CLAUDE.md +162 -0
  5. package/DOCKER.md +85 -0
  6. package/Dockerfile +46 -0
  7. package/README.md +720 -0
  8. package/TODO-anti-detection.md +326 -0
  9. package/bin/cicy +176 -0
  10. package/bin/preinstall.sh +32 -0
  11. package/copy-to-desktop.sh +26 -0
  12. package/docs/AUTOMATION-API.md +342 -0
  13. package/docs/REQUEST_MONITORING.md +435 -0
  14. package/docs/REST-API-FEATURE.md +155 -0
  15. package/docs/REST-API.md +319 -0
  16. package/docs/feature-distributed-multi-agent.md +555 -0
  17. package/docs/yaml.md +255 -0
  18. package/electron-mcp-fixed.command +134 -0
  19. package/electron-mcp-simple.command +135 -0
  20. package/electron-mcp.command +92 -0
  21. package/generate-openapi.js +158 -0
  22. package/jest.config.js +10 -0
  23. package/jest.setup.global.js +13 -0
  24. package/jest.teardown.global.js +7 -0
  25. package/package.json +75 -0
  26. package/service.sh +164 -0
  27. package/src/config.js +8 -0
  28. package/src/extension/inject.js +135 -0
  29. package/src/main-old.js +837 -0
  30. package/src/main.js +403 -0
  31. package/src/preload-rpc.js +4 -0
  32. package/src/server/args-parser.js +37 -0
  33. package/src/server/electron-setup.js +33 -0
  34. package/src/server/express-app.js +166 -0
  35. package/src/server/logging.js +58 -0
  36. package/src/server/mcp-server.js +53 -0
  37. package/src/server/tool-registry.js +77 -0
  38. package/src/server/ui-routes.js +81 -0
  39. package/src/swagger-ui.html +41 -0
  40. package/src/tools/account-tools.js +194 -0
  41. package/src/tools/automation-tools.js +297 -0
  42. package/src/tools/cdp-tools.js +444 -0
  43. package/src/tools/clipboard-tools.js +180 -0
  44. package/src/tools/download-tools.js +57 -0
  45. package/src/tools/exec-js.js +297 -0
  46. package/src/tools/exec-tools.js +139 -0
  47. package/src/tools/file-tools.js +212 -0
  48. package/src/tools/hook-chatgpt.js +489 -0
  49. package/src/tools/hook-gemini.js +454 -0
  50. package/src/tools/index.js +19 -0
  51. package/src/tools/ipc-bridge.js +31 -0
  52. package/src/tools/ping.js +60 -0
  53. package/src/tools/r-reset.js +28 -0
  54. package/src/tools/screenshot-tools.js +28 -0
  55. package/src/tools/system-tools.js +531 -0
  56. package/src/tools/window-tools.js +882 -0
  57. package/src/ui.html +914 -0
  58. package/src/utils/auth.js +81 -0
  59. package/src/utils/cdp-utils.js +8 -0
  60. package/src/utils/download-manager.js +41 -0
  61. package/src/utils/process-utils.js +185 -0
  62. package/src/utils/snapshot-utils.js +56 -0
  63. package/src/utils/window-monitor.js +605 -0
  64. package/src/utils/window-state.js +137 -0
  65. package/src/utils/window-utils.js +336 -0
  66. package/update-desktop.sh +33 -0
@@ -0,0 +1,605 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const os = require("os");
4
+ const beautify = require("js-beautify");
5
+ const log = require("electron-log");
6
+
7
+ // 存储每个窗口的日志和请求
8
+ const windowLogs = new Map();
9
+ const windowRequests = new Map(); // 已废弃,保留兼容性
10
+ const windowRequestDetails = new Map();
11
+ const windowIndexCounters = new Map();
12
+ const windowBeforeSendRequests = new Map(); // onBeforeSendHeaders 捕获的所有请求
13
+ const windowLoadingFinishedRequests = new Map(); // loadingFinished 捕获的完成请求
14
+
15
+ const MAX_INLINE_SIZE = 1024; // 1KB
16
+
17
+ // 标准化路径:移除特殊字符,保留 .
18
+ function sanitizePath(str) {
19
+ return str.replace(/^\/+|\/+$/g, "").replace(/[^a-zA-Z0-9-_.]/g, "_");
20
+ }
21
+
22
+ // 根据 content-type 获取扩展名
23
+ function getExtFromContentType(contentType, isBinary) {
24
+ if (!contentType) return isBinary ? "bin" : "txt";
25
+
26
+ const mimeMap = {
27
+ "application/json": "json",
28
+ "text/html": "html",
29
+ "text/css": "css",
30
+ "text/javascript": "js",
31
+ "application/javascript": "js",
32
+ "text/plain": "txt",
33
+ "image/png": "png",
34
+ "image/jpeg": "jpg",
35
+ "image/gif": "gif",
36
+ "image/webp": "webp",
37
+ "image/svg+xml": "svg",
38
+ "video/mp4": "mp4",
39
+ "video/webm": "webm",
40
+ "audio/mpeg": "mp3",
41
+ "audio/wav": "wav",
42
+ "application/pdf": "pdf",
43
+ "application/zip": "zip",
44
+ };
45
+
46
+ const mime = contentType.split(";")[0].trim().toLowerCase();
47
+ return mimeMap[mime] || (isBinary ? "bin" : "txt");
48
+ }
49
+
50
+ // 为每个窗口维护文件计数器
51
+ const windowFileCounters = new Map();
52
+
53
+ // 处理数据:二进制或大数据保存到文件
54
+ function handleData(winId, url, data, type, contentType) {
55
+ if (!data) return null;
56
+
57
+ // 检查是否是二进制数据
58
+ const isBinary = Buffer.isBuffer(data) || (data.bytes && Buffer.isBuffer(data.bytes));
59
+
60
+ // 检查大小
61
+ let dataSize = 0;
62
+ let content = data;
63
+
64
+ if (isBinary) {
65
+ const buffer = Buffer.isBuffer(data) ? data : data.bytes;
66
+ dataSize = buffer.length;
67
+ content = buffer;
68
+ } else {
69
+ const str = typeof data === "string" ? data : JSON.stringify(data);
70
+ dataSize = Buffer.byteLength(str);
71
+ content = str;
72
+ }
73
+
74
+ // 判断是否需要格式化的文件类型
75
+ const mime = contentType ? contentType.split(";")[0].trim().toLowerCase() : "";
76
+ const needsPretty =
77
+ mime === "application/json" ||
78
+ mime === "text/javascript" ||
79
+ mime === "application/javascript" ||
80
+ mime === "text/css" ||
81
+ mime === "text/html";
82
+
83
+ // 如果是可格式化类型,或者大于1KB,保存到文件
84
+ if (needsPretty || isBinary || dataSize > MAX_INLINE_SIZE) {
85
+ return saveDataToFile(winId, url, content, type, contentType, isBinary, dataSize);
86
+ }
87
+
88
+ // 其他小数据直接返回
89
+ return data;
90
+ }
91
+
92
+ // 强制保存数据到文件(用于 response/request 对象)
93
+ // type: 'response' | 'response-body' | 'request' | 'post'
94
+ function saveDataToFile(winId, url, content, type, contentType, isBinary, dataSize, timestamp) {
95
+ try {
96
+ // 解析 URL
97
+ const urlObj = new URL(url);
98
+ const domain = sanitizePath(urlObj.hostname);
99
+ let pathname = urlObj.pathname.replace(/^\/+|\/+$/g, "") || "root";
100
+
101
+ // 限制路径长度,避免 ENAMETOOLONG 错误
102
+ const MAX_PATH_LENGTH = 200;
103
+ if (pathname.length > MAX_PATH_LENGTH) {
104
+ // 截断并添加哈希
105
+ const crypto = require("crypto");
106
+ const hash = crypto.createHash("md5").update(pathname).digest("hex").substring(0, 8);
107
+ pathname = pathname.substring(0, MAX_PATH_LENGTH - 10) + "-" + hash;
108
+ }
109
+
110
+ let dir, baseName;
111
+
112
+ // 如果路径以 / 结尾,使用 index
113
+ if (urlObj.pathname.endsWith("/")) {
114
+ dir = path.join(os.homedir(), "request-data", `win-${winId}`, domain, pathname);
115
+ baseName = "index";
116
+ } else {
117
+ // 分离目录和文件名
118
+ const lastSlash = pathname.lastIndexOf("/");
119
+ const dirPath = lastSlash >= 0 ? pathname.substring(0, lastSlash) : "";
120
+ baseName = lastSlash >= 0 ? pathname.substring(lastSlash + 1) : pathname;
121
+
122
+ // 移除原有扩展名
123
+ baseName = baseName.replace(/\.[^.]*$/, "");
124
+
125
+ // 限制文件名长度
126
+ if (baseName.length > 100) {
127
+ const crypto = require("crypto");
128
+ const hash = crypto.createHash("md5").update(baseName).digest("hex").substring(0, 8);
129
+ baseName = baseName.substring(0, 90) + "-" + hash;
130
+ }
131
+
132
+ dir = path.join(os.homedir(), "request-data", `win-${winId}`, domain, dirPath);
133
+ }
134
+
135
+ // 获取计数器
136
+ if (!windowFileCounters.has(winId)) {
137
+ windowFileCounters.set(winId, new Map());
138
+ }
139
+ const counters = windowFileCounters.get(winId);
140
+ const key = `${domain}/${pathname}/${type}`;
141
+ const count = (counters.get(key) || 0) + 1;
142
+ counters.set(key, count);
143
+
144
+ // 根据 content-type 确定扩展名
145
+ const ext = getExtFromContentType(contentType, isBinary);
146
+
147
+ // 确定文件类型标识:header|body 和 req|res
148
+ let part, direction;
149
+ if (type === "response") {
150
+ part = "header";
151
+ direction = "res";
152
+ } else if (type === "response-body") {
153
+ part = "body";
154
+ direction = "res";
155
+ } else if (type === "request") {
156
+ part = "header";
157
+ direction = "req";
158
+ } else if (type === "post") {
159
+ part = "body";
160
+ direction = "req";
161
+ }
162
+
163
+ // 文件名格式:{basename}-{timestamp}-{index}-{header|body}-{req|res}.{ext}
164
+ const ts = timestamp || Date.now();
165
+ const filename = `${baseName}-${ts}-${count}-${part}-${direction}.${ext}`;
166
+
167
+ fs.mkdirSync(dir, { recursive: true });
168
+ const filepath = path.join(dir, filename);
169
+
170
+ if (isBinary) {
171
+ fs.writeFileSync(filepath, content);
172
+ } else {
173
+ let formattedContent = content;
174
+
175
+ // 使用 js-beautify 格式化
176
+ try {
177
+ const beautify = require("js-beautify");
178
+ const contentStr = typeof content === "string" ? content : JSON.stringify(content);
179
+
180
+ if (ext === "json") {
181
+ formattedContent = beautify.js_beautify(contentStr, { indent_size: 2 });
182
+ } else if (ext === "js") {
183
+ formattedContent = beautify.js_beautify(contentStr, { indent_size: 2 });
184
+ } else if (ext === "css") {
185
+ formattedContent = beautify.css(contentStr, { indent_size: 2 });
186
+ } else if (ext === "html") {
187
+ formattedContent = beautify.html(contentStr, { indent_size: 2 });
188
+ } else {
189
+ formattedContent = contentStr;
190
+ }
191
+ } catch (e) {
192
+ // 格式化失败,使用原始内容
193
+ formattedContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
194
+ }
195
+
196
+ fs.writeFileSync(filepath, formattedContent);
197
+ }
198
+
199
+ return {
200
+ __file: filepath,
201
+ __size: dataSize,
202
+ __binary: isBinary,
203
+ };
204
+ } catch (error) {
205
+ // 文件保存失败,返回错误信息
206
+ log.error(`[Window ${winId}] Failed to save file for ${url}:`, error.message);
207
+ return {
208
+ __error: error.message,
209
+ __url: url,
210
+ __size: dataSize,
211
+ };
212
+ }
213
+ }
214
+
215
+ // 保存请求数据到文件(节流)
216
+ let saveTimeout = null;
217
+ function saveRequestsToFile(winId) {
218
+ if (saveTimeout) clearTimeout(saveTimeout);
219
+ saveTimeout = setTimeout(() => {
220
+ try {
221
+ const urls = windowBeforeSendRequests.get(winId) || [];
222
+ const detailsMap = windowLoadingFinishedRequests.get(winId) || new Map();
223
+
224
+ const data = {
225
+ queue: urls,
226
+ map: Object.fromEntries(detailsMap),
227
+ };
228
+
229
+ // 保存到 request-data 目录
230
+ const dir = path.join(os.homedir(), "request-data", `win-${winId}`);
231
+ fs.mkdirSync(dir, { recursive: true });
232
+
233
+ const queueFile = path.join(dir, "queue.json");
234
+ const mapFile = path.join(dir, "map.json");
235
+
236
+ fs.writeFileSync(queueFile, JSON.stringify(urls, null, 2));
237
+ fs.writeFileSync(mapFile, JSON.stringify(Object.fromEntries(detailsMap), null, 2));
238
+ } catch (e) {
239
+ log.error(`Failed to save requests for window ${winId}:`, e.message);
240
+ }
241
+ }, 1000); // 1秒后保存
242
+ }
243
+
244
+ function initWindowMonitoring(win) {
245
+ const winId = win.id;
246
+
247
+ // 初始化队列和计数器
248
+ windowLogs.set(winId, []);
249
+ windowRequests.set(winId, []); // 保留兼容性
250
+ windowRequestDetails.set(winId, new Map());
251
+ windowIndexCounters.set(winId, { log: 0, request: 0 });
252
+ windowBeforeSendRequests.set(winId, []); // URL 队列
253
+ windowLoadingFinishedRequests.set(winId, new Map()); // 详情 Map (key: URL)
254
+
255
+ // 监听 onBeforeSendHeaders - 只记录 URL
256
+ win.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
257
+ const urlQueue = windowBeforeSendRequests.get(winId);
258
+ const detailsMap = windowLoadingFinishedRequests.get(winId);
259
+
260
+ // 队列只存 URL
261
+ if (urlQueue && !urlQueue.includes(details.url)) {
262
+ urlQueue.push(details.url);
263
+ }
264
+
265
+ // 详情存到 Map
266
+ if (detailsMap) {
267
+ const timestamp = Date.now();
268
+ const contentType =
269
+ details.requestHeaders["Content-Type"] || details.requestHeaders["content-type"];
270
+ const postData = handleData(winId, details.url, details.uploadData, "post", contentType);
271
+
272
+ const requestData = {
273
+ timestamp,
274
+ url: details.url,
275
+ method: details.method,
276
+ resourceType: details.resourceType,
277
+ headers: details.requestHeaders,
278
+ postData: postData,
279
+ };
280
+
281
+ // 保存完整 request 到文件
282
+ const requestStr = JSON.stringify(requestData);
283
+ const requestSize = Buffer.byteLength(requestStr);
284
+ const requestFile = saveDataToFile(
285
+ winId,
286
+ details.url,
287
+ requestStr,
288
+ "request",
289
+ "application/json",
290
+ false,
291
+ requestSize,
292
+ timestamp
293
+ );
294
+
295
+ // 如果 URL 已存在,追加到数组;否则创建新条目
296
+ if (detailsMap.has(details.url)) {
297
+ const entry = detailsMap.get(details.url);
298
+ entry.requests.push(requestFile);
299
+ } else {
300
+ detailsMap.set(details.url, {
301
+ requests: [requestFile],
302
+ responses: [],
303
+ });
304
+ }
305
+ saveRequestsToFile(winId);
306
+ }
307
+
308
+ callback({ requestHeaders: details.requestHeaders });
309
+ });
310
+
311
+ // 立即启用网络监控(在任何请求之前)
312
+ try {
313
+ win.webContents.debugger.sendCommand("Network.enable");
314
+ log.info(`[Window ${winId}] Network monitoring enabled`);
315
+ } catch (e) {
316
+ log.error(`[Window ${winId}] Failed to enable Network:`, e);
317
+ }
318
+
319
+ // 监听控制台日志
320
+ win.webContents.on("console-message", (event, level, message, line, sourceId) => {
321
+ const logs = windowLogs.get(winId);
322
+
323
+ const counters = windowIndexCounters.get(winId);
324
+ log.info(
325
+ `[${JSON.stringify({
326
+ timestamp: Date.now(),
327
+ level: ["verbose", "info", "warning", "error"][level] || "log",
328
+ message,
329
+ line,
330
+ source: sourceId,
331
+ })}`
332
+ );
333
+ if (logs && counters) {
334
+ logs.push({
335
+ index: ++counters.log,
336
+ timestamp: Date.now(),
337
+ level: ["verbose", "info", "warning", "error"][level] || "log",
338
+ message,
339
+ line,
340
+ source: sourceId,
341
+ });
342
+ }
343
+ });
344
+
345
+ // 临时存储 requestId -> url 映射和响应信息
346
+ const requestIdToUrl = new Map();
347
+ const requestIdToResponse = new Map();
348
+
349
+ // 监听网络请求
350
+ win.webContents.debugger.on("message", async (event, method, params) => {
351
+ // responseReceived: 记录 requestId -> url 映射和响应头
352
+ if (method === "Network.responseReceived") {
353
+ const url = params.response.url;
354
+ requestIdToUrl.set(params.requestId, url);
355
+ requestIdToResponse.set(params.requestId, {
356
+ status: params.response.status,
357
+ statusText: params.response.statusText,
358
+ headers: params.response.headers,
359
+ mimeType: params.response.mimeType,
360
+ });
361
+ }
362
+
363
+ // loadingFinished: 更新 Map 中的 response
364
+ if (method === "Network.loadingFinished") {
365
+ const url = requestIdToUrl.get(params.requestId);
366
+ const responseInfo = requestIdToResponse.get(params.requestId);
367
+
368
+ if (url && responseInfo) {
369
+ const loadingFinishedMap = windowLoadingFinishedRequests.get(winId);
370
+ const entry = loadingFinishedMap?.get(url);
371
+ if (entry) {
372
+ // 尝试获取 response body
373
+ let responseBody = null;
374
+ try {
375
+ const result = await win.webContents.debugger.sendCommand("Network.getResponseBody", {
376
+ requestId: params.requestId,
377
+ });
378
+ if (result.body) {
379
+ responseBody = handleData(
380
+ winId,
381
+ url,
382
+ result.base64Encoded ? Buffer.from(result.body, "base64") : result.body,
383
+ "response-body",
384
+ responseInfo.mimeType
385
+ );
386
+ }
387
+ } catch (e) {
388
+ // 某些请求无法获取 body(如 304, 204 等)
389
+ }
390
+
391
+ const responseData = {
392
+ timestamp: Date.now(),
393
+ status: responseInfo.status,
394
+ statusText: responseInfo.statusText,
395
+ headers: responseInfo.headers,
396
+ mimeType: responseInfo.mimeType,
397
+ encodedDataLength: params.encodedDataLength,
398
+ body: responseBody,
399
+ };
400
+
401
+ // 强制保存完整 response 到文件
402
+ const responseStr = JSON.stringify(responseData);
403
+ const responseSize = Buffer.byteLength(responseStr);
404
+ const responseFile = saveDataToFile(
405
+ winId,
406
+ url,
407
+ responseStr,
408
+ "response",
409
+ "application/json",
410
+ false,
411
+ responseSize,
412
+ responseData.timestamp
413
+ );
414
+
415
+ // 追加到 responses 数组(只保留文件引用)
416
+ entry.responses.push(responseFile);
417
+ saveRequestsToFile(winId);
418
+ }
419
+ requestIdToUrl.delete(params.requestId);
420
+ requestIdToResponse.delete(params.requestId);
421
+ }
422
+ }
423
+
424
+ // 保留旧的 requestWillBeSent 逻辑以兼容现有代码
425
+ if (method === "Network.requestWillBeSent") {
426
+ const requests = windowRequests.get(winId);
427
+ const details = windowRequestDetails.get(winId);
428
+ const counters = windowIndexCounters.get(winId);
429
+ if (requests && counters && details) {
430
+ const url = new URL(params.request.url);
431
+ const index = ++counters.request;
432
+ const postData = params.request.postData;
433
+ const postDataSize = postData ? Buffer.byteLength(postData, "utf8") : 0;
434
+
435
+ // 打印包含 __vid 的请求
436
+ if (params.request.url.includes("__vid")) {
437
+ log.info("\n=== REQUEST WITH __vid DETECTED ===");
438
+ log.info("URL:", params.request.url);
439
+ log.info("Method:", params.request.method);
440
+ log.info("Type:", params.type);
441
+ log.info("===================================\n");
442
+ }
443
+
444
+ requests.push({
445
+ index,
446
+ timestamp: Date.now(),
447
+ requestId: params.requestId,
448
+ url: params.request.url,
449
+ domain: url.hostname,
450
+ path: url.pathname + url.search,
451
+ method: params.request.method,
452
+ type: params.type,
453
+ mimeType: params.request.headers["Content-Type"] || params.type,
454
+ postDataSize,
455
+ });
456
+
457
+ // 存储详细信息
458
+ const detailData = {
459
+ requestId: params.requestId,
460
+ url: params.request.url,
461
+ method: params.request.method,
462
+ headers: params.request.headers,
463
+ postDataSize,
464
+ type: params.type,
465
+ };
466
+
467
+ // 如果 postData 太大,保存到文件
468
+ if (postData && postDataSize > MAX_INLINE_SIZE) {
469
+ const tmpDir = path.join(os.tmpdir(), "electron-mcp", `win-${winId}`);
470
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
471
+ const postDataFile = path.join(tmpDir, `post-${params.requestId}.dat`);
472
+ fs.writeFileSync(postDataFile, postData);
473
+ detailData.postDataFile = postDataFile;
474
+ } else if (postData) {
475
+ detailData.postData = postData;
476
+ }
477
+
478
+ details.set(index, detailData);
479
+ details.set(params.requestId, { url: params.request.url, method: params.request.method });
480
+ }
481
+ }
482
+
483
+ if (method === "Network.responseReceived") {
484
+ const details = windowRequestDetails.get(winId);
485
+ if (details) {
486
+ const info = details.get(params.requestId);
487
+ if (info) {
488
+ info.mimeType = params.response.mimeType;
489
+ info.responseHeaders = params.response.headers;
490
+ info.status = params.response.status;
491
+ }
492
+ }
493
+ }
494
+
495
+ if (method === "Network.loadingFinished") {
496
+ const details = windowRequestDetails.get(winId);
497
+ const requests = windowRequests.get(winId);
498
+ if (!details || !requests) return;
499
+ const info = details.get(params.requestId);
500
+ if (!info) return;
501
+
502
+ // 旧的文件保存逻辑已删除,现在使用新的 handleData 逻辑
503
+
504
+ win.webContents.session.flushStorageData();
505
+ }
506
+ });
507
+
508
+ // 页面重载时清空队列并重置计数器(注释掉,避免误清空)
509
+ // win.webContents.on("did-start-loading", () => {
510
+ // windowLogs.set(winId, []);
511
+ // windowRequests.set(winId, []);
512
+ // windowRequestDetails.set(winId, new Map());
513
+ // windowIndexCounters.set(winId, { log: 0, request: 0 });
514
+ // });
515
+
516
+ // 窗口关闭时清理
517
+ win.on("closed", () => {
518
+ // 清理临时文件
519
+ const tmpDir = path.join(os.tmpdir(), "electron-mcp", `win-${winId}`);
520
+ if (fs.existsSync(tmpDir)) {
521
+ fs.rmSync(tmpDir, { recursive: true, force: true });
522
+ }
523
+
524
+ windowLogs.delete(winId);
525
+ windowRequests.delete(winId);
526
+ windowRequestDetails.delete(winId);
527
+ windowIndexCounters.delete(winId);
528
+ });
529
+ }
530
+
531
+ function getConsoleLogs(winId) {
532
+ return windowLogs.get(winId) || [];
533
+ }
534
+
535
+ function getRequests(winId) {
536
+ return windowRequests.get(winId) || [];
537
+ }
538
+
539
+ function getBeforeSendRequests(winId) {
540
+ return windowBeforeSendRequests.get(winId) || [];
541
+ }
542
+
543
+ function getLoadingFinishedRequests(winId) {
544
+ const map = windowLoadingFinishedRequests.get(winId);
545
+ return map ? Array.from(map.values()) : [];
546
+ }
547
+
548
+ function getRequestDetailByUrl(winId, url) {
549
+ const map = windowLoadingFinishedRequests.get(winId);
550
+ return map ? map.get(url) : null;
551
+ }
552
+
553
+ function clearRequests(winId) {
554
+ windowLogs.set(winId, []);
555
+ windowRequests.set(winId, []);
556
+ windowBeforeSendRequests.set(winId, []);
557
+ windowLoadingFinishedRequests.set(winId, new Map());
558
+ windowRequestDetails.set(winId, new Map());
559
+ windowIndexCounters.set(winId, { log: 0, request: 0 });
560
+ }
561
+
562
+ function getRequestDetail(winId, index) {
563
+ // 优先从 .info.txt 文件加载
564
+ const captureBase = process.env.TEST === "TRUE" ? "CaptureDataTest" : "CaptureData";
565
+ const captureDir = path.join(os.homedir(), "Desktop", captureBase);
566
+
567
+ // 递归查找 .info.txt 文件
568
+ function findInfoFile(dir) {
569
+ if (!fs.existsSync(dir)) return null;
570
+
571
+ const files = fs.readdirSync(dir, { withFileTypes: true });
572
+ for (const file of files) {
573
+ const fullPath = path.join(dir, file.name);
574
+ if (file.isDirectory()) {
575
+ const found = findInfoFile(fullPath);
576
+ if (found) return found;
577
+ } else if (file.name.endsWith(".info.txt")) {
578
+ try {
579
+ const content = fs.readFileSync(fullPath, "utf8");
580
+ const data = JSON.parse(content);
581
+ if (data.index === index) return data;
582
+ } catch (e) {}
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+
588
+ const detail = findInfoFile(captureDir);
589
+ if (detail) return detail;
590
+
591
+ // 回退到内存数据
592
+ const details = windowRequestDetails.get(winId);
593
+ return details ? details.get(index) : null;
594
+ }
595
+
596
+ module.exports = {
597
+ initWindowMonitoring,
598
+ getConsoleLogs,
599
+ getRequests,
600
+ getBeforeSendRequests,
601
+ getLoadingFinishedRequests,
602
+ getRequestDetail,
603
+ getRequestDetailByUrl,
604
+ clearRequests,
605
+ };