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,882 @@
1
+ const { BrowserWindow } = require("electron");
2
+ const log = require("electron-log");
3
+ const { z } = require("zod");
4
+ const {
5
+ initWindowMonitoring,
6
+ getConsoleLogs,
7
+ getRequests,
8
+ getRequestDetail,
9
+ getBeforeSendRequests,
10
+ getRequestDetailByUrl,
11
+ getLoadingFinishedRequests,
12
+ clearRequests,
13
+ } = require("../utils/window-monitor");
14
+ const { captureSnapshot } = require("../utils/snapshot-utils");
15
+ const { createWindow, getWindowInfo } = require("../utils/window-utils");
16
+ const { config } = require("../config");
17
+
18
+ function registerTools(registerTool) {
19
+ registerTool(
20
+ "get_windows",
21
+ `获取当前所有 Electron 窗口的实时状态列表。返回每个窗口的详细信息,是窗口管理和自动化操作的基础工具。
22
+
23
+ 返回信息包括:
24
+ - id: 窗口的唯一标识符(调用其他 invoke_window 工具时必需)
25
+ - title/url: 窗口当前的标题和网址
26
+ - debuggerIsAttached: 调试器是否已附加
27
+ - isActive/isVisible: 窗口焦点和可见性状态
28
+ - accountIdx: 窗口所在帐户,帐户可以是0,1,2,3...每个相同帐户下面的所有窗口共享缓存cookie等,
29
+ - bounds: 窗口位置和大小 (x, y, width, height)
30
+ - 加载状态: isLoading, isDomReady, isCrashed 等
31
+
32
+ 主要用途:
33
+ - 窗口管理:获取窗口ID进行后续操作
34
+ - 状态监控:检查窗口是否正常运行
35
+ - 自动化测试:验证窗口状态和属性
36
+ - 调试辅助:查看所有窗口的实时信息`,
37
+ z.object({}),
38
+ async () => {
39
+ try {
40
+ const windows = BrowserWindow.getAllWindows()
41
+ .map(getWindowInfo)
42
+ .filter((w) => w !== null);
43
+ return { content: [{ type: "text", text: JSON.stringify(windows, null, 2) }] };
44
+ } catch (error) {
45
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
46
+ }
47
+ },
48
+ { tag: "Window" }
49
+ );
50
+
51
+ registerTool(
52
+ "get_window_info",
53
+ "获取指定窗口的详细信息",
54
+ z.object({ win_id: z.number().optional().default(1).describe("Window ID") }),
55
+ async ({ win_id }) => {
56
+ try {
57
+ const win = BrowserWindow.fromId(win_id);
58
+ if (!win) throw new Error(`Window ${win_id} not found`);
59
+ return { content: [{ type: "text", text: JSON.stringify(getWindowInfo(win), null, 2) }] };
60
+ } catch (error) {
61
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
62
+ }
63
+ },
64
+ { tag: "Window" }
65
+ );
66
+
67
+ registerTool(
68
+ "open_window",
69
+ "打开新的浏览器窗口。用于创建新窗口访问网页、测试多窗口应用或隔离不同的浏览会话。支持自定义窗口大小和位置。",
70
+ z.object({
71
+ url: z.string().describe("URL to open"),
72
+ accountIdx: z
73
+ .number()
74
+ .optional()
75
+ .default(0)
76
+ .describe("窗口所在帐户,帐户可以是0,1,2,3...每个相同帐户下面的所有窗口共享缓存cookie等"),
77
+ reuseWindow: z
78
+ .boolean()
79
+ .optional()
80
+ .default(true)
81
+ .describe("是否复用现有窗口。true=复用(默认),false=创建新窗口"),
82
+ options: z.object({}).optional().describe("Electron BrowserWindow options"),
83
+ }),
84
+ async ({ url, accountIdx, reuseWindow, options }) => {
85
+ // Determine if we should create a new window
86
+ const forceNew = reuseWindow === false;
87
+
88
+ // Get existing windows count before creating
89
+ const existingCount = BrowserWindow.getAllWindows().length;
90
+
91
+ // Create or reuse window
92
+ const win = createWindow({ url, ...options }, accountIdx, forceNew);
93
+
94
+ // Check if window was reused (count didn't change)
95
+ const newCount = BrowserWindow.getAllWindows().length;
96
+ const wasReused = newCount === existingCount;
97
+
98
+ if (wasReused) {
99
+ return {
100
+ content: [
101
+ {
102
+ type: "text",
103
+ text: `Window already exists (ID: ${win.id}), reusing it. URL load triggered. Please use tool: get_window_info and wait for webContents dom-ready`,
104
+ },
105
+ ],
106
+ };
107
+ }
108
+
109
+ return {
110
+ content: [
111
+ {
112
+ type: "text",
113
+ text: `Opened window with ID: ${win.id}, use tool: get_window_info and wait window webContents dom-ready`,
114
+ },
115
+ ],
116
+ };
117
+ },
118
+ { tag: "Window" }
119
+ );
120
+
121
+ registerTool(
122
+ "close_window",
123
+ "关闭窗口",
124
+ z.object({ win_id: z.number().optional().default(1).describe("Window ID") }),
125
+ async ({ win_id }) => {
126
+ try {
127
+ const win = BrowserWindow.fromId(win_id);
128
+ if (win) {
129
+ win.close();
130
+ return { content: [{ type: "text", text: `Closed ${win_id}` }] };
131
+ }
132
+ throw new Error(`Window ${win_id} not found`);
133
+ } catch (error) {
134
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
135
+ }
136
+ },
137
+ { tag: "Window" }
138
+ );
139
+
140
+ registerTool(
141
+ "load_url",
142
+ "加载URL",
143
+ z.object({
144
+ url: z.string().describe("URL"),
145
+ win_id: z.number().optional().describe("Window ID"),
146
+ }),
147
+ async ({ url, win_id }) => {
148
+ try {
149
+ const actualWinId = win_id || 1;
150
+ const win = BrowserWindow.fromId(actualWinId);
151
+ if (!win) throw new Error(`Window ${actualWinId} not found`);
152
+ await win.loadURL(url);
153
+ return { content: [{ type: "text", text: `Loaded ${url}` }] };
154
+ } catch (error) {
155
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
156
+ }
157
+ },
158
+ { tag: "Window" }
159
+ );
160
+
161
+ registerTool(
162
+ "get_title",
163
+ "获取窗口标题",
164
+ z.object({ win_id: z.number().optional().default(1).describe("Window ID") }),
165
+ async ({ win_id }) => {
166
+ try {
167
+ const win = BrowserWindow.fromId(win_id);
168
+ if (!win) throw new Error(`Window ${win_id} not found`);
169
+ return { content: [{ type: "text", text: win.getTitle() }] };
170
+ } catch (error) {
171
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
172
+ }
173
+ },
174
+ { tag: "Window" }
175
+ );
176
+
177
+ registerTool(
178
+ "control_electron_BrowserWindow",
179
+ "直接调用Electron BrowserWindow实例的方法和属性。主要用途:窗口控制(移动、调整大小、最小化、最大化)、状态管理(置顶、焦点、可见性)、属性获取(位置、大小、状态)、高级操作(透明度、边框、图标)。可访问win(BrowserWindow实例)和webContents对象,支持async/await。示例: win.getBounds() 或 win.maximize() 或 win.webContents.executeJavaScript('document.title')",
180
+ z.object({
181
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
182
+ code: z.string().describe("JS 代码"),
183
+ }),
184
+ async ({ win_id, code }) => {
185
+ try {
186
+ const win = BrowserWindow.fromId(win_id);
187
+ if (!win) throw new Error(`未找到窗口 ${win_id}`);
188
+
189
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
190
+ const execute = new AsyncFunction("win", "webContents", `return ${code}`);
191
+ const result = await execute(win, win.webContents);
192
+
193
+ let outputText =
194
+ typeof result === "object"
195
+ ? JSON.stringify(result, (k, v) => (typeof v === "bigint" ? v.toString() : v), 2)
196
+ : String(result);
197
+
198
+ return { content: [{ type: "text", text: outputText }] };
199
+ } catch (error) {
200
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
201
+ }
202
+ },
203
+ { tag: "Window" }
204
+ );
205
+
206
+ registerTool(
207
+ "set_window_bounds",
208
+ "设置窗口的位置和大小。可以单独设置 x, y 坐标或 width, height 尺寸,也可以同时设置。坐标原点在屏幕左上角。",
209
+ z.object({
210
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
211
+ x: z.number().optional().describe("窗口 X 坐标(像素)"),
212
+ y: z.number().optional().describe("窗口 Y 坐标(像素)"),
213
+ width: z.number().optional().describe("窗口宽度(像素)"),
214
+ height: z.number().optional().describe("窗口高度(像素)"),
215
+ }),
216
+ async ({ win_id, x, y, width, height }) => {
217
+ try {
218
+ const win = BrowserWindow.fromId(win_id);
219
+ if (!win) throw new Error(`未找到窗口 ${win_id}`);
220
+
221
+ const currentBounds = win.getBounds();
222
+ const newBounds = {
223
+ x: x !== undefined ? x : currentBounds.x,
224
+ y: y !== undefined ? y : currentBounds.y,
225
+ width: width !== undefined ? width : currentBounds.width,
226
+ height: height !== undefined ? height : currentBounds.height,
227
+ };
228
+
229
+ // Unmaximize/unfullscreen first so setBounds takes effect
230
+ if (win.isMaximized()) win.unmaximize();
231
+ if (win.isFullScreen()) win.setFullScreen(false);
232
+
233
+ win.setBounds(newBounds);
234
+ const updatedBounds = win.getBounds();
235
+
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: `窗口 ${win_id} 位置和大小已更新:\n${JSON.stringify(updatedBounds, null, 2)}`,
241
+ },
242
+ ],
243
+ };
244
+ } catch (error) {
245
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
246
+ }
247
+ },
248
+ { tag: "Window" }
249
+ );
250
+
251
+ registerTool(
252
+ "control_electron_WebContents",
253
+ "调用Electron webContents的方法和属性。主要用途:页面操作(截图、打印、缩放、导航)、内容获取(URL、标题、源码)、脚本执行(在页面中执行JS)、开发调试(控制台信息、性能数据)、媒体控制(音频/视频)。可访问webContents和win对象,支持async/await,自动处理NativeImage返回为图像格式。示例: webContents.getURL() 或 webContents.reload() 或 webContents.capturePage()",
254
+ z.object({
255
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
256
+ code: z.string().describe("JS 代码"),
257
+ }),
258
+ async ({ win_id, code }) => {
259
+ try {
260
+ const win = BrowserWindow.fromId(win_id);
261
+ if (!win) throw new Error(`未找到窗口 ${win_id}`);
262
+
263
+ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
264
+ const execute = new AsyncFunction("webContents", "win", `return ${code}`);
265
+ let result = await execute(win.webContents, win);
266
+
267
+ if (result?.constructor.name === "NativeImage") {
268
+ const size = result.getSize();
269
+ const base64 = result.toPNG().toString("base64");
270
+ return {
271
+ content: [
272
+ { type: "text", text: `Image: ${size.width}x${size.height}` },
273
+ { type: "image", data: base64, mimeType: "image/png" },
274
+ ],
275
+ };
276
+ }
277
+
278
+ let outputText =
279
+ typeof result === "object"
280
+ ? JSON.stringify(result, (k, v) => (typeof v === "bigint" ? v.toString() : v), 2)
281
+ : String(result);
282
+
283
+ return { content: [{ type: "text", text: outputText }] };
284
+ } catch (error) {
285
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
286
+ }
287
+ },
288
+ { tag: "Window" }
289
+ );
290
+
291
+ registerTool(
292
+ "get_console_logs",
293
+ "获取窗口的控制台日志。返回自窗口创建或上次重载以来的所有 console 输出,包括 log/info/warning/error 等级别。支持分页查询和过滤。",
294
+ z.object({
295
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
296
+ page: z.number().optional().default(1).describe("页码,从1开始"),
297
+ page_size: z.number().optional().default(50).describe("每页数量"),
298
+ keyword: z.string().optional().describe("关键词过滤,匹配日志消息"),
299
+ level: z.enum(["verbose", "info", "warning", "error"]).optional().describe("日志级别过滤"),
300
+ }),
301
+ async ({ win_id, page, page_size, keyword, level }) => {
302
+ try {
303
+ let logs = getConsoleLogs(win_id);
304
+
305
+ // 关键词过滤
306
+ if (keyword) {
307
+ logs = logs.filter((log) => log.message.includes(keyword));
308
+ }
309
+
310
+ // 级别过滤
311
+ if (level) {
312
+ logs = logs.filter((log) => log.level === level);
313
+ }
314
+
315
+ // 按时间倒序排列(最新的在前)
316
+ logs = [...logs].sort((a, b) => b.timestamp - a.timestamp);
317
+
318
+ const start = (page - 1) * page_size;
319
+ const end = start + page_size;
320
+ const paginated = logs.slice(start, end);
321
+
322
+ const header = `Console Logs (${logs.length} total, page ${page}/${Math.ceil(logs.length / page_size) || 1}):\n`;
323
+ const logLines = paginated
324
+ .map((log) => {
325
+ const time = new Date(log.timestamp).toISOString().replace("T", " ").substring(0, 23);
326
+ const source = log.source ? ` (${log.source.split("/").pop()}:${log.line})` : "";
327
+ const level = log.level.toUpperCase().padEnd(7);
328
+ const msg = log.message.replace(/\n/g, " ").substring(0, 200);
329
+ return `${time} ${level} ${msg}${source}`;
330
+ })
331
+ .join("\n");
332
+
333
+ return { content: [{ type: "text", text: header + logLines }] };
334
+ } catch (error) {
335
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
336
+ }
337
+ },
338
+ { tag: "Console" }
339
+ );
340
+
341
+ registerTool(
342
+ "get_requests",
343
+ "获取窗口的网络请求记录。返回所有请求的详细信息(包含文件路径)。支持 URL 过滤和分页。",
344
+ z.object({
345
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
346
+ page: z.number().optional().default(1).describe("页码,从1开始"),
347
+ page_size: z.number().optional().default(50).describe("每页数量"),
348
+ filter: z.string().optional().describe("URL 过滤关键词(支持正则表达式)"),
349
+ }),
350
+ async ({ win_id, page, page_size, filter }) => {
351
+ try {
352
+ const allRequests = getLoadingFinishedRequests(win_id);
353
+ let entries = Array.from(allRequests.entries()).map(([url, data]) => ({
354
+ url,
355
+ requestCount: data.requests?.length || 0,
356
+ responseCount: data.responses?.length || 0,
357
+ requests: data.requests || [],
358
+ responses: data.responses || [],
359
+ }));
360
+
361
+ // 应用过滤
362
+ if (filter) {
363
+ try {
364
+ const regex = new RegExp(filter, "i");
365
+ entries = entries.filter((entry) => regex.test(entry.url));
366
+ } catch (e) {
367
+ entries = entries.filter((entry) => entry.url.includes(filter));
368
+ }
369
+ }
370
+
371
+ const start = (page - 1) * page_size;
372
+ const end = start + page_size;
373
+ const paginated = entries.slice(start, end);
374
+
375
+ const result = {
376
+ total: entries.length,
377
+ page: page,
378
+ page_size: page_size,
379
+ total_pages: entries.length > 0 ? Math.ceil(entries.length / page_size) : 0,
380
+ data: paginated,
381
+ };
382
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
383
+ } catch (error) {
384
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
385
+ }
386
+ },
387
+ { tag: "Network" }
388
+ );
389
+
390
+ registerTool(
391
+ "filter_requests",
392
+ "根据关键词或文档类型过滤网络请求。搜索 URL、POST data 或按文档类型筛选。",
393
+ z.object({
394
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
395
+ keyword: z.string().optional().describe("搜索关键词(可选)"),
396
+ doc_type: z
397
+ .string()
398
+ .optional()
399
+ .describe("文档类型过滤(如 json, html, javascript, css, image, xml)"),
400
+ page: z.number().optional().default(1).describe("页码,从1开始"),
401
+ page_size: z.number().optional().default(50).describe("每页数量"),
402
+ }),
403
+ async ({ win_id, keyword, doc_type, page, page_size }) => {
404
+ try {
405
+ const allRequests = getRequests(win_id);
406
+
407
+ // 过滤匹配的请求
408
+ const filtered = allRequests.filter((req) => {
409
+ let match = true;
410
+
411
+ // 关键词过滤
412
+ if (keyword) {
413
+ const lowerKeyword = keyword.toLowerCase();
414
+ let keywordMatch = false;
415
+
416
+ // 检查 URL
417
+ if (req.url.toLowerCase().includes(lowerKeyword)) {
418
+ keywordMatch = true;
419
+ }
420
+
421
+ // 检查 POST data
422
+ if (!keywordMatch) {
423
+ const detail = getRequestDetail(win_id, req.index);
424
+ if (detail && detail.postData) {
425
+ const postData =
426
+ typeof detail.postData === "string"
427
+ ? detail.postData
428
+ : JSON.stringify(detail.postData);
429
+ if (postData.toLowerCase().includes(lowerKeyword)) {
430
+ keywordMatch = true;
431
+ }
432
+ }
433
+ }
434
+
435
+ match = match && keywordMatch;
436
+ }
437
+
438
+ // 文档类型过滤
439
+ if (doc_type && match) {
440
+ const lowerType = doc_type.toLowerCase();
441
+ const reqMime = (req.mimeType || "").toLowerCase();
442
+ match = match && reqMime.includes(lowerType);
443
+ }
444
+
445
+ return match;
446
+ });
447
+
448
+ const start = (page - 1) * page_size;
449
+ const end = start + page_size;
450
+ const paginated = filtered.slice(start, end);
451
+
452
+ const result = {
453
+ filters: { keyword, doc_type },
454
+ total: filtered.length,
455
+ page,
456
+ page_size,
457
+ total_pages: Math.ceil(filtered.length / page_size),
458
+ data: paginated,
459
+ };
460
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
461
+ } catch (error) {
462
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
463
+ }
464
+ },
465
+ { tag: "Network" }
466
+ );
467
+
468
+ registerTool(
469
+ "get_request_detail",
470
+ "获取指定请求的详细信息,包括请求头和请求体。使用请求的 index 来查询。",
471
+ z.object({
472
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
473
+ index: z.number().describe("请求的 index"),
474
+ }),
475
+ async ({ win_id, index }) => {
476
+ try {
477
+ const detail = getRequestDetail(win_id, index);
478
+ if (!detail) {
479
+ throw new Error(`Request with index ${index} not found`);
480
+ }
481
+
482
+ // 如果有 responseBodyFile 和 responseBodySize,检查大小
483
+ if (detail.responseBodyFile && detail.responseBodySize) {
484
+ if (detail.responseBodySize > 1024) {
485
+ // Response body > 1KB,返回文件路径
486
+ const result = {
487
+ ...detail,
488
+ responseBody: `[Response body too large: ${detail.responseBodySize} bytes]`,
489
+ responseBodyPath: detail.responseBodyFile,
490
+ };
491
+ delete result.responseBodyFile;
492
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
493
+ } else {
494
+ // Response body <= 1KB,读取并包含在响应中
495
+ const fs = require("fs");
496
+ if (fs.existsSync(detail.responseBodyFile)) {
497
+ const encoding = detail.base64Encoded ? "base64" : "utf8";
498
+ const body = fs.readFileSync(detail.responseBodyFile, encoding);
499
+ const result = {
500
+ ...detail,
501
+ responseBody: body,
502
+ };
503
+ delete result.responseBodyFile;
504
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
505
+ }
506
+ }
507
+ }
508
+
509
+ return { content: [{ type: "text", text: JSON.stringify(detail, null, 2) }] };
510
+ } catch (error) {
511
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
512
+ }
513
+ },
514
+ { tag: "Network" }
515
+ );
516
+
517
+ registerTool(
518
+ "session_download_url",
519
+ "使用窗口的 session 下载文件到指定路径。不会弹出保存对话框,下载完成后文件直接保存到 save_path。",
520
+ z.object({
521
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
522
+ url: z.string().describe("下载 URL"),
523
+ save_path: z.string().describe("保存路径(完整路径包含文件名),下载完成后文件保存在此路径"),
524
+ timeout: z.number().optional().default(300000).describe("超时时间(毫秒),默认 5 分钟"),
525
+ }),
526
+ async ({ win_id, url, save_path, timeout }) => {
527
+ try {
528
+ const win = BrowserWindow.fromId(win_id);
529
+ if (!win) throw new Error(`Window ${win_id} not found`);
530
+
531
+ const fs = require("fs");
532
+ const path = require("path");
533
+ const session = win.webContents.session;
534
+
535
+ // 检查文件是否已存在
536
+ if (fs.existsSync(save_path)) {
537
+ return {
538
+ content: [
539
+ {
540
+ type: "text",
541
+ text: JSON.stringify(
542
+ {
543
+ status: "exists",
544
+ url,
545
+ path: save_path,
546
+ message: "File already exists",
547
+ },
548
+ null,
549
+ 2
550
+ ),
551
+ },
552
+ ],
553
+ };
554
+ }
555
+
556
+ // 创建目录
557
+ fs.mkdirSync(path.dirname(save_path), { recursive: true });
558
+
559
+ // 下载文件
560
+ const result = await new Promise((resolve, reject) => {
561
+ const timeoutId = setTimeout(() => {
562
+ reject(new Error(`Download timeout after ${timeout / 1000}s`));
563
+ }, timeout);
564
+
565
+ session.once("will-download", (event, item) => {
566
+ try {
567
+ item.setSavePath(save_path);
568
+ item.resume();
569
+
570
+ item.on("updated", (event, state) => {
571
+ const received = item.getReceivedBytes();
572
+ const total = item.getTotalBytes();
573
+ log.info(`Downloading: ${received} / ${total} bytes`);
574
+ });
575
+
576
+ item.once("done", (event, state) => {
577
+ clearTimeout(timeoutId);
578
+
579
+ if (state !== "completed") {
580
+ return reject(new Error(`Download failed: ${state}`));
581
+ }
582
+
583
+ resolve({
584
+ status: "completed",
585
+ url,
586
+ path: save_path,
587
+ size: item.getTotalBytes(),
588
+ mime: item.getMimeType(),
589
+ filename: item.getFilename(),
590
+ });
591
+ });
592
+ } catch (err) {
593
+ clearTimeout(timeoutId);
594
+ reject(err);
595
+ }
596
+ });
597
+
598
+ session.downloadURL(url);
599
+ });
600
+
601
+ return {
602
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
603
+ };
604
+ } catch (error) {
605
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
606
+ }
607
+ },
608
+ { tag: "Network" }
609
+ );
610
+
611
+ registerTool(
612
+ "webpage_screenshot_to_clipboard",
613
+ `捕获指定窗口的页面截图并自动复制到系统剪贴板。这是快速获取页面视觉内容的便捷工具。
614
+
615
+ 主要用途:
616
+ - 快速截图:一键获取页面截图
617
+ - 文档记录:保存页面状态用于报告或文档
618
+ - 测试验证:验证页面显示效果
619
+ - 问题报告:快速获取错误页面截图
620
+ - 内容分享:将页面内容复制到其他应用
621
+
622
+ 特点:
623
+ - 自动复制到剪贴板,可直接粘贴到其他应用
624
+ - 返回 MCP 图像格式,支持在对话中显示
625
+ - 包含图像尺寸信息`,
626
+ z.object({
627
+ win_id: z.number().optional().describe("Window ID to capture (defaults to 1)"),
628
+ }),
629
+ async ({ win_id }) => {
630
+ try {
631
+ const actualWinId = win_id || 1;
632
+ const win = BrowserWindow.fromId(actualWinId);
633
+ if (!win) throw new Error(`Window ${actualWinId} not found`);
634
+ const result = await captureSnapshot(win.webContents, {
635
+ win_id: actualWinId,
636
+ });
637
+
638
+ return result;
639
+ } catch (error) {
640
+ return {
641
+ content: [{ type: "text", text: `Error capturing snapshot: ${error.message}` }],
642
+ isError: true,
643
+ };
644
+ }
645
+ },
646
+ { tag: "Screenshot" }
647
+ );
648
+
649
+ registerTool(
650
+ "webpage_snapshot",
651
+ "捕获页面的结构快照,包含 URL 和所有可交互元素的信息。支持按类型/文本搜索元素。",
652
+ z.object({
653
+ win_id: z.number().optional().describe("Window ID"),
654
+ max_elements: z.number().optional().default(20).describe("最大元素数量"),
655
+ show_overlays: z
656
+ .boolean()
657
+ .optional()
658
+ .default(false)
659
+ .describe("是否显示可点击元素遮罩(5秒后消失)"),
660
+ }),
661
+ async ({ win_id, max_elements, show_overlays }) => {
662
+ try {
663
+ const actualWinId = win_id || 1;
664
+ const win = BrowserWindow.fromId(actualWinId);
665
+ if (!win) throw new Error(`Window ${actualWinId} not found`);
666
+
667
+ const result = await win.webContents.executeJavaScript(`
668
+ (() => {
669
+ const url = window.location.href;
670
+ const title = document.title;
671
+ const viewportWidth = window.innerWidth;
672
+ const viewportHeight = window.innerHeight;
673
+ const scrollX = window.scrollX;
674
+ const scrollY = window.scrollY;
675
+
676
+ const getElementInfo = (el) => {
677
+ const rect = el.getBoundingClientRect();
678
+ return {
679
+ tag: el.tagName.toLowerCase(),
680
+ text: (el.textContent || '').substring(0, 100).trim(),
681
+ href: el.href || null,
682
+ src: el.src || null,
683
+ placeholder: el.placeholder || null,
684
+ id: el.id || null,
685
+ className: el.className ? el.className.substring(0, 50) : null,
686
+ x: Math.round(rect.x),
687
+ y: Math.round(rect.y),
688
+ width: Math.round(rect.width),
689
+ height: Math.round(rect.height)
690
+ };
691
+ };
692
+
693
+ const interactiveElements = [];
694
+ document.querySelectorAll('a, button, input, select, textarea, [onclick], [role="button"], [role="link"]').forEach(el => {
695
+ if (el.offsetParent !== null) {
696
+ interactiveElements.push(getElementInfo(el));
697
+ }
698
+ });
699
+
700
+ return {
701
+ url: url,
702
+ title: title,
703
+ viewportWidth: viewportWidth,
704
+ viewportHeight: viewportHeight,
705
+ scrollX: scrollX,
706
+ scrollY: scrollY,
707
+ interactive_elements: interactiveElements
708
+ };
709
+ })()
710
+ `);
711
+
712
+ const response = [
713
+ {
714
+ type: "text",
715
+ text:
716
+ "Page Snapshot\n" +
717
+ "url: " +
718
+ result.url +
719
+ "\n" +
720
+ "title: " +
721
+ result.title +
722
+ "\n" +
723
+ "viewport: " +
724
+ result.viewportWidth +
725
+ "x" +
726
+ result.viewportHeight +
727
+ "\n" +
728
+ "scroll: (" +
729
+ result.scrollX +
730
+ ", " +
731
+ result.scrollY +
732
+ ")\n" +
733
+ "Interactive Elements (" +
734
+ result.interactive_elements.length +
735
+ "):\n" +
736
+ result.interactive_elements
737
+ .slice(0, max_elements || 20)
738
+ .map(
739
+ (el, i) =>
740
+ i +
741
+ 1 +
742
+ ". [" +
743
+ el.tag +
744
+ "] " +
745
+ (el.text || "(no text)") +
746
+ " @ (" +
747
+ el.x +
748
+ ", " +
749
+ el.y +
750
+ ") " +
751
+ el.width +
752
+ "x" +
753
+ el.height
754
+ )
755
+ .join("\n"),
756
+ },
757
+ ];
758
+
759
+ if (show_overlays) {
760
+ await win.webContents.executeJavaScript(`
761
+ (function() {
762
+ const elements = ${JSON.stringify(result.interactive_elements)};
763
+ elements.forEach((el, i) => {
764
+ const overlay = document.createElement('div');
765
+ overlay.style.cssText = \`position:fixed;left:\${el.x}px;top:\${el.y}px;width:\${el.width}px;height:\${el.height}px;background:rgba(255,0,0,0.2);border:2px solid red;z-index:999999;pointer-events:none;box-sizing:border-box\`;
766
+ const label = document.createElement('div');
767
+ label.textContent = i + 1;
768
+ label.style.cssText = 'position:absolute;top:2px;left:2px;background:red;color:white;padding:2px 6px;font-size:12px;font-weight:bold;font-family:monospace;line-height:1';
769
+ overlay.appendChild(label);
770
+ document.body.appendChild(overlay);
771
+ setTimeout(() => overlay.remove(), 5000);
772
+ });
773
+ })()
774
+ `);
775
+ }
776
+
777
+ return { content: response };
778
+ } catch (error) {
779
+ return {
780
+ content: [{ type: "text", text: `Error: ${error.message}` }],
781
+ isError: true,
782
+ };
783
+ }
784
+ },
785
+ { tag: "Screenshot" }
786
+ );
787
+
788
+ registerTool(
789
+ "get_request_urls",
790
+ "获取窗口的所有请求 URL 列表(队列)。支持 URL 过滤。",
791
+ z.object({
792
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
793
+ page: z.number().optional().default(1).describe("页码,从1开始"),
794
+ page_size: z.number().optional().default(100).describe("每页数量"),
795
+ filter: z.string().optional().describe("URL 过滤关键词(支持正则表达式)"),
796
+ }),
797
+ async ({ win_id, page, page_size, filter }) => {
798
+ try {
799
+ let urls = getBeforeSendRequests(win_id);
800
+
801
+ // 应用过滤
802
+ if (filter) {
803
+ try {
804
+ const regex = new RegExp(filter, "i");
805
+ urls = urls.filter((url) => regex.test(url));
806
+ } catch (e) {
807
+ // 如果不是有效的正则,使用简单字符串匹配
808
+ urls = urls.filter((url) => url.includes(filter));
809
+ }
810
+ }
811
+
812
+ const start = (page - 1) * page_size;
813
+ const end = start + page_size;
814
+ const paginated = urls.slice(start, end);
815
+ const result = {
816
+ total: urls.length,
817
+ page: page,
818
+ page_size: page_size,
819
+ total_pages: urls.length > 0 ? Math.ceil(urls.length / page_size) : 0,
820
+ urls: paginated,
821
+ };
822
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
823
+ } catch (error) {
824
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
825
+ }
826
+ },
827
+ { tag: "Network" }
828
+ );
829
+
830
+ registerTool(
831
+ "get_request_detail_by_url",
832
+ "根据 URL 获取请求的完整详情。返回该 URL 的所有请求和响应的文件路径列表。",
833
+ z.object({
834
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
835
+ url: z.string().describe("请求的完整 URL"),
836
+ }),
837
+ async ({ win_id, url }) => {
838
+ try {
839
+ const detail = getRequestDetailByUrl(win_id, url);
840
+ if (!detail) {
841
+ return {
842
+ content: [{ type: "text", text: `Request not found for URL: ${url}` }],
843
+ isError: true,
844
+ };
845
+ }
846
+
847
+ // 返回文件路径列表,用户可以自己读取文件
848
+ const result = {
849
+ url,
850
+ requestCount: detail.requests?.length || 0,
851
+ responseCount: detail.responses?.length || 0,
852
+ requests: detail.requests || [],
853
+ responses: detail.responses || [],
854
+ };
855
+
856
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
857
+ } catch (error) {
858
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
859
+ }
860
+ },
861
+ { tag: "Network" }
862
+ );
863
+
864
+ registerTool(
865
+ "clear_requests",
866
+ "清空指定窗口的所有请求记录(仅清空内存,不删除已保存的文件)。",
867
+ z.object({
868
+ win_id: z.number().optional().default(1).describe("窗口 ID"),
869
+ }),
870
+ async ({ win_id }) => {
871
+ try {
872
+ clearRequests(win_id);
873
+ return { content: [{ type: "text", text: `Cleared all requests for window ${win_id}` }] };
874
+ } catch (error) {
875
+ return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
876
+ }
877
+ },
878
+ { tag: "Network" }
879
+ );
880
+ }
881
+
882
+ module.exports = registerTools;