@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,349 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AssembledData,
|
|
3
|
+
CryptoScriptSnippet,
|
|
4
|
+
SceneHint,
|
|
5
|
+
AuthChainItem,
|
|
6
|
+
FilteredRequest,
|
|
7
|
+
PromptTemplate,
|
|
8
|
+
RequestSummary,
|
|
9
|
+
} from "@shared/types";
|
|
10
|
+
|
|
11
|
+
interface PromptMessages {
|
|
12
|
+
system: string;
|
|
13
|
+
user: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const REVERSE_API_REQUIREMENTS = `1. 完整 API 端点清单:列出所有 API 的方法、路径、请求参数、响应 JSON 结构
|
|
17
|
+
2. 鉴权流程:Token/Cookie 获取、刷新、传递机制的完整链路
|
|
18
|
+
3. 请求依赖链:哪些请求的响应是后续请求的必要输入
|
|
19
|
+
4. 数据模型推断:从 API 响应结构推断后端数据模型
|
|
20
|
+
5. 复现代码:用 Python requests 库写出可直接运行的完整 API 调用流程`;
|
|
21
|
+
|
|
22
|
+
const SECURITY_AUDIT_REQUIREMENTS = `1. 认证安全:分析认证方式的安全性,是否存在弱口令、明文传输、Token 泄露风险
|
|
23
|
+
2. 敏感数据暴露:检查响应中是否包含不必要的敏感信息(密码、密钥、PII)
|
|
24
|
+
3. CSRF/XSS 风险:分析请求是否缺少 CSRF Token,响应头是否缺少安全头(CSP, X-Frame-Options 等)
|
|
25
|
+
4. 权限控制:分析是否存在越权访问的可能(水平/垂直越权)
|
|
26
|
+
5. 安全建议:针对发现的问题给出具体修复建议`;
|
|
27
|
+
|
|
28
|
+
const PERFORMANCE_REQUIREMENTS = `1. 请求时序分析:分析请求的串行/并行关系,识别阻塞链路
|
|
29
|
+
2. 冗余请求:识别重复或不必要的请求
|
|
30
|
+
3. 资源优化:分析资源加载顺序,识别可优化的静态资源
|
|
31
|
+
4. 缓存策略:分析 Cache-Control、ETag 等缓存头的使用情况
|
|
32
|
+
5. 性能建议:给出具体的性能优化建议和预期收益`;
|
|
33
|
+
|
|
34
|
+
const CRYPTO_REVERSE_REQUIREMENTS = `1. 加密算法识别:识别所有使用的加密/签名/哈希算法(AES、RSA、SHA、HMAC、SM2/3/4 等),标注具体库和方法名
|
|
35
|
+
2. 加密流程还原:完整描述每个请求参数的加密 pipeline(明文 → 各步骤 → 密文),画出数据流转图
|
|
36
|
+
3. 密钥管理分析:密钥来源(硬编码/动态/协商)、密钥格式(Hex/Base64/PEM)、密钥长度
|
|
37
|
+
4. 签名/校验机制:请求签名的生成算法、参与签名的参数排序规则、时间戳/nonce 机制
|
|
38
|
+
5. 复现代码:用 Python 写出完整的加密/签名/请求复现代码,确保可直接运行,包含所有必要的密钥和参数`;
|
|
39
|
+
|
|
40
|
+
const DEFAULT_REQUIREMENTS = `1. 场景识别:判断用户执行了什么操作(注册、登录、AI对话、支付等)
|
|
41
|
+
2. 交互流程概述:按时间顺序描述完整交互链路
|
|
42
|
+
3. API端点清单:列出所有关键API,标注方法、路径、用途
|
|
43
|
+
4. 鉴权机制分析:认证方式、凭据获取流程、凭据传递方式
|
|
44
|
+
5. 流式通信分析(如检测到SSE/WebSocket):协议类型、端点、请求/响应格式
|
|
45
|
+
6. 存储使用分析:Cookie/localStorage/sessionStorage 的关键变化
|
|
46
|
+
7. 关键依赖关系:请求之间的依赖和时序关系
|
|
47
|
+
8. 复现建议:用代码伪逻辑描述如何复现整个流程`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* PromptBuilder — Builds the analysis prompt from assembled data.
|
|
51
|
+
*/
|
|
52
|
+
export class PromptBuilder {
|
|
53
|
+
build(
|
|
54
|
+
data: AssembledData,
|
|
55
|
+
platformName: string,
|
|
56
|
+
purpose?: string,
|
|
57
|
+
template?: PromptTemplate,
|
|
58
|
+
allSummaries?: RequestSummary[],
|
|
59
|
+
): PromptMessages {
|
|
60
|
+
const hasToolAccess = allSummaries && allSummaries.length > data.requests.length;
|
|
61
|
+
|
|
62
|
+
const toolHint = hasToolAccess
|
|
63
|
+
? '\n你可以使用 get_request_detail 工具来查看任意请求的完整内容(包括被过滤的请求)。当你发现分析信息不足时,主动调用此工具获取更多细节。'
|
|
64
|
+
: '';
|
|
65
|
+
|
|
66
|
+
const system = (template?.systemPrompt
|
|
67
|
+
|| `你是一位网站协议分析专家。你的任务是分析用户在网站上的操作过程中产生的HTTP请求、JS调用和存储变化,识别其业务场景,并生成结构化的协议分析报告。Be precise and technical. Output in Chinese (Simplified).`) + toolHint;
|
|
68
|
+
|
|
69
|
+
const analysisRequirements = template?.requirements
|
|
70
|
+
|| this.buildAnalysisRequirements(purpose);
|
|
71
|
+
const requestsSection = this.formatRequests(data.requests);
|
|
72
|
+
const hooksSection = this.formatHooks(data.requests);
|
|
73
|
+
const storageSection = this.formatStorageDiff(data.storageDiff);
|
|
74
|
+
const sceneSection = this.formatSceneHints(data.sceneHints);
|
|
75
|
+
const authSection = this.formatAuthChain(data.authChain);
|
|
76
|
+
const streamingSection = this.formatStreamingRequests(
|
|
77
|
+
data.streamingRequests,
|
|
78
|
+
);
|
|
79
|
+
const cryptoHooksSection = this.formatCryptoHooks(data.requests);
|
|
80
|
+
const cryptoScriptsSection = this.formatCryptoScripts(data.cryptoScripts);
|
|
81
|
+
|
|
82
|
+
// 完整请求索引(仅当 Phase 1 过滤生效时添加)
|
|
83
|
+
const requestIndexSection = hasToolAccess
|
|
84
|
+
? this.formatRequestIndex(allSummaries!, data.requests.length)
|
|
85
|
+
: '';
|
|
86
|
+
|
|
87
|
+
const user = `以下是用户在 ${platformName} 上操作时的完整数据。
|
|
88
|
+
|
|
89
|
+
## 场景线索
|
|
90
|
+
${sceneSection}
|
|
91
|
+
|
|
92
|
+
## 鉴权链
|
|
93
|
+
${authSection}
|
|
94
|
+
|
|
95
|
+
## 流式通信
|
|
96
|
+
${streamingSection}
|
|
97
|
+
|
|
98
|
+
## 请求日志
|
|
99
|
+
${requestsSection}
|
|
100
|
+
|
|
101
|
+
## JS Hook 数据
|
|
102
|
+
${hooksSection}
|
|
103
|
+
|
|
104
|
+
## 加密操作记录
|
|
105
|
+
${cryptoHooksSection}
|
|
106
|
+
|
|
107
|
+
## 相关加密代码片段
|
|
108
|
+
${cryptoScriptsSection}
|
|
109
|
+
|
|
110
|
+
## 存储变化
|
|
111
|
+
${storageSection}
|
|
112
|
+
${requestIndexSection}
|
|
113
|
+
## 分析要求
|
|
114
|
+
${analysisRequirements}`;
|
|
115
|
+
|
|
116
|
+
return { system, user };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Phase 1:构建轻量级预过滤 prompt,用于 AI 判断请求相关性
|
|
121
|
+
*/
|
|
122
|
+
buildFilterPrompt(
|
|
123
|
+
summaries: RequestSummary[],
|
|
124
|
+
sceneHints: SceneHint[],
|
|
125
|
+
purpose?: string,
|
|
126
|
+
template?: PromptTemplate,
|
|
127
|
+
): PromptMessages {
|
|
128
|
+
const system = `你是一个HTTP请求相关性过滤器。给定请求摘要列表和分析目的,判断哪些请求与分析目的相关。
|
|
129
|
+
仅返回JSON数组,包含相关请求的序号。例如:[1, 3, 5, 8]
|
|
130
|
+
宁可多选也不要遗漏——如果一个请求可能相关,就包含它。
|
|
131
|
+
不要返回任何其他内容,只返回JSON数组。`;
|
|
132
|
+
|
|
133
|
+
const analysisRequirements = template?.requirements
|
|
134
|
+
|| this.buildAnalysisRequirements(purpose);
|
|
135
|
+
const sceneSection = this.formatSceneHints(sceneHints);
|
|
136
|
+
|
|
137
|
+
const summaryLines = summaries.map(s => {
|
|
138
|
+
const ct = s.contentType ? ` [${s.contentType.split(';')[0].trim()}]` : '';
|
|
139
|
+
return `#${s.seq} ${s.method} ${s.url} -> ${s.status ?? 'pending'}${ct}`;
|
|
140
|
+
}).join('\n');
|
|
141
|
+
|
|
142
|
+
const user = `## 分析目的
|
|
143
|
+
${analysisRequirements}
|
|
144
|
+
|
|
145
|
+
## 场景线索
|
|
146
|
+
${sceneSection}
|
|
147
|
+
|
|
148
|
+
## 请求摘要(共 ${summaries.length} 条)
|
|
149
|
+
${summaryLines}
|
|
150
|
+
|
|
151
|
+
请返回与分析目的相关的请求序号JSON数组。包含直接相关和支撑性请求(如认证请求)。`;
|
|
152
|
+
|
|
153
|
+
return { system, user };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private buildAnalysisRequirements(purpose?: string): string {
|
|
157
|
+
if (!purpose || purpose === "auto") {
|
|
158
|
+
return DEFAULT_REQUIREMENTS;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const predefinedMap: Record<string, string> = {
|
|
162
|
+
"reverse-api": REVERSE_API_REQUIREMENTS,
|
|
163
|
+
"security-audit": SECURITY_AUDIT_REQUIREMENTS,
|
|
164
|
+
performance: PERFORMANCE_REQUIREMENTS,
|
|
165
|
+
"crypto-reverse": CRYPTO_REVERSE_REQUIREMENTS,
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (predefinedMap[purpose]) {
|
|
169
|
+
return predefinedMap[purpose];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return `用户指定的分析重点:${purpose}
|
|
173
|
+
|
|
174
|
+
在完成上述重点分析的同时,也请覆盖以下基础分析:
|
|
175
|
+
${DEFAULT_REQUIREMENTS}`;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private formatSceneHints(hints: SceneHint[]): string {
|
|
179
|
+
if (hints.length === 0) return "(无场景线索)";
|
|
180
|
+
return hints
|
|
181
|
+
.map((h) => `- **${h.scene}** [${h.confidence}]: ${h.evidence}`)
|
|
182
|
+
.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private formatAuthChain(chain: AuthChainItem[]): string {
|
|
186
|
+
if (chain.length === 0) return "(无鉴权数据)";
|
|
187
|
+
return chain
|
|
188
|
+
.map((a) => {
|
|
189
|
+
const consumers =
|
|
190
|
+
a.consumers.length > 0 ? `\n 使用者: ${a.consumers.join(", ")}` : "";
|
|
191
|
+
return `- **${a.credentialType}** (来源: ${a.source})${consumers}`;
|
|
192
|
+
})
|
|
193
|
+
.join("\n");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private formatStreamingRequests(requests: FilteredRequest[]): string {
|
|
197
|
+
if (requests.length === 0) return "(无流式通信)";
|
|
198
|
+
return requests.map((r) => `- #${r.seq} ${r.method} ${r.url}`).join("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private formatRequests(requests: AssembledData["requests"]): string {
|
|
202
|
+
if (requests.length === 0) return "(无请求记录)";
|
|
203
|
+
return requests
|
|
204
|
+
.map((r) => {
|
|
205
|
+
const lines: string[] = [
|
|
206
|
+
`#${r.seq} ${r.method} ${r.url} → ${r.status || "pending"}`,
|
|
207
|
+
];
|
|
208
|
+
const important = this.filterHeaders(r.headers);
|
|
209
|
+
if (Object.keys(important).length > 0)
|
|
210
|
+
lines.push(` Headers: ${JSON.stringify(important)}`);
|
|
211
|
+
if (r.body)
|
|
212
|
+
lines.push(
|
|
213
|
+
` Body: ${this.sanitizeBody(r.body, 2000)}`,
|
|
214
|
+
);
|
|
215
|
+
if (r.responseBody)
|
|
216
|
+
lines.push(
|
|
217
|
+
` Response: ${this.sanitizeBody(r.responseBody, 2000)}`,
|
|
218
|
+
);
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
})
|
|
221
|
+
.join("\n\n");
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private formatHooks(requests: AssembledData["requests"]): string {
|
|
225
|
+
const allHooks = requests.flatMap((r) => r.hooks);
|
|
226
|
+
if (allHooks.length === 0) return "(无 JS Hook 记录)";
|
|
227
|
+
return allHooks
|
|
228
|
+
.map(
|
|
229
|
+
(h) =>
|
|
230
|
+
`[${h.hook_type}] ${h.function_name}: args=${this.sanitizeBody(h.arguments, 500)}${h.result ? ` result=${this.sanitizeBody(h.result, 500)}` : ""}`,
|
|
231
|
+
)
|
|
232
|
+
.join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private formatStorageDiff(diff: AssembledData["storageDiff"]): string {
|
|
236
|
+
const sections: string[] = [];
|
|
237
|
+
for (const [type, d] of Object.entries(diff)) {
|
|
238
|
+
const parts: string[] = [];
|
|
239
|
+
if (Object.keys(d.added).length > 0)
|
|
240
|
+
parts.push(
|
|
241
|
+
` 新增: ${Object.entries(d.added)
|
|
242
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
243
|
+
.join(", ")}`,
|
|
244
|
+
);
|
|
245
|
+
if (Object.keys(d.changed).length > 0)
|
|
246
|
+
parts.push(
|
|
247
|
+
` 变更: ${Object.entries(d.changed)
|
|
248
|
+
.map(([k, v]) => `${k}: "${v.old}" → "${v.new}"`)
|
|
249
|
+
.join(", ")}`,
|
|
250
|
+
);
|
|
251
|
+
if (d.removed.length > 0) parts.push(` 删除: ${d.removed.join(", ")}`);
|
|
252
|
+
if (parts.length > 0) sections.push(`${type}:\n${parts.join("\n")}`);
|
|
253
|
+
}
|
|
254
|
+
return sections.length > 0 ? sections.join("\n\n") : "(无存储变化)";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private formatCryptoHooks(requests: AssembledData['requests']): string {
|
|
258
|
+
const cryptoHooks = requests.flatMap(r => r.hooks).filter(
|
|
259
|
+
h => h.hook_type === 'crypto' || h.hook_type === 'crypto_lib'
|
|
260
|
+
);
|
|
261
|
+
if (cryptoHooks.length === 0) return '(无加密操作记录)';
|
|
262
|
+
|
|
263
|
+
// Group by function name
|
|
264
|
+
const groups = new Map<string, typeof cryptoHooks>();
|
|
265
|
+
for (const h of cryptoHooks) {
|
|
266
|
+
const key = h.function_name;
|
|
267
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
268
|
+
groups.get(key)!.push(h);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const lines: string[] = [];
|
|
272
|
+
for (const [funcName, hooks] of groups) {
|
|
273
|
+
lines.push(`- **${funcName}** (${hooks.length}次调用)`);
|
|
274
|
+
// Show up to 3 representative calls
|
|
275
|
+
for (const h of hooks.slice(0, 3)) {
|
|
276
|
+
const args = this.sanitizeBody(h.arguments, 200);
|
|
277
|
+
const result = h.result ? this.sanitizeBody(h.result, 200) : '';
|
|
278
|
+
lines.push(` args=${args}${result ? ` → ${result}` : ''}`);
|
|
279
|
+
if (h.call_stack) {
|
|
280
|
+
const topFrame = h.call_stack.split('\n')[0]?.trim();
|
|
281
|
+
if (topFrame) lines.push(` 来源: ${topFrame}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (hooks.length > 3) lines.push(` ...及其他 ${hooks.length - 3} 次调用`);
|
|
285
|
+
}
|
|
286
|
+
return lines.join('\n');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private formatCryptoScripts(snippets: CryptoScriptSnippet[]): string {
|
|
290
|
+
if (!snippets || snippets.length === 0) return '(无相关加密代码)';
|
|
291
|
+
return snippets.map(s => {
|
|
292
|
+
const patterns = s.matchedPatterns.join(', ');
|
|
293
|
+
return `### ${s.scriptUrl} (行 ${s.lineRange[0]}-${s.lineRange[1]})\n匹配: ${patterns}\n\`\`\`javascript\n${s.content}\n\`\`\``;
|
|
294
|
+
}).join('\n\n');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
private formatRequestIndex(summaries: RequestSummary[], analysisCount: number): string {
|
|
298
|
+
const lines = summaries.map(s => {
|
|
299
|
+
const ct = s.contentType ? ` [${s.contentType.split(';')[0].trim()}]` : '';
|
|
300
|
+
return `#${s.seq} ${s.method} ${s.url} -> ${s.status ?? 'pending'}${ct}`;
|
|
301
|
+
});
|
|
302
|
+
return `
|
|
303
|
+
## 完整请求索引(包含被过滤的请求)
|
|
304
|
+
以下是本次会话中所有 ${summaries.length} 条请求的摘要(当前深度分析仅包含其中 ${analysisCount} 条)。
|
|
305
|
+
如果你认为被过滤的请求可能与分析相关,可以调用 get_request_detail 工具获取其完整内容。
|
|
306
|
+
|
|
307
|
+
${lines.join('\n')}
|
|
308
|
+
|
|
309
|
+
`;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Sanitize body content for safe embedding in prompts sent to LLM APIs.
|
|
314
|
+
* Removes control characters and non-printable chars that may break JSON
|
|
315
|
+
* parsing in intermediate proxies (e.g., Go-based API gateways).
|
|
316
|
+
*/
|
|
317
|
+
private sanitizeBody(text: string, maxLen: number): string {
|
|
318
|
+
// Remove ASCII control chars (except \n \r \t) and Unicode replacement char
|
|
319
|
+
const cleaned = text.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\uFFFD]/g, '');
|
|
320
|
+
if (cleaned.length <= maxLen) return cleaned;
|
|
321
|
+
// Unicode-safe truncation: avoid cutting surrogate pairs
|
|
322
|
+
let end = maxLen;
|
|
323
|
+
const code = cleaned.charCodeAt(end - 1);
|
|
324
|
+
if (code >= 0xD800 && code <= 0xDBFF) end--;
|
|
325
|
+
return cleaned.substring(0, end) + '...';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private filterHeaders(
|
|
329
|
+
headers: Record<string, string>,
|
|
330
|
+
): Record<string, string> {
|
|
331
|
+
const important = [
|
|
332
|
+
"authorization",
|
|
333
|
+
"x-token",
|
|
334
|
+
"x-csrf-token",
|
|
335
|
+
"x-request-id",
|
|
336
|
+
"x-signature",
|
|
337
|
+
"content-type",
|
|
338
|
+
"cookie",
|
|
339
|
+
"referer",
|
|
340
|
+
"origin",
|
|
341
|
+
"user-agent",
|
|
342
|
+
];
|
|
343
|
+
const result: Record<string, string> = {};
|
|
344
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
345
|
+
if (important.includes(key.toLowerCase())) result[key] = value;
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import type { FilteredRequest, SceneHint } from '@shared/types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SceneDetector — Detects business scenarios from captured requests using rule-based heuristics.
|
|
5
|
+
*
|
|
6
|
+
* 职责:在数据送给 AI 之前,基于规则对捕获的请求数据做一轮场景预判,
|
|
7
|
+
* 输出 SceneHint[] 供 PromptBuilder 使用。规则是辅助而非决策,最终场景判断仍由 AI 完成。
|
|
8
|
+
*/
|
|
9
|
+
export class SceneDetector {
|
|
10
|
+
/**
|
|
11
|
+
* Detects all applicable scenes from a list of requests.
|
|
12
|
+
* 检测场景:一次分析可命中多个场景(如登录 + AI对话)
|
|
13
|
+
*
|
|
14
|
+
* 优化:单次遍历所有请求,为每个请求生成所有匹配的场景,避免 O(11N) 复杂度
|
|
15
|
+
*/
|
|
16
|
+
detect(requests: FilteredRequest[]): SceneHint[] {
|
|
17
|
+
const scenesMap = new Map<string, { hint: SceneHint; count: number }>()
|
|
18
|
+
const trackScenes: SceneHint[] = []
|
|
19
|
+
|
|
20
|
+
// 单次遍历所有请求
|
|
21
|
+
for (const req of requests) {
|
|
22
|
+
// AI Chat - SSE 响应
|
|
23
|
+
if (this.isSSEResponse(req) && this.matchesAiApiPattern(req)) {
|
|
24
|
+
this.addSceneHint(scenesMap, 'ai-chat', 'high', 'SSE 响应检测到 text/event-stream', req.seq)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// AI Chat - API 路径特征
|
|
28
|
+
if (this.matchesAiApiPattern(req)) {
|
|
29
|
+
this.addSceneHint(scenesMap, 'ai-chat', 'high', 'API 特征路径检测', req.seq)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// AI Chat - 请求体特征
|
|
33
|
+
if (this.hasAiPayloadFields(req)) {
|
|
34
|
+
this.addSceneHint(scenesMap, 'ai-chat', 'medium', 'AI 典型字段', req.seq)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// OAuth 场景
|
|
38
|
+
if (this.matchesOAuthPattern(req)) {
|
|
39
|
+
this.addSceneHint(scenesMap, 'auth-oauth', 'high', '/oauth 路径或 redirect_uri 参数', req.seq)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Token 鉴权
|
|
43
|
+
if (this.hasTokenInResponse(req) || this.hasBearerAuth(req)) {
|
|
44
|
+
this.addSceneHint(scenesMap, 'auth-token', 'high', 'Token 鉴权', req.seq)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Session 鉴权
|
|
48
|
+
if (this.hasSessionCookie(req)) {
|
|
49
|
+
this.addSceneHint(scenesMap, 'auth-session', 'medium', 'Set-Cookie 响应', req.seq)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 注册场景
|
|
53
|
+
if (this.matchesRegistrationPattern(req)) {
|
|
54
|
+
this.addSceneHint(scenesMap, 'registration', 'high', '/register|/signup 路径和 email/password 字段', req.seq)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 登录场景
|
|
58
|
+
if (this.matchesLoginPattern(req)) {
|
|
59
|
+
this.addSceneHint(scenesMap, 'login', 'high', '/login|/signin 路径和 password 字段', req.seq)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// WebSocket 场景
|
|
63
|
+
if (this.isWebSocketRequest(req)) {
|
|
64
|
+
this.addSceneHint(scenesMap, 'websocket', 'high', 'Upgrade: websocket 请求头', req.seq)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// SSE 流场景 - 仅在非 AI Chat 时标记为 sse-stream
|
|
68
|
+
if (this.isSSEResponse(req) && !this.matchesAiApiPattern(req)) {
|
|
69
|
+
this.addSceneHint(scenesMap, 'sse-stream', 'high', 'SSE 流响应', req.seq)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 通用 API 场景
|
|
73
|
+
if (this.isJsonResponse(req)) {
|
|
74
|
+
this.addSceneHint(scenesMap, 'api-general', 'low', 'JSON API 请求/响应', req.seq)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 加密操作场景
|
|
78
|
+
if (this.hasCryptoHooks(req)) {
|
|
79
|
+
this.addSceneHint(scenesMap, 'crypto-encryption', 'high', '检测到加密/签名/哈希操作 Hook', req.seq)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 签名请求头场景
|
|
83
|
+
if (this.hasSignatureHeaders(req)) {
|
|
84
|
+
this.addSceneHint(scenesMap, 'crypto-encryption', 'medium', '请求包含签名/加密相关 Header', req.seq)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Array.from(scenesMap.values()).map(entry => entry.hint)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 辅助方法:添加或更新场景
|
|
93
|
+
*/
|
|
94
|
+
private addSceneHint(
|
|
95
|
+
scenesMap: Map<string, { hint: SceneHint; count: number }>,
|
|
96
|
+
scene: string,
|
|
97
|
+
confidence: 'high' | 'medium' | 'low',
|
|
98
|
+
evidence: string,
|
|
99
|
+
requestSeq: number
|
|
100
|
+
): void {
|
|
101
|
+
const key = `${scene}:${confidence}`
|
|
102
|
+
const existing = scenesMap.get(key)
|
|
103
|
+
|
|
104
|
+
if (existing) {
|
|
105
|
+
existing.count++
|
|
106
|
+
if (!existing.hint.relatedRequestIds.includes(`#${requestSeq}`)) {
|
|
107
|
+
existing.hint.relatedRequestIds.push(`#${requestSeq}`)
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
scenesMap.set(key, {
|
|
111
|
+
hint: {
|
|
112
|
+
scene,
|
|
113
|
+
confidence,
|
|
114
|
+
evidence,
|
|
115
|
+
relatedRequestIds: [`#${requestSeq}`]
|
|
116
|
+
},
|
|
117
|
+
count: 1
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 检查请求是否为 SSE 响应
|
|
124
|
+
*/
|
|
125
|
+
private isSSEResponse(req: FilteredRequest): boolean {
|
|
126
|
+
const contentType = req.responseHeaders?.['content-type']?.toLowerCase() || ''
|
|
127
|
+
return contentType.includes('text/event-stream')
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 检查请求是否匹配 AI API 路径特征
|
|
132
|
+
*/
|
|
133
|
+
private matchesAiApiPattern(req: FilteredRequest): boolean {
|
|
134
|
+
const aiPatterns = [
|
|
135
|
+
'/chat/completions',
|
|
136
|
+
'/v1/messages',
|
|
137
|
+
'/v1/chat/completions',
|
|
138
|
+
'/api/chat',
|
|
139
|
+
'/api/completions',
|
|
140
|
+
'/openai/v1',
|
|
141
|
+
'/claude/messages',
|
|
142
|
+
'/generate'
|
|
143
|
+
]
|
|
144
|
+
return aiPatterns.some(pattern => req.url.toLowerCase().includes(pattern.toLowerCase()))
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 检查请求体是否包含 AI 典型字段
|
|
149
|
+
*/
|
|
150
|
+
private hasAiPayloadFields(req: FilteredRequest): boolean {
|
|
151
|
+
if (!req.body) return false
|
|
152
|
+
try {
|
|
153
|
+
const bodyObj = JSON.parse(req.body)
|
|
154
|
+
const bodyStr = JSON.stringify(bodyObj).toLowerCase()
|
|
155
|
+
const aiPayloadPatterns = ['messages', 'model', 'stream', 'temperature', 'max_tokens', 'prompt']
|
|
156
|
+
const matchCount = aiPayloadPatterns.filter(p => bodyStr.includes(p.toLowerCase())).length
|
|
157
|
+
return matchCount >= 2
|
|
158
|
+
} catch {
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 检查请求是否匹配 OAuth 模式
|
|
165
|
+
*/
|
|
166
|
+
private matchesOAuthPattern(req: FilteredRequest): boolean {
|
|
167
|
+
const oauthPatterns = ['/oauth/authorize', '/oauth/token', '/oauth2/authorize', '/oauth2/token']
|
|
168
|
+
const urlLower = req.url.toLowerCase()
|
|
169
|
+
const hasOAuthPath = oauthPatterns.some(p => urlLower.includes(p.toLowerCase()))
|
|
170
|
+
const hasRedirectUri = urlLower.includes('redirect_uri')
|
|
171
|
+
return hasOAuthPath || hasRedirectUri
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 检查响应中是否有 Token(严格的 JSON 字段检查)
|
|
176
|
+
*/
|
|
177
|
+
private hasTokenInResponse(req: FilteredRequest): boolean {
|
|
178
|
+
if (!req.responseBody) return false
|
|
179
|
+
try {
|
|
180
|
+
const bodyObj = JSON.parse(req.responseBody)
|
|
181
|
+
return !!(
|
|
182
|
+
bodyObj?.access_token ||
|
|
183
|
+
bodyObj?.refresh_token ||
|
|
184
|
+
bodyObj?.token ||
|
|
185
|
+
bodyObj?.auth_token ||
|
|
186
|
+
bodyObj?.id_token
|
|
187
|
+
)
|
|
188
|
+
} catch {
|
|
189
|
+
return false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 检查请求中是否有 Bearer Token(严格检查)
|
|
195
|
+
*/
|
|
196
|
+
private hasBearerAuth(req: FilteredRequest): boolean {
|
|
197
|
+
const authHeader = Object.entries(req.headers || {}).find(
|
|
198
|
+
([key]) => key.toLowerCase() === 'authorization'
|
|
199
|
+
)?.[1] || ''
|
|
200
|
+
return authHeader.toLowerCase().startsWith('bearer ') && authHeader.length > 10
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 检查是否是注册请求
|
|
205
|
+
*/
|
|
206
|
+
private matchesRegistrationPattern(req: FilteredRequest): boolean {
|
|
207
|
+
const regPatterns = ['/register', '/signup', '/sign-up', '/user/register', '/api/register']
|
|
208
|
+
const urlLower = req.url.toLowerCase()
|
|
209
|
+
const hasRegPath = regPatterns.some(p => urlLower.includes(p.toLowerCase()))
|
|
210
|
+
|
|
211
|
+
if (!hasRegPath) return false
|
|
212
|
+
|
|
213
|
+
if (req.method !== 'POST') return false
|
|
214
|
+
|
|
215
|
+
// 检查请求体是否包含 email/password
|
|
216
|
+
if (req.body) {
|
|
217
|
+
const bodyLower = req.body.toLowerCase()
|
|
218
|
+
return bodyLower.includes('email') || bodyLower.includes('password')
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return false
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 检查是否是登录请求
|
|
226
|
+
*/
|
|
227
|
+
private matchesLoginPattern(req: FilteredRequest): boolean {
|
|
228
|
+
const loginPatterns = ['/login', '/signin', '/sign-in', '/user/login', '/api/login', '/auth/login']
|
|
229
|
+
const urlLower = req.url.toLowerCase()
|
|
230
|
+
const hasLoginPath = loginPatterns.some(p => urlLower.includes(p.toLowerCase()))
|
|
231
|
+
|
|
232
|
+
if (!hasLoginPath) return false
|
|
233
|
+
|
|
234
|
+
if (req.method !== 'POST') return false
|
|
235
|
+
|
|
236
|
+
// 检查请求体是否包含 password
|
|
237
|
+
if (req.body) {
|
|
238
|
+
const bodyLower = req.body.toLowerCase()
|
|
239
|
+
return bodyLower.includes('password') || bodyLower.includes('passwd')
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 检查是否是 WebSocket 请求
|
|
247
|
+
*/
|
|
248
|
+
private isWebSocketRequest(req: FilteredRequest): boolean {
|
|
249
|
+
const upgrade = Object.entries(req.headers || {}).find(
|
|
250
|
+
([key]) => key.toLowerCase() === 'upgrade'
|
|
251
|
+
)?.[1]
|
|
252
|
+
return upgrade?.toLowerCase() === 'websocket'
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* 检查是否有 Session Cookie
|
|
257
|
+
*/
|
|
258
|
+
private hasSessionCookie(req: FilteredRequest): boolean {
|
|
259
|
+
const cookie = Object.entries(req.headers || {}).find(
|
|
260
|
+
([key]) => key.toLowerCase() === 'cookie'
|
|
261
|
+
)?.[1]
|
|
262
|
+
return !!cookie
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* 检查是否是 JSON 响应
|
|
267
|
+
*/
|
|
268
|
+
private isJsonResponse(req: FilteredRequest): boolean {
|
|
269
|
+
const contentType = req.responseHeaders?.['content-type']?.toLowerCase() || ''
|
|
270
|
+
return contentType.includes('json') || (req.responseBody && this.isJsonString(req.responseBody))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 检查字符串是否为有效的 JSON
|
|
276
|
+
*/
|
|
277
|
+
private isJsonString(str: string): boolean {
|
|
278
|
+
try {
|
|
279
|
+
JSON.parse(str)
|
|
280
|
+
return true
|
|
281
|
+
} catch {
|
|
282
|
+
return false
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 检查请求是否关联了加密类 Hook
|
|
288
|
+
*/
|
|
289
|
+
private hasCryptoHooks(req: FilteredRequest): boolean {
|
|
290
|
+
return req.hooks.some(h => h.hook_type === 'crypto' || h.hook_type === 'crypto_lib')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* 检查请求头中是否有签名/加密相关 Header
|
|
295
|
+
*/
|
|
296
|
+
private hasSignatureHeaders(req: FilteredRequest): boolean {
|
|
297
|
+
const signatureHeaders = ['x-signature', 'x-sign', 'x-timestamp', 'x-nonce', 'x-request-sign', 'signature']
|
|
298
|
+
return Object.keys(req.headers || {}).some(
|
|
299
|
+
key => signatureHeaders.includes(key.toLowerCase())
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
}
|