@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,206 @@
1
+ import type { CapturedRequest, JsHookRecord, CryptoScriptSnippet } from '@shared/types'
2
+ import type { RequestsRepo, JsHooksRepo } from '../db/repositories'
3
+
4
+ const CONTEXT_LINES = 30
5
+
6
+ const TIER1_PATTERNS = [
7
+ 'crypto.subtle', 'CryptoJS', 'JSEncrypt', 'forge.cipher', 'forge.pki',
8
+ 'forge.md', 'forge.hmac', 'new RSAKey', 'CryptoKey', 'sm2.doEncrypt',
9
+ 'sm3(', 'sm4.encrypt',
10
+ ]
11
+
12
+ const TIER2_PATTERNS = [
13
+ 'encrypt(', 'decrypt(', '.sign(', '.verify(', '.digest(', 'hmac',
14
+ 'AES', 'RSA', 'DES', 'SHA256', 'SHA1', 'SHA512', 'MD5', 'PBKDF2',
15
+ 'createCipher', 'createDecipher', 'createHash', 'createHmac',
16
+ 'publicKey', 'privateKey', 'secretKey',
17
+ ]
18
+
19
+ const TIER3_PATTERNS = [
20
+ 'btoa(', 'atob(', 'Base64', '.charCodeAt', 'fromCharCode',
21
+ 'TextEncoder', 'encodeURIComponent',
22
+ ]
23
+
24
+ const DEFAULT_BUDGET_CHARS = 20000
25
+
26
+ /**
27
+ * CryptoScriptExtractor — Scans stored JS response bodies for crypto-related code
28
+ * and extracts relevant snippets with surrounding context.
29
+ */
30
+ export class CryptoScriptExtractor {
31
+ constructor(
32
+ private requestsRepo: RequestsRepo,
33
+ private jsHooksRepo: JsHooksRepo,
34
+ ) {}
35
+
36
+ extract(sessionId: string, budgetChars: number = DEFAULT_BUDGET_CHARS): CryptoScriptSnippet[] {
37
+ const allRequests = this.requestsRepo.findBySession(sessionId)
38
+ const jsRequests = allRequests.filter(r => this.isJsRequest(r))
39
+
40
+ if (jsRequests.length === 0) return []
41
+
42
+ // Get hook call stacks for correlation
43
+ const hooks = this.jsHooksRepo.findBySession(sessionId)
44
+ const cryptoHooks = hooks.filter(h => h.hook_type === 'crypto' || h.hook_type === 'crypto_lib')
45
+ const hookScriptUrls = this.extractScriptUrlsFromHooks(cryptoHooks)
46
+
47
+ // Scan each JS file for crypto patterns
48
+ const allSnippets: (CryptoScriptSnippet & { hookCorrelation: number })[] = []
49
+
50
+ for (const req of jsRequests) {
51
+ if (!req.response_body) continue
52
+ const lines = req.response_body.split('\n')
53
+ const matches = this.findPatternMatches(lines)
54
+
55
+ if (matches.length === 0) continue
56
+
57
+ // Merge overlapping ranges
58
+ const merged = this.mergeRanges(matches, lines.length)
59
+
60
+ // Calculate hook correlation score
61
+ const scriptUrl = req.url
62
+ const hookCorrelation = hookScriptUrls.filter(u => scriptUrl.includes(u)).length
63
+
64
+ for (const range of merged) {
65
+ const content = lines.slice(range.start, range.end + 1).join('\n')
66
+ allSnippets.push({
67
+ scriptUrl,
68
+ lineRange: [range.start + 1, range.end + 1], // 1-based
69
+ content,
70
+ matchedPatterns: range.patterns,
71
+ tier: range.bestTier,
72
+ hookCorrelation,
73
+ })
74
+ }
75
+ }
76
+
77
+ // Sort: tier ASC, hookCorrelation DESC, then by content length ASC
78
+ allSnippets.sort((a, b) => {
79
+ if (a.tier !== b.tier) return a.tier - b.tier
80
+ if (a.hookCorrelation !== b.hookCorrelation) return b.hookCorrelation - a.hookCorrelation
81
+ return a.content.length - b.content.length
82
+ })
83
+
84
+ // Fill budget
85
+ const result: CryptoScriptSnippet[] = []
86
+ let usedChars = 0
87
+
88
+ for (const snippet of allSnippets) {
89
+ if (usedChars + snippet.content.length > budgetChars) {
90
+ // Try to fit a truncated version if at least 500 chars remain in budget
91
+ const remaining = budgetChars - usedChars
92
+ if (remaining >= 500) {
93
+ result.push({
94
+ scriptUrl: snippet.scriptUrl,
95
+ lineRange: snippet.lineRange,
96
+ content: snippet.content.substring(0, remaining) + '\n[TRUNCATED]',
97
+ matchedPatterns: snippet.matchedPatterns,
98
+ tier: snippet.tier,
99
+ })
100
+ usedChars = budgetChars
101
+ }
102
+ break
103
+ }
104
+ result.push({
105
+ scriptUrl: snippet.scriptUrl,
106
+ lineRange: snippet.lineRange,
107
+ content: snippet.content,
108
+ matchedPatterns: snippet.matchedPatterns,
109
+ tier: snippet.tier,
110
+ })
111
+ usedChars += snippet.content.length
112
+ }
113
+
114
+ return result
115
+ }
116
+
117
+ private isJsRequest(r: CapturedRequest): boolean {
118
+ if (r.method !== 'GET') return false
119
+ if (!r.response_body) return false
120
+ return /\.js(\?|$)/i.test(r.url) || (r.content_type?.includes('javascript') ?? false)
121
+ }
122
+
123
+ private findPatternMatches(lines: string[]): { line: number; tier: 1 | 2 | 3; pattern: string }[] {
124
+ const matches: { line: number; tier: 1 | 2 | 3; pattern: string }[] = []
125
+
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const l = lines[i]
128
+ for (const p of TIER1_PATTERNS) {
129
+ if (l.includes(p)) {
130
+ matches.push({ line: i, tier: 1, pattern: p })
131
+ break // one match per line per tier is enough
132
+ }
133
+ }
134
+ for (const p of TIER2_PATTERNS) {
135
+ if (l.includes(p)) {
136
+ matches.push({ line: i, tier: 2, pattern: p })
137
+ break
138
+ }
139
+ }
140
+ for (const p of TIER3_PATTERNS) {
141
+ if (l.includes(p)) {
142
+ matches.push({ line: i, tier: 3, pattern: p })
143
+ break
144
+ }
145
+ }
146
+ }
147
+
148
+ return matches
149
+ }
150
+
151
+ private mergeRanges(
152
+ matches: { line: number; tier: 1 | 2 | 3; pattern: string }[],
153
+ totalLines: number,
154
+ ): { start: number; end: number; patterns: string[]; bestTier: 1 | 2 | 3 }[] {
155
+ if (matches.length === 0) return []
156
+
157
+ // Expand each match to a range with context
158
+ const ranges = matches.map(m => ({
159
+ start: Math.max(0, m.line - CONTEXT_LINES),
160
+ end: Math.min(totalLines - 1, m.line + CONTEXT_LINES),
161
+ patterns: [m.pattern],
162
+ bestTier: m.tier,
163
+ }))
164
+
165
+ // Sort by start position
166
+ ranges.sort((a, b) => a.start - b.start)
167
+
168
+ // Merge overlapping
169
+ const merged: typeof ranges = [ranges[0]]
170
+ for (let i = 1; i < ranges.length; i++) {
171
+ const prev = merged[merged.length - 1]
172
+ const curr = ranges[i]
173
+ if (curr.start <= prev.end + 1) {
174
+ // Overlapping, merge
175
+ prev.end = Math.max(prev.end, curr.end)
176
+ prev.patterns = [...new Set([...prev.patterns, ...curr.patterns])]
177
+ prev.bestTier = Math.min(prev.bestTier, curr.bestTier) as 1 | 2 | 3
178
+ } else {
179
+ merged.push(curr)
180
+ }
181
+ }
182
+
183
+ return merged
184
+ }
185
+
186
+ private extractScriptUrlsFromHooks(hooks: JsHookRecord[]): string[] {
187
+ const urls: string[] = []
188
+ for (const hook of hooks) {
189
+ if (!hook.call_stack) continue
190
+ // Parse stack frames like "at funcName (https://example.com/js/api.js:142:15)"
191
+ const urlMatches = hook.call_stack.match(/https?:\/\/[^\s:)]+\.js/g)
192
+ if (urlMatches) {
193
+ for (const url of urlMatches) {
194
+ // Extract just the path portion for matching
195
+ try {
196
+ const pathname = new URL(url).pathname
197
+ if (!urls.includes(pathname)) urls.push(pathname)
198
+ } catch {
199
+ if (!urls.includes(url)) urls.push(url)
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return urls
205
+ }
206
+ }
@@ -0,0 +1,205 @@
1
+ import type { CapturedRequest, JsHookRecord, StorageSnapshot, AssembledData, FilteredRequest, StorageDiff, AuthChainItem, RequestSummary } from '@shared/types'
2
+ import type { RequestsRepo, JsHooksRepo, StorageSnapshotsRepo } from '../db/repositories'
3
+ import { SceneDetector } from './scene-detector'
4
+ import { CryptoScriptExtractor } from './crypto-script-extractor'
5
+
6
+ const STATIC_EXTENSIONS = /\.(js|css|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|ico|map)(\?|$)/i
7
+ const API_CONTENT_TYPES = ['json', 'form-urlencoded', 'multipart']
8
+ const TOKEN_BUDGET = 30000
9
+ const CHARS_PER_TOKEN = 4
10
+ const CRYPTO_BUDGET_CHARS = 20000
11
+
12
+ /**
13
+ * DataAssembler — Reads session data from SQLite, filters, associates, and budgets it.
14
+ */
15
+ export class DataAssembler {
16
+ constructor(private requestsRepo: RequestsRepo, private jsHooksRepo: JsHooksRepo, private storageSnapshotsRepo: StorageSnapshotsRepo) {}
17
+
18
+ assemble(sessionId: string): AssembledData {
19
+ const allRequests = this.requestsRepo.findBySession(sessionId)
20
+ const allHooks = this.jsHooksRepo.findBySession(sessionId)
21
+ const allSnapshots = this.storageSnapshotsRepo.findBySession(sessionId)
22
+
23
+ const filteredRequests = allRequests.filter(r => this.isRelevantRequest(r))
24
+
25
+ const assembledRequests = filteredRequests.map(r => {
26
+ const relatedHooks = allHooks.filter(h => Math.abs(h.timestamp - r.timestamp) <= 2000)
27
+ return {
28
+ seq: r.sequence, method: r.method, url: r.url,
29
+ headers: this.safeParseJson(r.request_headers) || {},
30
+ body: r.request_body, status: r.status_code,
31
+ responseHeaders: r.response_headers ? this.safeParseJson(r.response_headers) : null,
32
+ responseBody: r.response_body, hooks: relatedHooks
33
+ }
34
+ })
35
+
36
+ const storageDiff = {
37
+ cookies: this.calcDiff(allSnapshots, 'cookie'),
38
+ localStorage: this.calcDiff(allSnapshots, 'localStorage'),
39
+ sessionStorage: this.calcDiff(allSnapshots, 'sessionStorage')
40
+ }
41
+
42
+ this.applyTokenBudget(assembledRequests)
43
+
44
+ // Extract crypto-related JS snippets from stored JS response bodies
45
+ const cryptoExtractor = new CryptoScriptExtractor(this.requestsRepo, this.jsHooksRepo)
46
+ const cryptoScripts = cryptoExtractor.extract(sessionId, CRYPTO_BUDGET_CHARS)
47
+
48
+ const estimatedTokens = this.estimateTokens(assembledRequests, storageDiff)
49
+
50
+ return {
51
+ requests: assembledRequests,
52
+ storageDiff,
53
+ estimatedTokens,
54
+ sceneHints: new SceneDetector().detect(assembledRequests),
55
+ streamingRequests: (() => {
56
+ const rawRequestMap = new Map(allRequests.map(r => [r.sequence, r]))
57
+ return assembledRequests.filter(r => {
58
+ const rawReq = rawRequestMap.get(r.seq)
59
+ return rawReq?.is_streaming || rawReq?.is_websocket
60
+ })
61
+ })(),
62
+ authChain: this.extractAuthChain(assembledRequests),
63
+ cryptoScripts,
64
+ }
65
+ }
66
+
67
+ private isRelevantRequest(r: CapturedRequest): boolean {
68
+ if (r.method !== 'GET') return true
69
+ if (STATIC_EXTENSIONS.test(r.url)) return false
70
+ if (r.content_type && API_CONTENT_TYPES.some(t => r.content_type!.includes(t))) return true
71
+ if (r.content_type?.includes('html')) return true
72
+ if (r.request_body) return true
73
+ return false
74
+ }
75
+
76
+ private calcDiff(snapshots: StorageSnapshot[], type: string): StorageDiff {
77
+ const typed = snapshots.filter(s => s.storage_type === type)
78
+ if (typed.length < 2) return { added: {}, changed: {}, removed: [] }
79
+ const first = this.safeParseJson(typed[0].data) || {}
80
+ const last = this.safeParseJson(typed[typed.length - 1].data) || {}
81
+ const added: Record<string, string> = {}, changed: Record<string, { old: string; new: string }> = {}, removed: string[] = []
82
+ for (const key of Object.keys(last)) {
83
+ if (!(key in first)) added[key] = String(last[key])
84
+ else if (JSON.stringify(first[key]) !== JSON.stringify(last[key])) changed[key] = { old: String(first[key]), new: String(last[key]) }
85
+ }
86
+ for (const key of Object.keys(first)) { if (!(key in last)) removed.push(key) }
87
+ return { added, changed, removed }
88
+ }
89
+
90
+ private applyTokenBudget(requests: FilteredRequest[]): void {
91
+ let totalChars = requests.reduce((sum, r) => sum + JSON.stringify(r.headers).length + r.url.length + (r.body?.length || 0) + (r.responseBody?.length || 0), 0)
92
+ const budgetChars = TOKEN_BUDGET * CHARS_PER_TOKEN
93
+ if (totalChars <= budgetChars) return
94
+ const sorted = [...requests].filter(r => r.responseBody && r.responseBody.length > 500).sort((a, b) => (b.responseBody?.length || 0) - (a.responseBody?.length || 0))
95
+ for (const req of sorted) {
96
+ if (totalChars <= budgetChars) break
97
+ const currentLen = req.responseBody?.length || 0
98
+ const truncLen = Math.max(500, Math.floor(currentLen / 4))
99
+ req.responseBody = req.responseBody!.substring(0, truncLen) + '\n[TRUNCATED FOR TOKEN BUDGET]'
100
+ totalChars -= (currentLen - truncLen)
101
+ }
102
+ }
103
+
104
+ private estimateTokens(requests: FilteredRequest[], storageDiff: AssembledData['storageDiff']): number {
105
+ let chars = requests.reduce((sum, r) => sum + JSON.stringify(r).length, 0) + JSON.stringify(storageDiff).length
106
+ return Math.ceil(chars / CHARS_PER_TOKEN)
107
+ }
108
+
109
+ private safeParseJson(json: string | null): Record<string, any> | null {
110
+ if (!json) return null
111
+ try { return JSON.parse(json) } catch { return null }
112
+ }
113
+
114
+ private extractAuthChain(requests: FilteredRequest[]): AuthChainItem[] {
115
+ const authChain: AuthChainItem[] = []
116
+
117
+ for (const req of requests) {
118
+ // 检查响应中是否返回了 token
119
+ if (req.responseBody) {
120
+ try {
121
+ const data = JSON.parse(req.responseBody)
122
+ if (data.access_token) {
123
+ authChain.push({
124
+ source: `${req.method} ${new URL(req.url).pathname} 响应`,
125
+ credentialType: 'Bearer Token',
126
+ credential: this.maskCredential(data.access_token),
127
+ consumers: []
128
+ })
129
+ }
130
+ if (data.refresh_token) {
131
+ authChain.push({
132
+ source: `${req.method} ${new URL(req.url).pathname} 响应`,
133
+ credentialType: 'Refresh Token',
134
+ credential: this.maskCredential(data.refresh_token),
135
+ consumers: []
136
+ })
137
+ }
138
+ } catch { /* non-JSON response */ }
139
+ }
140
+
141
+ // 检查响应中的 Set-Cookie
142
+ if (req.responseHeaders) {
143
+ const rawSetCookie = req.responseHeaders['set-cookie'] || req.responseHeaders['Set-Cookie']
144
+ if (rawSetCookie) {
145
+ const cookies = Array.isArray(rawSetCookie) ? rawSetCookie : [rawSetCookie]
146
+ for (const cookie of cookies) {
147
+ const cookieName = String(cookie).split('=')[0]
148
+ authChain.push({
149
+ source: `${req.method} ${new URL(req.url).pathname} Set-Cookie`,
150
+ credentialType: 'Session Cookie',
151
+ credential: `${cookieName}=...`,
152
+ consumers: []
153
+ })
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ // 标记 consumers:哪些后续请求使用了这些凭据
160
+ for (const req of requests) {
161
+ const authHeader = req.headers['authorization'] || req.headers['Authorization'] || ''
162
+ if (authHeader.startsWith('Bearer ')) {
163
+ const token = authHeader.substring(7)
164
+ const matchingItem = authChain.find(a => a.credentialType === 'Bearer Token' && token.startsWith(a.credential.substring(0, 8)))
165
+ if (matchingItem) {
166
+ try { matchingItem.consumers.push(new URL(req.url).pathname) } catch { matchingItem.consumers.push(req.url) }
167
+ }
168
+ }
169
+ }
170
+
171
+ return authChain
172
+ }
173
+
174
+ private maskCredential(value: string): string {
175
+ if (value.length <= 16) return '***'
176
+ return value.substring(0, 8) + '...' + value.substring(value.length - 8)
177
+ }
178
+
179
+ /**
180
+ * 从已组装数据中提取轻量请求摘要(用于 Phase 1 预过滤)
181
+ */
182
+ extractSummaries(data: AssembledData): RequestSummary[] {
183
+ return data.requests.map(r => ({
184
+ seq: r.seq,
185
+ method: r.method,
186
+ url: r.url,
187
+ status: r.status,
188
+ contentType: r.responseHeaders?.['content-type'] ?? null,
189
+ }))
190
+ }
191
+
192
+ /**
193
+ * 按序号过滤已组装数据,保留全局上下文(sceneHints、authChain、storageDiff、cryptoScripts)
194
+ */
195
+ filterBySeqs(data: AssembledData, selectedSeqs: number[]): AssembledData {
196
+ const seqSet = new Set(selectedSeqs)
197
+ const filteredRequests = data.requests.filter(r => seqSet.has(r.seq))
198
+ return {
199
+ ...data,
200
+ requests: filteredRequests,
201
+ streamingRequests: data.streamingRequests.filter(r => seqSet.has(r.seq)),
202
+ estimatedTokens: this.estimateTokens(filteredRequests, data.storageDiff),
203
+ }
204
+ }
205
+ }