@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.
- package/.codeartsdoer/.codebaseignore +0 -0
- package/.codeartsdoer/AGENTS.md +12 -0
- package/.github/workflows/build.yml +146 -0
- package/README.en.md +264 -0
- package/README.md +276 -0
- package/RELEASE_NOTES.md +16 -0
- package/USAGE.md +490 -0
- package/color-preview-r3.html +414 -0
- package/color-preview.html +414 -0
- package/dev-app-update.yml +3 -0
- package/electron-builder.yml +36 -0
- package/electron.vite.config.ts +40 -0
- package/package.json +53 -0
- package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
- package/resources/doloffer-logo.png +0 -0
- package/resources/entitlements.mac.plist +12 -0
- package/resources/icon.ico +0 -0
- package/resources/icon.png +0 -0
- package/src/main/ai/ai-analyzer.ts +517 -0
- package/src/main/ai/crypto-script-extractor.ts +206 -0
- package/src/main/ai/data-assembler.ts +205 -0
- package/src/main/ai/llm-router.ts +1120 -0
- package/src/main/ai/prompt-builder.ts +349 -0
- package/src/main/ai/scene-detector.ts +302 -0
- package/src/main/capture/capture-engine.ts +130 -0
- package/src/main/capture/interaction-recorder.ts +171 -0
- package/src/main/capture/js-injector.ts +57 -0
- package/src/main/capture/replay-engine.ts +256 -0
- package/src/main/capture/storage-collector.ts +76 -0
- package/src/main/cdp/cdp-manager.ts +233 -0
- package/src/main/db/database.ts +41 -0
- package/src/main/db/migrations.ts +235 -0
- package/src/main/db/repositories.ts +574 -0
- package/src/main/fingerprint/http-spoofing.ts +48 -0
- package/src/main/fingerprint/presets.ts +173 -0
- package/src/main/fingerprint/profile-generator.ts +115 -0
- package/src/main/fingerprint/profile-store.ts +52 -0
- package/src/main/index.ts +260 -0
- package/src/main/ipc.ts +856 -0
- package/src/main/logger.ts +42 -0
- package/src/main/mcp/mcp-config.ts +66 -0
- package/src/main/mcp/mcp-manager.ts +155 -0
- package/src/main/mcp/mcp-server.ts +1038 -0
- package/src/main/prompt-templates.ts +170 -0
- package/src/main/proxy/ca-manager.ts +204 -0
- package/src/main/proxy/cert-download-page.ts +171 -0
- package/src/main/proxy/cert-installer.ts +242 -0
- package/src/main/proxy/mitm-proxy-config.ts +37 -0
- package/src/main/proxy/mitm-proxy-server.ts +1085 -0
- package/src/main/proxy/system-proxy.ts +248 -0
- package/src/main/session/session-manager.ts +724 -0
- package/src/main/tab-manager.ts +582 -0
- package/src/main/updater.ts +111 -0
- package/src/main/window.ts +235 -0
- package/src/preload/hook-script.ts +270 -0
- package/src/preload/index.ts +211 -0
- package/src/preload/interaction-hook.ts +286 -0
- package/src/preload/stealth-script.ts +302 -0
- package/src/preload/target-preload.ts +15 -0
- package/src/renderer/App.tsx +656 -0
- package/src/renderer/components/AiLogDetail.tsx +173 -0
- package/src/renderer/components/AiLogList.tsx +101 -0
- package/src/renderer/components/AiLogView.module.css +364 -0
- package/src/renderer/components/AiLogView.tsx +86 -0
- package/src/renderer/components/AnalyzeBar.module.css +79 -0
- package/src/renderer/components/AnalyzeBar.tsx +104 -0
- package/src/renderer/components/BrowserPanel.module.css +67 -0
- package/src/renderer/components/BrowserPanel.tsx +90 -0
- package/src/renderer/components/ControlBar.module.css +47 -0
- package/src/renderer/components/ControlBar.tsx +205 -0
- package/src/renderer/components/HookLog.tsx +132 -0
- package/src/renderer/components/InteractionLog.tsx +183 -0
- package/src/renderer/components/MCPServerModal.tsx +427 -0
- package/src/renderer/components/PromptTemplateModal.tsx +254 -0
- package/src/renderer/components/ReportView.module.css +413 -0
- package/src/renderer/components/ReportView.tsx +429 -0
- package/src/renderer/components/RequestDetail.module.css +191 -0
- package/src/renderer/components/RequestDetail.tsx +202 -0
- package/src/renderer/components/RequestLog.module.css +69 -0
- package/src/renderer/components/RequestLog.tsx +208 -0
- package/src/renderer/components/SessionList.module.css +245 -0
- package/src/renderer/components/SessionList.tsx +247 -0
- package/src/renderer/components/SettingsModal.tsx +100 -0
- package/src/renderer/components/StatusBar.module.css +44 -0
- package/src/renderer/components/StatusBar.tsx +102 -0
- package/src/renderer/components/StorageView.module.css +41 -0
- package/src/renderer/components/StorageView.tsx +178 -0
- package/src/renderer/components/TabBar.module.css +88 -0
- package/src/renderer/components/TabBar.tsx +70 -0
- package/src/renderer/components/Titlebar.module.css +254 -0
- package/src/renderer/components/Titlebar.tsx +169 -0
- package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
- package/src/renderer/components/settings/GeneralSection.tsx +164 -0
- package/src/renderer/components/settings/LLMSection.tsx +148 -0
- package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
- package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
- package/src/renderer/components/settings/ProxySection.tsx +110 -0
- package/src/renderer/css-modules.d.ts +4 -0
- package/src/renderer/hooks/useCapture.ts +383 -0
- package/src/renderer/hooks/useConfirm.tsx +91 -0
- package/src/renderer/hooks/useSession.ts +136 -0
- package/src/renderer/hooks/useTabs.ts +103 -0
- package/src/renderer/i18n/en.ts +167 -0
- package/src/renderer/i18n/index.ts +47 -0
- package/src/renderer/i18n/zh.ts +170 -0
- package/src/renderer/index.html +12 -0
- package/src/renderer/main.tsx +15 -0
- package/src/renderer/styles/global.css +144 -0
- package/src/renderer/styles/themes/ayu-dark.css +59 -0
- package/src/renderer/styles/themes/catppuccin.css +59 -0
- package/src/renderer/styles/themes/discord.css +59 -0
- package/src/renderer/styles/themes/dracula.css +59 -0
- package/src/renderer/styles/themes/github-dark.css +59 -0
- package/src/renderer/styles/themes/gruvbox.css +59 -0
- package/src/renderer/styles/themes/index.css +11 -0
- package/src/renderer/styles/themes/light.css +59 -0
- package/src/renderer/styles/themes/nord.css +59 -0
- package/src/renderer/styles/themes/one-dark.css +59 -0
- package/src/renderer/styles/themes/tokyo-night.css +59 -0
- package/src/renderer/styles/tokens.css +137 -0
- package/src/renderer/theme.ts +31 -0
- package/src/renderer/ui/Badge.module.css +38 -0
- package/src/renderer/ui/Badge.tsx +36 -0
- package/src/renderer/ui/Button.module.css +142 -0
- package/src/renderer/ui/Button.tsx +46 -0
- package/src/renderer/ui/Collapse.module.css +49 -0
- package/src/renderer/ui/Collapse.tsx +57 -0
- package/src/renderer/ui/CopyableBlock.module.css +56 -0
- package/src/renderer/ui/CopyableBlock.tsx +42 -0
- package/src/renderer/ui/Empty.module.css +19 -0
- package/src/renderer/ui/Empty.tsx +34 -0
- package/src/renderer/ui/Icons.tsx +346 -0
- package/src/renderer/ui/Input.module.css +103 -0
- package/src/renderer/ui/Input.tsx +94 -0
- package/src/renderer/ui/InputNumber.module.css +68 -0
- package/src/renderer/ui/InputNumber.tsx +104 -0
- package/src/renderer/ui/Modal.module.css +83 -0
- package/src/renderer/ui/Modal.tsx +67 -0
- package/src/renderer/ui/Popconfirm.module.css +73 -0
- package/src/renderer/ui/Popconfirm.tsx +74 -0
- package/src/renderer/ui/Progress.module.css +35 -0
- package/src/renderer/ui/Progress.tsx +30 -0
- package/src/renderer/ui/Select.module.css +91 -0
- package/src/renderer/ui/Select.tsx +100 -0
- package/src/renderer/ui/Spinner.module.css +44 -0
- package/src/renderer/ui/Spinner.tsx +27 -0
- package/src/renderer/ui/Switch.module.css +39 -0
- package/src/renderer/ui/Switch.tsx +43 -0
- package/src/renderer/ui/Tabs.module.css +76 -0
- package/src/renderer/ui/Tabs.tsx +53 -0
- package/src/renderer/ui/Tag.module.css +66 -0
- package/src/renderer/ui/Tag.tsx +47 -0
- package/src/renderer/ui/Timeline.module.css +42 -0
- package/src/renderer/ui/Timeline.tsx +29 -0
- package/src/renderer/ui/Toast.module.css +99 -0
- package/src/renderer/ui/Toast.tsx +90 -0
- package/src/renderer/ui/Tooltip.module.css +26 -0
- package/src/renderer/ui/Tooltip.tsx +23 -0
- package/src/renderer/ui/VirtualTable.module.css +230 -0
- package/src/renderer/ui/VirtualTable.tsx +416 -0
- package/src/renderer/ui/index.ts +55 -0
- package/src/shared/types.ts +695 -0
- package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
- package/tests/main/ai/llm-router.test.ts +1537 -0
- package/tests/main/ai/prompt-builder.test.ts +178 -0
- package/tests/main/ai/scene-detector.test.ts +212 -0
- package/tests/main/db/migrations.test.ts +134 -0
- package/tests/main/release-workflow.test.ts +59 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +23 -0
- package/tsconfig.web.json +24 -0
- 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
|
+
}
|