@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,856 @@
1
+ import { ipcMain, dialog, app, session, shell } from "electron";
2
+ import { networkInterfaces } from "os";
3
+ import type { LLMProviderConfig, MCPServerConfig, MCPServerSettings, MitmProxyConfig, ProxyConfig, PromptTemplate } from "@shared/types";
4
+ import type { SessionManager } from "./session/session-manager";
5
+ import type { AiAnalyzer } from "./ai/ai-analyzer";
6
+ import type { WindowManager } from "./window";
7
+ import type { Updater } from "./updater";
8
+ import type { MCPClientManager } from "./mcp/mcp-manager";
9
+ import type { MitmProxyServer } from "./proxy/mitm-proxy-server";
10
+ import type { CaManager } from "./proxy/ca-manager";
11
+ import type { ProfileStore } from './fingerprint/profile-store';
12
+ import { CertInstaller } from "./proxy/cert-installer";
13
+ import { SystemProxy } from "./proxy/system-proxy";
14
+ import { loadMitmProxyConfig, saveMitmProxyConfig } from "./proxy/mitm-proxy-config";
15
+ import {
16
+ loadTemplates,
17
+ saveTemplate,
18
+ deleteTemplate,
19
+ resetTemplate,
20
+ findTemplate,
21
+ } from "./prompt-templates";
22
+ import {
23
+ loadMCPServers,
24
+ saveMCPServer,
25
+ deleteMCPServer,
26
+ } from "./mcp/mcp-config";
27
+ import type {
28
+ RequestsRepo,
29
+ JsHooksRepo,
30
+ StorageSnapshotsRepo,
31
+ AnalysisReportsRepo,
32
+ SessionsRepo,
33
+ ChatMessagesRepo,
34
+ AiRequestLogRepo,
35
+ InteractionEventsRepo,
36
+ } from "./db/repositories";
37
+ import { readFileSync, writeFileSync, existsSync } from "fs";
38
+ import { join } from "path";
39
+ import { randomUUID } from "node:crypto";
40
+
41
+ /**
42
+ * Register all IPC handlers for communication between renderer and main process.
43
+ */
44
+
45
+ /** Active analysis abort controllers, keyed by sessionId */
46
+ const analysisControllers = new Map<string, AbortController>();
47
+
48
+ /** Report IDs with in-flight chat calls — protected from cascade deletion */
49
+ const activeChatReports = new Set<string>();
50
+
51
+ export function registerIpcHandlers(deps: {
52
+ sessionManager: SessionManager;
53
+ aiAnalyzer: AiAnalyzer;
54
+ windowManager: WindowManager;
55
+ updater: Updater;
56
+ mcpManager: MCPClientManager;
57
+ mitmProxy: MitmProxyServer;
58
+ caManager: CaManager;
59
+ sessionsRepo: SessionsRepo;
60
+ requestsRepo: RequestsRepo;
61
+ jsHooksRepo: JsHooksRepo;
62
+ storageSnapshotsRepo: StorageSnapshotsRepo;
63
+ reportsRepo: AnalysisReportsRepo;
64
+ chatMessagesRepo: ChatMessagesRepo;
65
+ profileStore: ProfileStore;
66
+ aiRequestLogRepo: AiRequestLogRepo;
67
+ interactionEventsRepo: InteractionEventsRepo;
68
+ }): void {
69
+ const {
70
+ sessionManager,
71
+ aiAnalyzer,
72
+ windowManager,
73
+ updater,
74
+ mcpManager,
75
+ mitmProxy,
76
+ caManager,
77
+ sessionsRepo,
78
+ requestsRepo,
79
+ jsHooksRepo,
80
+ storageSnapshotsRepo,
81
+ reportsRepo,
82
+ chatMessagesRepo,
83
+ profileStore,
84
+ aiRequestLogRepo,
85
+ interactionEventsRepo,
86
+ } = deps;
87
+
88
+ // ---- Session Management ----
89
+
90
+ ipcMain.handle(
91
+ "session:create",
92
+ async (_event, name: string, targetUrl: string) => {
93
+ return sessionManager.createSession(name, targetUrl);
94
+ },
95
+ );
96
+
97
+ ipcMain.handle("session:list", async () => {
98
+ return sessionManager.listSessions();
99
+ });
100
+
101
+ ipcMain.handle("session:start", async (_event, sessionId: string) => {
102
+ const tabManager = windowManager.getTabManager();
103
+ const mainWin = windowManager.getMainWindow();
104
+ if (!tabManager || !mainWin) throw new Error("Browser not ready");
105
+ const proxyConfig = loadProxyConfig();
106
+ await sessionManager.startCapture(
107
+ sessionId,
108
+ tabManager,
109
+ mainWin.webContents,
110
+ proxyConfig,
111
+ );
112
+ });
113
+
114
+ ipcMain.handle("session:pause", async (_event, sessionId: string) => {
115
+ await sessionManager.pauseCapture(sessionId);
116
+ });
117
+
118
+ ipcMain.handle("session:resume", async (_event, sessionId: string) => {
119
+ await sessionManager.resumeCapture(sessionId);
120
+ });
121
+
122
+ ipcMain.handle("session:stop", async (_event, sessionId: string) => {
123
+ await sessionManager.stopCapture(sessionId);
124
+ });
125
+
126
+ ipcMain.handle("session:delete", async (_event, sessionId: string) => {
127
+ // Check if any reports in this session have in-flight chat calls
128
+ const sessionReports = reportsRepo.findBySession(sessionId);
129
+ const hasActiveChat = sessionReports.some(r => activeChatReports.has(r.id));
130
+ if (hasActiveChat) {
131
+ throw new Error("Cannot delete session while AI chat is in progress. Please wait for the response to complete.");
132
+ }
133
+ const tabManager = windowManager.getTabManager();
134
+ await sessionManager.deleteSession(sessionId, tabManager ?? undefined);
135
+ });
136
+
137
+ // ---- Window Control (frameless window) ----
138
+
139
+ ipcMain.handle("window:minimize", () => {
140
+ windowManager.getMainWindow()?.minimize();
141
+ });
142
+
143
+ ipcMain.handle("window:maximize", () => {
144
+ const win = windowManager.getMainWindow();
145
+ if (win?.isMaximized()) {
146
+ win.unmaximize();
147
+ } else {
148
+ win?.maximize();
149
+ }
150
+ });
151
+
152
+ ipcMain.handle("window:close", () => {
153
+ windowManager.getMainWindow()?.close();
154
+ });
155
+
156
+ ipcMain.handle("window:isMaximized", () => {
157
+ return windowManager.getMainWindow()?.isMaximized() ?? false;
158
+ });
159
+
160
+ // ---- Browser Control ----
161
+
162
+ ipcMain.handle("browser:navigate", async (_event, url: string) => {
163
+ await windowManager.navigateTo(url);
164
+ });
165
+
166
+ ipcMain.handle("browser:back", async () => {
167
+ windowManager.goBack();
168
+ });
169
+
170
+ ipcMain.handle("browser:forward", async () => {
171
+ windowManager.goForward();
172
+ });
173
+
174
+ ipcMain.handle("browser:reload", async () => {
175
+ windowManager.reload();
176
+ });
177
+
178
+ ipcMain.handle("browser:clearEnv", async () => {
179
+ const elSession = sessionManager.getActiveElectronSession() ?? session.defaultSession;
180
+ await elSession.clearStorageData();
181
+ await elSession.clearCache();
182
+ const wc = windowManager.getTabManager()?.getActiveWebContents();
183
+ if (wc && !wc.isDestroyed()) wc.reload();
184
+ });
185
+
186
+ ipcMain.handle("browser:setRatio", async (_event, ratio: number) => {
187
+ windowManager.setBrowserRatio(ratio);
188
+ });
189
+
190
+ // Renderer reports exact browser placeholder bounds (fire-and-forget)
191
+ ipcMain.on("browser:syncBounds", (_event, bounds: { x: number; y: number; width: number; height: number }) => {
192
+ windowManager.syncBrowserBounds(bounds);
193
+ });
194
+
195
+ ipcMain.handle("browser:setVisible", async (_event, visible: boolean) => {
196
+ windowManager.setTargetViewVisible(visible);
197
+ });
198
+
199
+ ipcMain.handle("browser:toggleDevTools", async () => {
200
+ const wc = windowManager.getTabManager()?.getActiveWebContents();
201
+ if (!wc || wc.isDestroyed()) return;
202
+ if (wc.isDevToolsOpened()) {
203
+ wc.closeDevTools();
204
+ } else {
205
+ wc.openDevTools({ mode: 'detach' });
206
+ }
207
+ });
208
+
209
+ // ---- Tab Management ----
210
+
211
+ ipcMain.handle("tabs:create", async (_event, url?: string) => {
212
+ const tabManager = windowManager.getTabManager();
213
+ if (!tabManager) throw new Error("Tab manager not ready");
214
+ const tab = tabManager.createTab(url);
215
+ return { id: tab.id, url: tab.url, title: tab.title, isActive: true };
216
+ });
217
+
218
+ ipcMain.handle("tabs:close", async (_event, tabId: string) => {
219
+ const tabManager = windowManager.getTabManager();
220
+ if (!tabManager) throw new Error("Tab manager not ready");
221
+ tabManager.closeTab(tabId);
222
+ });
223
+
224
+ ipcMain.handle("tabs:activate", async (_event, tabId: string) => {
225
+ const tabManager = windowManager.getTabManager();
226
+ if (!tabManager) throw new Error("Tab manager not ready");
227
+ tabManager.activateTab(tabId);
228
+ });
229
+
230
+ ipcMain.handle("tabs:list", async () => {
231
+ const tabManager = windowManager.getTabManager();
232
+ if (!tabManager) return [];
233
+ const activeTab = tabManager.getActiveTab();
234
+ return tabManager.getAllTabs().map((t) => ({
235
+ id: t.id,
236
+ url: t.url,
237
+ title: t.title,
238
+ isActive: t.id === activeTab?.id,
239
+ isLoading: t.isLoading,
240
+ }));
241
+ });
242
+
243
+ // Forward TabManager events to the renderer
244
+ const tabManager = windowManager.getTabManager();
245
+ const mainWin = windowManager.getMainWindow();
246
+ if (tabManager && mainWin) {
247
+ tabManager.on(
248
+ "tab-created",
249
+ (tabInfo: { id: string; url: string; title: string }) => {
250
+ if (mainWin.isDestroyed()) return;
251
+ mainWin.webContents.send("tabs:created", {
252
+ id: tabInfo.id,
253
+ url: tabInfo.url,
254
+ title: tabInfo.title,
255
+ isActive: true,
256
+ });
257
+ },
258
+ );
259
+ tabManager.on("tab-closed", (data: { tabId: string }) => {
260
+ if (mainWin.isDestroyed()) return;
261
+ mainWin.webContents.send("tabs:closed", data);
262
+ });
263
+ tabManager.on(
264
+ "tab-activated",
265
+ (data: { tabId: string; url: string; title: string }) => {
266
+ if (mainWin.isDestroyed()) return;
267
+ mainWin.webContents.send("tabs:activated", data);
268
+ },
269
+ );
270
+ tabManager.on(
271
+ "tab-updated",
272
+ (data: { tabId: string; url?: string; title?: string; isLoading?: boolean }) => {
273
+ if (mainWin.isDestroyed()) return;
274
+ mainWin.webContents.send("tabs:updated", data);
275
+ },
276
+ );
277
+ }
278
+
279
+ // ---- Data Queries ----
280
+
281
+ ipcMain.handle("data:requests", async (_event, sessionId: string) => {
282
+ return requestsRepo.findBySession(sessionId);
283
+ });
284
+
285
+ ipcMain.handle("data:hooks", async (_event, sessionId: string) => {
286
+ return jsHooksRepo.findBySession(sessionId);
287
+ });
288
+
289
+ ipcMain.handle("data:storage", async (_event, sessionId: string) => {
290
+ return storageSnapshotsRepo.findBySession(sessionId);
291
+ });
292
+
293
+ ipcMain.handle("data:reports", async (_event, sessionId: string) => {
294
+ return reportsRepo.findBySession(sessionId);
295
+ });
296
+
297
+ ipcMain.handle("data:clear", async (_event, sessionId: string) => {
298
+ requestsRepo.deleteBySession(sessionId);
299
+ jsHooksRepo.deleteBySession(sessionId);
300
+ storageSnapshotsRepo.deleteBySession(sessionId);
301
+
302
+ // Protect reports with in-flight chat from cascade deletion
303
+ const allReports = reportsRepo.findBySession(sessionId);
304
+ const protectedIds = new Set(
305
+ allReports.filter(r => activeChatReports.has(r.id)).map(r => r.id)
306
+ );
307
+
308
+ if (protectedIds.size === 0) {
309
+ reportsRepo.deleteBySession(sessionId);
310
+ } else {
311
+ // Delete only unprotected reports
312
+ for (const r of allReports) {
313
+ if (!protectedIds.has(r.id)) {
314
+ reportsRepo.deleteById(r.id);
315
+ }
316
+ }
317
+ }
318
+ });
319
+
320
+ // ---- AI Analysis ----
321
+
322
+ ipcMain.handle("ai:analyze", async (_event, sessionId: string, purpose?: string, selectedSeqs?: number[]) => {
323
+ const config = loadLLMConfig();
324
+ if (!config) throw new Error("LLM provider not configured");
325
+
326
+ const win = windowManager.getMainWindow();
327
+ const onProgress = win
328
+ ? (chunk: string) => {
329
+ win.webContents.send("ai:progress", chunk);
330
+ }
331
+ : undefined;
332
+
333
+ // 连接所有启用的 MCP 服务器
334
+ const mcpServers = loadMCPServers();
335
+ if (mcpServers.some((s) => s.enabled)) {
336
+ await mcpManager.connectAll(mcpServers);
337
+ }
338
+
339
+ // Resolve template: if purpose matches a template ID, load it
340
+ const template = purpose ? findTemplate(purpose) : findTemplate("auto");
341
+
342
+ // Cancel any existing analysis for this session
343
+ analysisControllers.get(sessionId)?.abort();
344
+ const controller = new AbortController();
345
+ analysisControllers.set(sessionId, controller);
346
+
347
+ try {
348
+ return await aiAnalyzer.analyze(sessionId, config, onProgress, purpose, template ?? undefined, selectedSeqs, controller.signal);
349
+ } finally {
350
+ analysisControllers.delete(sessionId);
351
+ }
352
+ });
353
+
354
+ ipcMain.handle("ai:cancel", async (_event, sessionId: string) => {
355
+ analysisControllers.get(sessionId)?.abort();
356
+ analysisControllers.delete(sessionId);
357
+ });
358
+
359
+ ipcMain.handle(
360
+ "ai:chat",
361
+ async (
362
+ _event,
363
+ sessionId: string,
364
+ reportId: string,
365
+ history: Array<{ role: string; content: string }>,
366
+ userMessage: string,
367
+ ) => {
368
+ const config = loadLLMConfig();
369
+ if (!config) throw new Error("LLM provider not configured");
370
+
371
+ const win = windowManager.getMainWindow();
372
+ const onProgress = win
373
+ ? (chunk: string) => {
374
+ win.webContents.send("ai:progress", chunk);
375
+ }
376
+ : undefined;
377
+
378
+ if (reportId) {
379
+ activeChatReports.add(reportId);
380
+ }
381
+
382
+ try {
383
+ const reply = await aiAnalyzer.chat(sessionId, config, history, userMessage, onProgress, reportId);
384
+
385
+ // Persist user message and AI reply to database
386
+ if (reportId) {
387
+ chatMessagesRepo.append(reportId, 'user', userMessage);
388
+ chatMessagesRepo.append(reportId, 'assistant', reply);
389
+ }
390
+
391
+ return reply;
392
+ } finally {
393
+ if (reportId) {
394
+ activeChatReports.delete(reportId);
395
+ }
396
+ }
397
+ },
398
+ );
399
+
400
+ // ---- Chat Messages Persistence ----
401
+
402
+ ipcMain.handle("data:chatMessages", async (_event, reportId: string) => {
403
+ return chatMessagesRepo.findByReport(reportId);
404
+ });
405
+
406
+ ipcMain.handle("data:saveChatMessages", async (_event, reportId: string, messages: Array<{ role: string; content: string }>) => {
407
+ try {
408
+ chatMessagesRepo.insertMany(reportId, messages);
409
+ } catch (e: unknown) {
410
+ const msg = e instanceof Error ? e.message : String(e);
411
+ if (msg.includes('FOREIGN KEY constraint failed')) {
412
+ console.warn(`[data:saveChatMessages] Report ${reportId} no longer exists, skipping`);
413
+ } else {
414
+ throw e;
415
+ }
416
+ }
417
+ });
418
+
419
+ // ---- Settings ----
420
+
421
+ ipcMain.handle("settings:getLLM", async () => {
422
+ return loadLLMConfig();
423
+ });
424
+
425
+ ipcMain.handle(
426
+ "settings:saveLLM",
427
+ async (_event, config: LLMProviderConfig) => {
428
+ saveLLMConfig(config);
429
+ },
430
+ );
431
+
432
+ // ---- File Export ----
433
+
434
+ ipcMain.handle(
435
+ "dialog:exportFile",
436
+ async (_event, defaultName: string, content: string) => {
437
+ const win = windowManager.getMainWindow();
438
+ if (!win) return false;
439
+ const { canceled, filePath } = await dialog.showSaveDialog(win, {
440
+ defaultPath: defaultName,
441
+ filters: [
442
+ { name: "Markdown", extensions: ["md"] },
443
+ { name: "All Files", extensions: ["*"] },
444
+ ],
445
+ });
446
+ if (canceled || !filePath) return false;
447
+ writeFileSync(filePath, content, "utf-8");
448
+ return true;
449
+ },
450
+ );
451
+
452
+ // ---- Auto Update ----
453
+
454
+ ipcMain.handle("app:version", () => {
455
+ return app.getVersion();
456
+ });
457
+
458
+ ipcMain.handle("update:check", async () => {
459
+ updater.checkForUpdates();
460
+ });
461
+
462
+ ipcMain.on("update:install", () => {
463
+ updater.quitAndInstall();
464
+ });
465
+
466
+ // ---- Prompt Templates ----
467
+
468
+ ipcMain.handle("templates:list", async () => {
469
+ return loadTemplates();
470
+ });
471
+
472
+ ipcMain.handle("templates:save", async (_event, template: PromptTemplate) => {
473
+ saveTemplate(template);
474
+ });
475
+
476
+ ipcMain.handle("templates:delete", async (_event, id: string) => {
477
+ deleteTemplate(id);
478
+ });
479
+
480
+ ipcMain.handle("templates:reset", async (_event, id: string) => {
481
+ resetTemplate(id);
482
+ });
483
+
484
+ // ---- MCP Servers ----
485
+
486
+ ipcMain.handle("mcp:list", async () => {
487
+ return loadMCPServers();
488
+ });
489
+
490
+ ipcMain.handle("mcp:save", async (_event, server: MCPServerConfig) => {
491
+ saveMCPServer(server);
492
+ });
493
+
494
+ ipcMain.handle("mcp:delete", async (_event, id: string) => {
495
+ deleteMCPServer(id);
496
+ // 同时断开该服务器连接
497
+ await mcpManager.disconnect(id);
498
+ });
499
+
500
+ // ---- Export Requests ----
501
+
502
+ ipcMain.handle("data:exportRequests", async (_event, sessionId: string) => {
503
+ const win = windowManager.getMainWindow();
504
+ if (!win) return false;
505
+ const requests = requestsRepo.findBySession(sessionId);
506
+ if (requests.length === 0) return false;
507
+ const sessionInfo = sessionsRepo.findById(sessionId);
508
+ const sessionName = sessionInfo?.name || "requests";
509
+ const timestamp = new Date().toISOString().slice(0, 10);
510
+ const defaultName = `${sessionName}-${timestamp}.json`;
511
+ const { canceled, filePath } = await dialog.showSaveDialog(win, {
512
+ defaultPath: defaultName,
513
+ filters: [
514
+ { name: "JSON", extensions: ["json"] },
515
+ { name: "All Files", extensions: ["*"] },
516
+ ],
517
+ });
518
+ if (canceled || !filePath) return false;
519
+ writeFileSync(filePath, JSON.stringify(requests, null, 2), "utf-8");
520
+ return true;
521
+ });
522
+
523
+ // ---- AI Request Logs ----
524
+
525
+ ipcMain.handle("data:aiRequestLogs", async (_event, sessionId: string) => {
526
+ return aiRequestLogRepo.findBySession(sessionId);
527
+ });
528
+
529
+ ipcMain.handle("data:aiRequestLogsAll", async (_event, limit: number, offset: number) => {
530
+ return aiRequestLogRepo.findAll(limit, offset);
531
+ });
532
+
533
+ ipcMain.handle("data:aiRequestLogDetail", async (_event, id: number) => {
534
+ return aiRequestLogRepo.findById(id);
535
+ });
536
+
537
+ // ---- Proxy ----
538
+
539
+ ipcMain.handle("proxy:get", async () => {
540
+ return loadProxyConfig();
541
+ });
542
+
543
+ ipcMain.handle("proxy:save", async (_event, config: ProxyConfig) => {
544
+ saveProxyConfigFile(config);
545
+ // Sync upstream proxy to MITM proxy first (synchronous, won't fail)
546
+ deps.mitmProxy.setUpstreamProxy(config);
547
+ await applyProxy(config);
548
+ // Also apply to the active session's partition if one exists
549
+ const activeElSession = sessionManager.getActiveElectronSession();
550
+ if (activeElSession) {
551
+ await applyProxy(config, activeElSession);
552
+ }
553
+ });
554
+
555
+ // ---- MCP Server Config ----
556
+
557
+ ipcMain.handle("mcp-server:getConfig", async () => {
558
+ return loadMCPServerConfig();
559
+ });
560
+
561
+ ipcMain.handle("mcp-server:saveConfig", async (_event, config: MCPServerSettings) => {
562
+ saveMCPServerConfig(config);
563
+
564
+ const { initMCPServer, stopMCPServer, isMCPServerRunning } = await import("./mcp/mcp-server");
565
+ if (config.enabled && !isMCPServerRunning()) {
566
+ await initMCPServer(
567
+ { sessionManager, aiAnalyzer, windowManager, requestsRepo, jsHooksRepo, storageSnapshotsRepo, reportsRepo, interactionEventsRepo },
568
+ config.port,
569
+ config.authEnabled,
570
+ config.authToken,
571
+ );
572
+ } else if (!config.enabled && isMCPServerRunning()) {
573
+ await stopMCPServer();
574
+ }
575
+ });
576
+
577
+ ipcMain.handle("mcp-server:status", async () => {
578
+ const { isMCPServerRunning } = await import("./mcp/mcp-server");
579
+ const config = loadMCPServerConfig();
580
+ return { running: isMCPServerRunning(), port: config.port };
581
+ });
582
+
583
+ // ---- MITM Proxy ----
584
+
585
+ ipcMain.handle("mitm-proxy:getConfig", async () => {
586
+ return loadMitmProxyConfig();
587
+ });
588
+
589
+ ipcMain.handle("mitm-proxy:saveConfig", async (_event, config: MitmProxyConfig) => {
590
+ saveMitmProxyConfig(config);
591
+ if (config.enabled && !deps.mitmProxy.isRunning()) {
592
+ await deps.caManager.init();
593
+ await deps.mitmProxy.start(config.port);
594
+ } else if (!config.enabled && deps.mitmProxy.isRunning()) {
595
+ await deps.mitmProxy.stop();
596
+ // Also disable system proxy if it was enabled
597
+ if (config.systemProxy) {
598
+ await SystemProxy.disable();
599
+ saveMitmProxyConfig({ ...config, systemProxy: false });
600
+ }
601
+ }
602
+ });
603
+
604
+ ipcMain.handle("mitm-proxy:status", async () => {
605
+ const config = loadMitmProxyConfig();
606
+ // Collect local IPv4 addresses for LAN device configuration
607
+ const localIPs: string[] = [];
608
+ const nets = networkInterfaces();
609
+ for (const name of Object.keys(nets)) {
610
+ for (const net of nets[name] || []) {
611
+ if (net.family === "IPv4" && !net.internal) {
612
+ localIPs.push(net.address);
613
+ }
614
+ }
615
+ }
616
+ return {
617
+ running: deps.mitmProxy.isRunning(),
618
+ port: deps.mitmProxy.getPort(),
619
+ caInitialized: deps.caManager.isInitialized(),
620
+ caInstalled: config.caInstalled,
621
+ caCertPath: deps.caManager.isInitialized() ? deps.caManager.getCaCertPath() : null,
622
+ systemProxyEnabled: config.systemProxy,
623
+ localIPs,
624
+ };
625
+ });
626
+
627
+ ipcMain.handle("mitm-proxy:installCA", async () => {
628
+ // Ensure CA is generated before trying to install
629
+ if (!deps.caManager.isInitialized()) {
630
+ await deps.caManager.init();
631
+ }
632
+ const result = await CertInstaller.install(deps.caManager.getCaCertPath());
633
+ if (result.success) {
634
+ const config = loadMitmProxyConfig();
635
+ saveMitmProxyConfig({ ...config, caInstalled: true });
636
+ }
637
+ return result;
638
+ });
639
+
640
+ ipcMain.handle("mitm-proxy:uninstallCA", async () => {
641
+ if (!deps.caManager.isInitialized()) {
642
+ await deps.caManager.init();
643
+ }
644
+ const result = await CertInstaller.uninstall(deps.caManager.getCaCertPath());
645
+ if (result.success) {
646
+ const config = loadMitmProxyConfig();
647
+ saveMitmProxyConfig({ ...config, caInstalled: false });
648
+ }
649
+ return result;
650
+ });
651
+
652
+ ipcMain.handle("mitm-proxy:exportCA", async () => {
653
+ if (!deps.caManager.isInitialized()) {
654
+ await deps.caManager.init();
655
+ }
656
+ const { dialog } = await import("electron");
657
+ const win = deps.windowManager.getMainWindow();
658
+ if (!win) return false;
659
+ const certPath = deps.caManager.getCaCertPath();
660
+ const { canceled, filePath } = await dialog.showSaveDialog(win, {
661
+ defaultPath: "anything-analyzer-ca.crt",
662
+ filters: [
663
+ { name: "Certificate", extensions: ["crt", "pem"] },
664
+ { name: "All Files", extensions: ["*"] },
665
+ ],
666
+ });
667
+ if (canceled || !filePath) return false;
668
+ const { readFileSync, writeFileSync } = await import("fs");
669
+ writeFileSync(filePath, readFileSync(certPath));
670
+ return true;
671
+ });
672
+
673
+ ipcMain.handle("mitm-proxy:regenerateCA", async () => {
674
+ if (deps.mitmProxy.isRunning()) await deps.mitmProxy.stop();
675
+ await deps.caManager.regenerate();
676
+ const config = loadMitmProxyConfig();
677
+ saveMitmProxyConfig({ ...config, caInstalled: false });
678
+ });
679
+
680
+ ipcMain.handle("mitm-proxy:enableSystemProxy", async () => {
681
+ const config = loadMitmProxyConfig();
682
+ const result = await SystemProxy.enable(config.port);
683
+ if (result.success) {
684
+ saveMitmProxyConfig({ ...config, systemProxy: true });
685
+ }
686
+ return result;
687
+ });
688
+
689
+ ipcMain.handle("mitm-proxy:disableSystemProxy", async () => {
690
+ const result = await SystemProxy.disable();
691
+ if (result.success) {
692
+ const config = loadMitmProxyConfig();
693
+ saveMitmProxyConfig({ ...config, systemProxy: false });
694
+ }
695
+ return result;
696
+ });
697
+
698
+ // ---- Shell ----
699
+ ipcMain.handle("shell:openExternal", async (_event, url: string) => {
700
+ const { shell } = await import("electron");
701
+ await shell.openExternal(url);
702
+ });
703
+
704
+ // ---- Fingerprint Profile ----
705
+
706
+ ipcMain.handle("fingerprint:get", async (_event, sessionId: string) => {
707
+ return profileStore.get(sessionId) ?? null;
708
+ });
709
+
710
+ ipcMain.handle("fingerprint:update", async (_event, profileJson: string) => {
711
+ const profile = JSON.parse(profileJson);
712
+ profileStore.update(profile);
713
+ });
714
+
715
+ ipcMain.handle("fingerprint:regenerate", async (_event, sessionId: string) => {
716
+ return profileStore.regenerate(sessionId) ?? null;
717
+ });
718
+
719
+ ipcMain.handle("fingerprint:enable", async (_event, sessionId: string) => {
720
+ const tabManager = windowManager.getTabManager();
721
+ if (!tabManager) throw new Error("Browser not ready");
722
+ const proxyConfig = loadProxyConfig();
723
+ await sessionManager.enableStealth(sessionId, tabManager, proxyConfig);
724
+ });
725
+
726
+ ipcMain.handle("fingerprint:disable", async () => {
727
+ await sessionManager.disableStealth();
728
+ });
729
+
730
+ // ---- Interaction Recording ----
731
+
732
+ ipcMain.handle("interaction:getEvents", async (_event, sessionId: string, limit?: number) => {
733
+ return interactionEventsRepo.findBySession(sessionId, limit ?? 1000);
734
+ });
735
+
736
+ ipcMain.handle("interaction:getCount", async (_event, sessionId: string) => {
737
+ return interactionEventsRepo.count(sessionId);
738
+ });
739
+
740
+ ipcMain.handle("interaction:clear", async (_event, sessionId: string) => {
741
+ interactionEventsRepo.deleteBySession(sessionId);
742
+ });
743
+
744
+ // ---- Log Files ----
745
+
746
+ ipcMain.handle("log:getPath", () => {
747
+ return join(app.getPath("userData"), "logs", "main.log");
748
+ });
749
+
750
+ ipcMain.handle("log:openFolder", async () => {
751
+ const logDir = join(app.getPath("userData"), "logs");
752
+ shell.openPath(logDir);
753
+ });
754
+
755
+ ipcMain.handle("log:export", async () => {
756
+ const logPath = join(app.getPath("userData"), "logs", "main.log");
757
+ if (!existsSync(logPath)) return false;
758
+ const mainWin = windowManager.getMainWindow();
759
+ const result = await dialog.showSaveDialog(mainWin!, {
760
+ defaultPath: `anything-analyzer-logs-${new Date().toISOString().slice(0, 10)}.log`,
761
+ filters: [{ name: "Log Files", extensions: ["log", "txt"] }],
762
+ });
763
+ if (result.canceled || !result.filePath) return false;
764
+ const { copyFileSync } = await import("fs");
765
+ copyFileSync(logPath, result.filePath);
766
+ return true;
767
+ });
768
+ }
769
+
770
+ // ---- Config persistence helpers ----
771
+
772
+ function getConfigPath(): string {
773
+ return join(app.getPath("userData"), "llm-config.json");
774
+ }
775
+
776
+ export function loadLLMConfig(): LLMProviderConfig | null {
777
+ const path = getConfigPath();
778
+ if (!existsSync(path)) return null;
779
+ try {
780
+ return JSON.parse(readFileSync(path, "utf-8")) as LLMProviderConfig;
781
+ } catch {
782
+ return null;
783
+ }
784
+ }
785
+
786
+ function saveLLMConfig(config: LLMProviderConfig): void {
787
+ writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
788
+ }
789
+
790
+ // ---- Proxy config persistence ----
791
+
792
+ function getProxyConfigPath(): string {
793
+ return join(app.getPath("userData"), "proxy-config.json");
794
+ }
795
+
796
+ export function loadProxyConfig(): ProxyConfig | null {
797
+ const path = getProxyConfigPath();
798
+ if (!existsSync(path)) return null;
799
+ try {
800
+ return JSON.parse(readFileSync(path, "utf-8")) as ProxyConfig;
801
+ } catch {
802
+ return null;
803
+ }
804
+ }
805
+
806
+ function saveProxyConfigFile(config: ProxyConfig): void {
807
+ writeFileSync(getProxyConfigPath(), JSON.stringify(config, null, 2), "utf-8");
808
+ }
809
+
810
+ export async function applyProxy(
811
+ config: ProxyConfig | null,
812
+ elSession: Electron.Session = session.defaultSession,
813
+ ): Promise<void> {
814
+ if (!config || config.type === "none") {
815
+ await elSession.setProxy({ mode: "direct" });
816
+ return;
817
+ }
818
+
819
+ // Chromium proxyRules do NOT support inline credentials (user:pass@host)
820
+ // — that causes ERR_NO_SUPPORTED_PROXIES. Use plain host:port instead.
821
+ // Proxy auth is handled via app.on('login') in index.ts.
822
+ const proxyRules = `${config.type}://${config.host}:${config.port}`;
823
+ await elSession.setProxy({ proxyRules });
824
+ }
825
+
826
+ // ---- MCP Server config persistence ----
827
+
828
+ const DEFAULT_MCP_SERVER_CONFIG: MCPServerSettings = { enabled: false, port: 23816, authEnabled: true, authToken: '' };
829
+
830
+ function getMCPServerConfigPath(): string {
831
+ return join(app.getPath("userData"), "mcp-server-config.json");
832
+ }
833
+
834
+ export function loadMCPServerConfig(): MCPServerSettings {
835
+ const path = getMCPServerConfigPath();
836
+ let config: MCPServerSettings;
837
+ if (!existsSync(path)) {
838
+ config = { ...DEFAULT_MCP_SERVER_CONFIG };
839
+ } else {
840
+ try {
841
+ config = { ...DEFAULT_MCP_SERVER_CONFIG, ...JSON.parse(readFileSync(path, "utf-8")) };
842
+ } catch {
843
+ config = { ...DEFAULT_MCP_SERVER_CONFIG };
844
+ }
845
+ }
846
+ // Auto-generate token if empty (first run or upgraded from old config)
847
+ if (!config.authToken) {
848
+ config.authToken = randomUUID();
849
+ saveMCPServerConfig(config);
850
+ }
851
+ return config;
852
+ }
853
+
854
+ function saveMCPServerConfig(config: MCPServerSettings): void {
855
+ writeFileSync(getMCPServerConfigPath(), JSON.stringify(config, null, 2), "utf-8");
856
+ }