@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
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.cs.allow-jit</key>
|
|
6
|
+
<true/>
|
|
7
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
+
<true/>
|
|
9
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
10
|
+
<true/>
|
|
11
|
+
</dict>
|
|
12
|
+
</plist>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2
|
+
import type { AnalysisReport, AssembledData, FilteredRequest, LLMProviderConfig, PromptTemplate, AiRequestLogData } from "@shared/types";
|
|
3
|
+
import type {
|
|
4
|
+
SessionsRepo,
|
|
5
|
+
RequestsRepo,
|
|
6
|
+
JsHooksRepo,
|
|
7
|
+
StorageSnapshotsRepo,
|
|
8
|
+
AnalysisReportsRepo,
|
|
9
|
+
AiRequestLogRepo,
|
|
10
|
+
} from "../db/repositories";
|
|
11
|
+
import { DataAssembler } from "./data-assembler";
|
|
12
|
+
import { PromptBuilder } from "./prompt-builder";
|
|
13
|
+
import { LLMRouter } from "./llm-router";
|
|
14
|
+
import type { MCPClientManager, MCPToolInfo } from "../mcp/mcp-manager";
|
|
15
|
+
|
|
16
|
+
/** 请求数低于此值时跳过 Phase 1 预过滤 */
|
|
17
|
+
const PRE_FILTER_THRESHOLD = 20;
|
|
18
|
+
/** Phase 1 选出的请求少于此值时回退到全量分析 */
|
|
19
|
+
const PRE_FILTER_MIN_SELECTED = 3;
|
|
20
|
+
/** Phase 1 响应最大 token 数 */
|
|
21
|
+
const PHASE1_MAX_TOKENS = 1024;
|
|
22
|
+
/** 需要全量请求的分析目的(不跳过任何请求) */
|
|
23
|
+
const SKIP_FILTER_PURPOSES = ["performance"];
|
|
24
|
+
/** 对话历史最大字符数(约 25K tokens),超过时压缩旧消息 */
|
|
25
|
+
const MAX_CHAT_CONTEXT_CHARS = 100_000;
|
|
26
|
+
/** 每条工具结果保留的最大字符数 */
|
|
27
|
+
const TOOL_RESULT_MAX_CHARS = 2000;
|
|
28
|
+
/** 保留最近 N 条 assistant 消息的 tool_context(更早的会被剥离) */
|
|
29
|
+
const KEEP_TOOL_CONTEXT_RECENT = 2;
|
|
30
|
+
|
|
31
|
+
/** 内置 tool:查看请求详情 */
|
|
32
|
+
const BUILTIN_TOOLS: MCPToolInfo[] = [
|
|
33
|
+
{
|
|
34
|
+
serverName: '_builtin',
|
|
35
|
+
name: 'get_request_detail',
|
|
36
|
+
description: '获取指定序号的HTTP请求的完整详细内容,包括所有请求头、请求体、响应头和响应体。当你需要查看被过滤掉的请求或需要查看完整的请求/响应内容时使用此工具。',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
seq: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: '请求序号(从完整请求索引中获取)',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
required: ['seq'],
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* AiAnalyzer — Orchestrates data assembly, prompt building, LLM calling,
|
|
52
|
+
* and report generation.
|
|
53
|
+
*/
|
|
54
|
+
export class AiAnalyzer {
|
|
55
|
+
private mcpManager: MCPClientManager | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(
|
|
58
|
+
private sessionsRepo: SessionsRepo,
|
|
59
|
+
private requestsRepo: RequestsRepo,
|
|
60
|
+
private jsHooksRepo: JsHooksRepo,
|
|
61
|
+
private storageSnapshotsRepo: StorageSnapshotsRepo,
|
|
62
|
+
private reportsRepo: AnalysisReportsRepo,
|
|
63
|
+
private aiRequestLogRepo: AiRequestLogRepo,
|
|
64
|
+
) {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 注入 MCP 客户端管理器(可选)
|
|
68
|
+
*/
|
|
69
|
+
setMCPManager(manager: MCPClientManager): void {
|
|
70
|
+
this.mcpManager = manager;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Create a logging callback for LLMRouter that captures context via closure.
|
|
75
|
+
*/
|
|
76
|
+
private createLogCallback(
|
|
77
|
+
sessionId: string,
|
|
78
|
+
reportId: string | null,
|
|
79
|
+
type: 'analyze' | 'chat' | 'filter',
|
|
80
|
+
config: LLMProviderConfig,
|
|
81
|
+
) {
|
|
82
|
+
return (data: AiRequestLogData) => {
|
|
83
|
+
try {
|
|
84
|
+
this.aiRequestLogRepo.insert({
|
|
85
|
+
session_id: sessionId,
|
|
86
|
+
report_id: reportId,
|
|
87
|
+
type,
|
|
88
|
+
provider: config.name,
|
|
89
|
+
model: config.model,
|
|
90
|
+
...data,
|
|
91
|
+
prompt_tokens: 0,
|
|
92
|
+
completion_tokens: 0,
|
|
93
|
+
created_at: Date.now(),
|
|
94
|
+
});
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.warn('[AiRequestLog] Failed to insert log:', e);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async analyze(
|
|
102
|
+
sessionId: string,
|
|
103
|
+
config: LLMProviderConfig,
|
|
104
|
+
onProgress?: (chunk: string) => void,
|
|
105
|
+
purpose?: string,
|
|
106
|
+
template?: PromptTemplate,
|
|
107
|
+
selectedSeqs?: number[],
|
|
108
|
+
signal?: AbortSignal,
|
|
109
|
+
): Promise<AnalysisReport> {
|
|
110
|
+
// Get session info
|
|
111
|
+
const session = this.sessionsRepo.findById(sessionId);
|
|
112
|
+
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
113
|
+
|
|
114
|
+
// Extract platform name from target URL
|
|
115
|
+
let platformName = "unknown";
|
|
116
|
+
try {
|
|
117
|
+
platformName = new URL(session.target_url).hostname;
|
|
118
|
+
} catch {
|
|
119
|
+
/* ignore */
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Assemble data
|
|
123
|
+
const assembler = new DataAssembler(
|
|
124
|
+
this.requestsRepo,
|
|
125
|
+
this.jsHooksRepo,
|
|
126
|
+
this.storageSnapshotsRepo,
|
|
127
|
+
);
|
|
128
|
+
const fullData = assembler.assemble(sessionId);
|
|
129
|
+
|
|
130
|
+
if (fullData.requests.length === 0) {
|
|
131
|
+
throw new Error("No captured requests to analyze");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 手动选择模式:跳过 Phase 1,直接过滤
|
|
135
|
+
const manualSelection = selectedSeqs && selectedSeqs.length > 0;
|
|
136
|
+
let analysisData: AssembledData = fullData;
|
|
137
|
+
let filterPromptTokens: number | null = null;
|
|
138
|
+
let filterCompletionTokens: number | null = null;
|
|
139
|
+
let allSummaries = undefined as ReturnType<DataAssembler['extractSummaries']> | undefined;
|
|
140
|
+
|
|
141
|
+
if (manualSelection) {
|
|
142
|
+
analysisData = assembler.filterBySeqs(fullData, selectedSeqs);
|
|
143
|
+
onProgress?.(`> 手动选择模式:分析 ${analysisData.requests.length} 条选中的请求。\n\n`);
|
|
144
|
+
} else {
|
|
145
|
+
// Phase 1: 预过滤(可选)
|
|
146
|
+
const shouldFilter =
|
|
147
|
+
fullData.requests.length >= PRE_FILTER_THRESHOLD &&
|
|
148
|
+
!SKIP_FILTER_PURPOSES.includes(purpose ?? "");
|
|
149
|
+
|
|
150
|
+
if (shouldFilter) {
|
|
151
|
+
try {
|
|
152
|
+
onProgress?.(`> 正在过滤:分析 ${fullData.requests.length} 条请求的相关性...\n\n`);
|
|
153
|
+
|
|
154
|
+
allSummaries = assembler.extractSummaries(fullData);
|
|
155
|
+
const promptBuilder = new PromptBuilder();
|
|
156
|
+
const filterPrompt = promptBuilder.buildFilterPrompt(
|
|
157
|
+
allSummaries,
|
|
158
|
+
fullData.sceneHints,
|
|
159
|
+
purpose,
|
|
160
|
+
template,
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
const phase1Config: LLMProviderConfig = { ...config, maxTokens: PHASE1_MAX_TOKENS };
|
|
164
|
+
const phase1Router = new LLMRouter(phase1Config, this.createLogCallback(sessionId, null, 'filter', phase1Config));
|
|
165
|
+
const phase1Messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
|
|
166
|
+
{ role: "system", content: filterPrompt.system },
|
|
167
|
+
{ role: "user", content: filterPrompt.user },
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// 非流式调用
|
|
171
|
+
signal?.throwIfAborted();
|
|
172
|
+
const phase1Result = await phase1Router.complete(phase1Messages, undefined, signal);
|
|
173
|
+
filterPromptTokens = phase1Result.promptTokens;
|
|
174
|
+
filterCompletionTokens = phase1Result.completionTokens;
|
|
175
|
+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'filter', phase1Result.promptTokens, phase1Result.completionTokens);
|
|
176
|
+
|
|
177
|
+
const validSeqs = new Set(fullData.requests.map(r => r.seq));
|
|
178
|
+
const filteredSeqs = this.parseFilterResponse(phase1Result.content, validSeqs);
|
|
179
|
+
|
|
180
|
+
if (filteredSeqs && filteredSeqs.length >= PRE_FILTER_MIN_SELECTED) {
|
|
181
|
+
analysisData = assembler.filterBySeqs(fullData, filteredSeqs);
|
|
182
|
+
onProgress?.(`> 过滤完成:从 ${fullData.requests.length} 条中选出 ${filteredSeqs.length} 条相关请求进行深度分析。\n\n`);
|
|
183
|
+
} else {
|
|
184
|
+
onProgress?.(`> 过滤结果不足,使用全部 ${fullData.requests.length} 条请求分析。\n\n`);
|
|
185
|
+
allSummaries = undefined; // 未过滤,不需要完整索引
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
onProgress?.(`> 预过滤失败,使用全部 ${fullData.requests.length} 条请求分析。\n\n`);
|
|
189
|
+
allSummaries = undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Phase 2: 深度分析
|
|
195
|
+
const promptBuilder = new PromptBuilder();
|
|
196
|
+
// 仅当 Phase 1 实际过滤生效时才传入全量摘要(生成完整请求索引 + 工具提示)
|
|
197
|
+
const filteredApplied = analysisData !== fullData;
|
|
198
|
+
const { system, user } = promptBuilder.build(
|
|
199
|
+
analysisData, platformName, purpose, template,
|
|
200
|
+
filteredApplied ? allSummaries : undefined,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Call LLM with retry
|
|
204
|
+
const router = new LLMRouter(config, this.createLogCallback(sessionId, null, 'analyze', config));
|
|
205
|
+
let content = "";
|
|
206
|
+
let promptTokens = 0;
|
|
207
|
+
let completionTokens = 0;
|
|
208
|
+
|
|
209
|
+
// 构建请求查找表(内置 tool 用)
|
|
210
|
+
const requestMap = new Map(fullData.requests.map(r => [r.seq, r]));
|
|
211
|
+
|
|
212
|
+
// 仅当 Phase 1 过滤生效(非手动选择)时才提供内置 tool
|
|
213
|
+
const builtinTools = (filteredApplied && !manualSelection) ? BUILTIN_TOOLS : [];
|
|
214
|
+
const mcpTools = this.mcpManager?.hasConnections()
|
|
215
|
+
? this.mcpManager.listAllTools()
|
|
216
|
+
: [];
|
|
217
|
+
const allTools = [...builtinTools, ...mcpTools];
|
|
218
|
+
|
|
219
|
+
// tool 调用路由:内置 tool 本地处理,其他委托给 MCP
|
|
220
|
+
const mcpMgr = this.mcpManager;
|
|
221
|
+
const callTool = async (name: string, args: Record<string, unknown>): Promise<string> => {
|
|
222
|
+
if (name === 'get_request_detail') {
|
|
223
|
+
const seq = args.seq as number;
|
|
224
|
+
const req = requestMap.get(seq);
|
|
225
|
+
if (!req) return `Error: 未找到序号为 ${seq} 的请求`;
|
|
226
|
+
return this.formatRequestDetail(req);
|
|
227
|
+
}
|
|
228
|
+
if (mcpMgr) return mcpMgr.callTool(name, args);
|
|
229
|
+
throw new Error(`Tool not found: ${name}`);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
233
|
+
try {
|
|
234
|
+
signal?.throwIfAborted();
|
|
235
|
+
const messages: Array<{ role: "system" | "user" | "assistant"; content: string }> = [
|
|
236
|
+
{ role: "system", content: system },
|
|
237
|
+
{ role: "user", content: user },
|
|
238
|
+
];
|
|
239
|
+
|
|
240
|
+
let result;
|
|
241
|
+
if (allTools.length > 0) {
|
|
242
|
+
// 有工具可用时走 agentic loop(非流式,但支持 tool calling)
|
|
243
|
+
result = await router.completeWithTools(
|
|
244
|
+
messages,
|
|
245
|
+
allTools,
|
|
246
|
+
callTool,
|
|
247
|
+
onProgress,
|
|
248
|
+
10,
|
|
249
|
+
signal,
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
// 无工具时走流式调用(保持逐字输出 UX)
|
|
253
|
+
result = await router.complete(messages, onProgress, signal);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
content = result.content;
|
|
257
|
+
promptTokens = result.promptTokens;
|
|
258
|
+
completionTokens = result.completionTokens;
|
|
259
|
+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'analyze', result.promptTokens, result.completionTokens);
|
|
260
|
+
break;
|
|
261
|
+
} catch (err) {
|
|
262
|
+
// Don't retry if cancelled
|
|
263
|
+
if (signal?.aborted) throw err;
|
|
264
|
+
if (attempt === 1)
|
|
265
|
+
throw new Error(
|
|
266
|
+
`AI 分析失败(已重试): ${(err as Error).message}`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Save report
|
|
272
|
+
const report: AnalysisReport = {
|
|
273
|
+
id: uuidv4(),
|
|
274
|
+
session_id: sessionId,
|
|
275
|
+
created_at: Date.now(),
|
|
276
|
+
llm_provider: config.name,
|
|
277
|
+
llm_model: config.model,
|
|
278
|
+
prompt_tokens: promptTokens,
|
|
279
|
+
completion_tokens: completionTokens,
|
|
280
|
+
report_content: content,
|
|
281
|
+
filter_prompt_tokens: filterPromptTokens,
|
|
282
|
+
filter_completion_tokens: filterCompletionTokens,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
this.reportsRepo.insert(report);
|
|
286
|
+
|
|
287
|
+
return report;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* 解析 Phase 1 过滤响应:提取 JSON 数组中的有效序号
|
|
292
|
+
*/
|
|
293
|
+
private parseFilterResponse(raw: string, validSeqs: Set<number>): number[] | null {
|
|
294
|
+
let cleaned = raw.trim();
|
|
295
|
+
// 去除 markdown 代码块包裹
|
|
296
|
+
cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, '');
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(cleaned);
|
|
300
|
+
if (!Array.isArray(parsed)) return null;
|
|
301
|
+
const nums = parsed.filter(
|
|
302
|
+
(n): n is number => typeof n === 'number' && validSeqs.has(n),
|
|
303
|
+
);
|
|
304
|
+
return nums.length > 0 ? nums : null;
|
|
305
|
+
} catch {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 格式化单个请求的完整详情(内置 tool 返回值)
|
|
312
|
+
*/
|
|
313
|
+
private formatRequestDetail(req: FilteredRequest): string {
|
|
314
|
+
const lines = [
|
|
315
|
+
`# 请求 #${req.seq}`,
|
|
316
|
+
`${req.method} ${req.url} → ${req.status ?? 'pending'}`,
|
|
317
|
+
'',
|
|
318
|
+
'## 请求头',
|
|
319
|
+
JSON.stringify(req.headers, null, 2),
|
|
320
|
+
];
|
|
321
|
+
if (req.body) {
|
|
322
|
+
lines.push('', '## 请求体', req.body);
|
|
323
|
+
}
|
|
324
|
+
if (req.responseHeaders) {
|
|
325
|
+
lines.push('', '## 响应头', JSON.stringify(req.responseHeaders, null, 2));
|
|
326
|
+
}
|
|
327
|
+
if (req.responseBody) {
|
|
328
|
+
lines.push('', '## 响应体', req.responseBody);
|
|
329
|
+
}
|
|
330
|
+
if (req.hooks.length > 0) {
|
|
331
|
+
lines.push('', '## 关联 JS Hooks');
|
|
332
|
+
for (const h of req.hooks) {
|
|
333
|
+
lines.push(`[${h.hook_type}] ${h.function_name}: args=${h.arguments}${h.result ? ` result=${h.result}` : ''}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return lines.join('\n');
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async chat(
|
|
340
|
+
sessionId: string,
|
|
341
|
+
config: LLMProviderConfig,
|
|
342
|
+
history: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
|
|
343
|
+
userMessage: string,
|
|
344
|
+
onProgress?: (chunk: string) => void,
|
|
345
|
+
reportId?: string,
|
|
346
|
+
): Promise<string> {
|
|
347
|
+
// Build messages array: existing history + new user message
|
|
348
|
+
const messages = [
|
|
349
|
+
...history,
|
|
350
|
+
{ role: 'user' as const, content: userMessage },
|
|
351
|
+
]
|
|
352
|
+
|
|
353
|
+
// Trim old messages if total context exceeds limit
|
|
354
|
+
// Keep: messages[0] (system) + messages[1] (report) + most recent turns
|
|
355
|
+
await this.compressMessages(messages, MAX_CHAT_CONTEXT_CHARS, config, sessionId, reportId ?? null);
|
|
356
|
+
|
|
357
|
+
const router = new LLMRouter(config, this.createLogCallback(sessionId, reportId ?? null, 'chat', config))
|
|
358
|
+
|
|
359
|
+
// Build request lookup for builtin tool
|
|
360
|
+
const assembler = new DataAssembler(
|
|
361
|
+
this.requestsRepo,
|
|
362
|
+
this.jsHooksRepo,
|
|
363
|
+
this.storageSnapshotsRepo,
|
|
364
|
+
);
|
|
365
|
+
const fullData = assembler.assemble(sessionId);
|
|
366
|
+
const requestMap = new Map(fullData.requests.map(r => [r.seq, r]));
|
|
367
|
+
|
|
368
|
+
// Collect available tools: builtin + MCP
|
|
369
|
+
const builtinTools = fullData.requests.length > 0 ? BUILTIN_TOOLS : [];
|
|
370
|
+
const mcpTools = this.mcpManager?.hasConnections()
|
|
371
|
+
? this.mcpManager.listAllTools()
|
|
372
|
+
: [];
|
|
373
|
+
const allTools = [...builtinTools, ...mcpTools];
|
|
374
|
+
|
|
375
|
+
const mcpMgr = this.mcpManager;
|
|
376
|
+
|
|
377
|
+
// Collect tool interactions for context preservation
|
|
378
|
+
const toolInteractions: string[] = [];
|
|
379
|
+
const callTool = async (name: string, args: Record<string, unknown>): Promise<string> => {
|
|
380
|
+
let result: string;
|
|
381
|
+
if (name === 'get_request_detail') {
|
|
382
|
+
const seq = args.seq as number;
|
|
383
|
+
const req = requestMap.get(seq);
|
|
384
|
+
if (!req) {
|
|
385
|
+
result = `Error: 未找到序号为 ${seq} 的请求`;
|
|
386
|
+
} else {
|
|
387
|
+
result = this.formatRequestDetail(req);
|
|
388
|
+
}
|
|
389
|
+
} else if (mcpMgr) {
|
|
390
|
+
result = await mcpMgr.callTool(name, args);
|
|
391
|
+
} else {
|
|
392
|
+
throw new Error(`Tool not found: ${name}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Record tool interaction for context preservation across turns
|
|
396
|
+
const argsStr = JSON.stringify(args);
|
|
397
|
+
const truncatedResult = result.length > TOOL_RESULT_MAX_CHARS
|
|
398
|
+
? result.slice(0, TOOL_RESULT_MAX_CHARS) + `\n...(truncated, ${result.length} chars total)`
|
|
399
|
+
: result;
|
|
400
|
+
toolInteractions.push(`[${name}](${argsStr})\n${truncatedResult}`);
|
|
401
|
+
|
|
402
|
+
return result;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
let replyContent: string;
|
|
406
|
+
|
|
407
|
+
if (allTools.length > 0) {
|
|
408
|
+
const result = await router.completeWithTools(messages, allTools, callTool, onProgress, 5);
|
|
409
|
+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'chat', result.promptTokens, result.completionTokens);
|
|
410
|
+
replyContent = result.content;
|
|
411
|
+
} else {
|
|
412
|
+
const result = await router.complete(messages, onProgress);
|
|
413
|
+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'chat', result.promptTokens, result.completionTokens);
|
|
414
|
+
replyContent = result.content;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Embed tool interaction context in assistant reply for future turns
|
|
418
|
+
if (toolInteractions.length > 0) {
|
|
419
|
+
const ctx = toolInteractions.join('\n---\n');
|
|
420
|
+
replyContent += `\n\n<tool_context>\n${ctx}\n</tool_context>`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return replyContent;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 两级上下文压缩,保留所有对话信息。
|
|
428
|
+
*
|
|
429
|
+
* 第一级(无 LLM 调用):剥离旧 assistant 消息中的 <tool_context>,
|
|
430
|
+
* 仅保留最近 KEEP_TOOL_CONTEXT_RECENT 条 assistant 消息的工具上下文。
|
|
431
|
+
*
|
|
432
|
+
* 第二级(需要 LLM 调用):如果仍然超限,将最旧的对话轮次
|
|
433
|
+
* (messages[2] 到中间某个位置)用 LLM 压缩为一条摘要消息替换。
|
|
434
|
+
*
|
|
435
|
+
* 始终保留 messages[0](system)和 messages[1](report)。
|
|
436
|
+
*/
|
|
437
|
+
private async compressMessages(
|
|
438
|
+
messages: Array<{ role: string; content: string }>,
|
|
439
|
+
maxChars: number,
|
|
440
|
+
config: LLMProviderConfig,
|
|
441
|
+
sessionId: string,
|
|
442
|
+
reportId: string | null,
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
const totalChars = () => messages.reduce((sum, m) => sum + m.content.length, 0);
|
|
445
|
+
|
|
446
|
+
if (totalChars() <= maxChars) return;
|
|
447
|
+
|
|
448
|
+
// ---- Tier 1: Strip <tool_context> from older assistant messages ----
|
|
449
|
+
// Find assistant messages (skip messages[1] which is the initial report)
|
|
450
|
+
let assistantCount = 0;
|
|
451
|
+
for (let i = messages.length - 1; i >= 2; i--) {
|
|
452
|
+
if (messages[i].role === 'assistant') {
|
|
453
|
+
assistantCount++;
|
|
454
|
+
if (assistantCount > KEEP_TOOL_CONTEXT_RECENT) {
|
|
455
|
+
// Strip tool_context from this older assistant message
|
|
456
|
+
messages[i] = {
|
|
457
|
+
...messages[i],
|
|
458
|
+
content: messages[i].content.replace(/\n*<tool_context>[\s\S]*?<\/tool_context>\s*$/g, ''),
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (totalChars() <= maxChars) return;
|
|
465
|
+
|
|
466
|
+
// ---- Tier 2: LLM-based summarization of oldest conversation turns ----
|
|
467
|
+
// Keep at least the most recent 4 messages (2 turns) + system + report
|
|
468
|
+
const minKeepFromEnd = 4;
|
|
469
|
+
const conversationMessages = messages.slice(2); // exclude system + report
|
|
470
|
+
if (conversationMessages.length <= minKeepFromEnd) return; // too few to compress
|
|
471
|
+
|
|
472
|
+
// Split: older turns to compress, recent turns to keep
|
|
473
|
+
const splitIdx = conversationMessages.length - minKeepFromEnd;
|
|
474
|
+
const toCompress = conversationMessages.slice(0, splitIdx);
|
|
475
|
+
// Build summary prompt
|
|
476
|
+
const conversationText = toCompress
|
|
477
|
+
.map((m) => `[${m.role}]: ${m.content.slice(0, 3000)}`)
|
|
478
|
+
.join('\n\n');
|
|
479
|
+
|
|
480
|
+
const summaryPrompt = [
|
|
481
|
+
{
|
|
482
|
+
role: 'system' as const,
|
|
483
|
+
content: '你是一个对话摘要助手。将以下多轮对话压缩为一段简洁的摘要,保留所有关键信息:讨论过的请求序号、API端点、发现的问题、用户关注的重点。用中文输出。',
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
role: 'user' as const,
|
|
487
|
+
content: `请将以下对话压缩为摘要(保留关键技术细节,尤其是请求序号和具体发现):\n\n${conversationText}`,
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
const router = new LLMRouter(config, this.createLogCallback(sessionId, reportId, 'compress', config));
|
|
493
|
+
const result = await router.complete(summaryPrompt);
|
|
494
|
+
const summary = result.content;
|
|
495
|
+
|
|
496
|
+
if (summary) {
|
|
497
|
+
// Replace compressed messages with a single summary
|
|
498
|
+
// Remove old conversation turns (index 2 to 2+splitIdx), insert summary
|
|
499
|
+
messages.splice(2, splitIdx, {
|
|
500
|
+
role: 'assistant',
|
|
501
|
+
content: `<conversation_summary>\n${summary}\n</conversation_summary>`,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
} catch (err) {
|
|
505
|
+
// Summarization failed — fall back to stripping old messages' content to fit
|
|
506
|
+
console.warn('Chat history compression failed, falling back to content truncation:', err);
|
|
507
|
+
for (let i = 2; i < messages.length - minKeepFromEnd && totalChars() > maxChars; i++) {
|
|
508
|
+
if (messages[i].content.length > 500) {
|
|
509
|
+
messages[i] = {
|
|
510
|
+
...messages[i],
|
|
511
|
+
content: messages[i].content.slice(0, 500) + '\n...(compressed)',
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|