Skip to content

Commit 22c40d1

Browse files
committed
chore(release): v3.6.52
1 parent 25319f4 commit 22c40d1

7 files changed

Lines changed: 290 additions & 226 deletions

File tree

‎RELEASE_NOTES.md‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
# Anything Analyzer v3.6.51
1+
# Anything Analyzer v3.6.52
2+
3+
## 变更
4+
5+
- **统一 tools provider 路由** — 分析与追问复用同一套最新 LLM provider 配置和工具调用协议。
6+
- **修复追问工具调用兼容性** — 规范化 MCP 工具名、兼容 Responses/Chat Completions function schema,并保留真实工具名映射。
7+
- **支持持续工具调用** — 不再因轮次上限或工具异常自动退回无工具请求。
8+
- **优化上下文预算** — 大量请求的预过滤按批次发送,追问上下文使用确定性压缩,避免单次超大请求。
29

310
## 修复
411

@@ -10,7 +17,7 @@
1017

1118
| 平台 | 文件 |
1219
|------|------|
13-
| Windows | Anything-Analyzer-Setup-3.6.51.exe |
14-
| macOS (Apple Silicon) | Anything-Analyzer-3.6.51-arm64.dmg |
15-
| macOS (Intel) | Anything-Analyzer-3.6.51-x64.dmg |
16-
| Linux | Anything-Analyzer-3.6.51.AppImage |
20+
| Windows | Anything-Analyzer-Setup-3.6.52.exe |
21+
| macOS (Apple Silicon) | Anything-Analyzer-3.6.52-arm64.dmg |
22+
| macOS (Intel) | Anything-Analyzer-3.6.52-x64.dmg |
23+
| Linux | Anything-Analyzer-3.6.52.AppImage |

‎package.json‎

Lines changed: 48 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,48 @@
1-
{
2-
"name": "anything-analyzer",
3-
"version": "3.6.51",
4-
"description": "Universal web protocol analyzer with embedded browser and AI-powered analysis",
5-
"packageManager": "pnpm@10.24.0",
6-
"main": "./out/main/index.js",
7-
"scripts": {
8-
"dev": "electron-vite dev",
9-
"build": "electron-vite build",
10-
"preview": "electron-vite preview",
11-
"test": "vitest run",
12-
"test:watch": "vitest",
13-
"lint": "eslint src/ --ext .ts,.tsx",
14-
"postinstall": "electron-builder install-app-deps"
15-
},
16-
"dependencies": {
17-
"@ant-design/icons": "^5.6.1",
18-
"@modelcontextprotocol/sdk": "^1.29.0",
19-
"@vscode/sudo-prompt": "^9.3.2",
20-
"antd": "^5.24.0",
21-
"better-sqlite3": "^11.8.1",
22-
"electron-log": "^5.4.3",
23-
"electron-updater": "^6.8.3",
24-
"node-forge": "^1.4.0",
25-
"react": "^19.0.0",
26-
"react-dom": "^19.0.0",
27-
"react-markdown": "^9.0.3",
28-
"react-window": "^2.2.7",
29-
"rehype-highlight": "^7.0.2",
30-
"remark-gfm": "^4.0.0",
31-
"socks": "^2.8.7",
32-
"uuid": "^11.1.0",
33-
"zod": "^4.3.6"
34-
},
35-
"devDependencies": {
36-
"@types/better-sqlite3": "^7.6.12",
37-
"@types/node-forge": "^1.3.14",
38-
"@types/react": "^19.0.0",
39-
"@types/react-dom": "^19.0.0",
40-
"@types/uuid": "^10.0.0",
41-
"@vitejs/plugin-react": "^4.3.4",
42-
"electron": "^35.0.0",
43-
"electron-builder": "^25.1.8",
44-
"electron-vite": "^5.0.0",
45-
"typescript": "^5.7.0",
46-
"vitest": "^3.0.0"
47-
}
48-
}
1+
{
2+
"name": "anything-analyzer",
3+
"version": "3.6.52",
4+
"description": "Universal web protocol analyzer with embedded browser and AI-powered analysis",
5+
"packageManager": "pnpm@10.24.0",
6+
"main": "./out/main/index.js",
7+
"scripts": {
8+
"dev": "electron-vite dev",
9+
"build": "electron-vite build",
10+
"preview": "electron-vite preview",
11+
"test": "vitest run",
12+
"test:watch": "vitest",
13+
"lint": "eslint src/ --ext .ts,.tsx",
14+
"postinstall": "electron-builder install-app-deps"
15+
},
16+
"dependencies": {
17+
"@ant-design/icons": "^5.6.1",
18+
"@modelcontextprotocol/sdk": "^1.29.0",
19+
"@vscode/sudo-prompt": "^9.3.2",
20+
"antd": "^5.24.0",
21+
"better-sqlite3": "^11.8.1",
22+
"electron-log": "^5.4.3",
23+
"electron-updater": "^6.8.3",
24+
"node-forge": "^1.4.0",
25+
"react": "^19.0.0",
26+
"react-dom": "^19.0.0",
27+
"react-markdown": "^9.0.3",
28+
"react-window": "^2.2.7",
29+
"rehype-highlight": "^7.0.2",
30+
"remark-gfm": "^4.0.0",
31+
"socks": "^2.8.7",
32+
"uuid": "^11.1.0",
33+
"zod": "^4.3.6"
34+
},
35+
"devDependencies": {
36+
"@types/better-sqlite3": "^7.6.12",
37+
"@types/node-forge": "^1.3.14",
38+
"@types/react": "^19.0.0",
39+
"@types/react-dom": "^19.0.0",
40+
"@types/uuid": "^10.0.0",
41+
"@vitejs/plugin-react": "^4.3.4",
42+
"electron": "^35.0.0",
43+
"electron-builder": "^25.1.8",
44+
"electron-vite": "^5.0.0",
45+
"typescript": "^5.7.0",
46+
"vitest": "^3.0.0"
47+
}
48+
}

‎src/main/ai/ai-analyzer.ts‎

Lines changed: 47 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { DataAssembler } from "./data-assembler";
1212
import { PromptBuilder } from "./prompt-builder";
1313
import { LLMRouter } from "./llm-router";
1414
import type { MCPClientManager, MCPToolInfo } from "../mcp/mcp-manager";
15+
import { compactChatMessages, DEFAULT_CHAT_CONTEXT_CHARS } from "./context-budget";
1516

1617
/** 请求数低于此值时跳过 Phase 1 预过滤 */
1718
const PRE_FILTER_THRESHOLD = 20;
@@ -21,12 +22,8 @@ const PRE_FILTER_MIN_SELECTED = 3;
2122
const PHASE1_MAX_TOKENS = 1024;
2223
/** 需要全量请求的分析目的(不跳过任何请求) */
2324
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;
25+
/** 预过滤每次发送的请求摘要上限,避免单次上下文膨胀 */
26+
const FILTER_BATCH_SIZE = 100;
3027

3128
/** 内置 tool:查看请求详情 */
3229
const BUILTIN_TOOLS: MCPToolInfo[] = [
@@ -152,32 +149,38 @@ export class AiAnalyzer {
152149
onProgress?.(`> 正在过滤:分析 ${fullData.requests.length} 条请求的相关性...\n\n`);
153150

154151
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-
163152
const phase1Config: LLMProviderConfig = { ...config, maxTokens: PHASE1_MAX_TOKENS };
164153
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-
177154
const validSeqs = new Set(fullData.requests.map(r => r.seq));
178-
const filteredSeqs = this.parseFilterResponse(phase1Result.content, validSeqs);
155+
const selectedSeqs = new Set<number>();
156+
157+
for (let batchStart = 0; batchStart < allSummaries.length; batchStart += FILTER_BATCH_SIZE) {
158+
const batchSummaries = allSummaries.slice(batchStart, batchStart + FILTER_BATCH_SIZE);
159+
const filterPrompt = new PromptBuilder().buildFilterPrompt(
160+
batchSummaries,
161+
fullData.sceneHints,
162+
purpose,
163+
template,
164+
);
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+
const batchNumber = Math.floor(batchStart / FILTER_BATCH_SIZE) + 1;
171+
const batchCount = Math.ceil(allSummaries.length / FILTER_BATCH_SIZE);
172+
onProgress?.(`> 正在过滤第 ${batchNumber}/${batchCount} 批请求(${batchSummaries.length} 条)...\n\n`);
173+
signal?.throwIfAborted();
174+
const phase1Result = await phase1Router.complete(phase1Messages, undefined, signal);
175+
filterPromptTokens = (filterPromptTokens ?? 0) + phase1Result.promptTokens;
176+
filterCompletionTokens = (filterCompletionTokens ?? 0) + phase1Result.completionTokens;
177+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'filter', phase1Result.promptTokens, phase1Result.completionTokens);
178+
this.parseFilterResponse(phase1Result.content, validSeqs)?.forEach((seq) => selectedSeqs.add(seq));
179+
}
179180

180-
if (filteredSeqs && filteredSeqs.length >= PRE_FILTER_MIN_SELECTED) {
181+
const filteredSeqs = [...selectedSeqs];
182+
183+
if (filteredSeqs.length >= PRE_FILTER_MIN_SELECTED) {
181184
analysisData = assembler.filterBySeqs(fullData, filteredSeqs);
182185
onProgress?.(`> 过滤完成:从 ${fullData.requests.length} 条中选出 ${filteredSeqs.length} 条相关请求进行深度分析。\n\n`);
183186
} else {
@@ -220,7 +223,7 @@ export class AiAnalyzer {
220223
const mcpMgr = this.mcpManager;
221224
const callTool = async (name: string, args: Record<string, unknown>): Promise<string> => {
222225
if (name === 'get_request_detail') {
223-
const seq = args.seq as number;
226+
const seq = typeof args.seq === 'number' ? args.seq : Number(args.seq);
224227
const req = requestMap.get(seq);
225228
if (!req) return `Error: 未找到序号为 ${seq} 的请求`;
226229
return this.formatRequestDetail(req);
@@ -245,7 +248,7 @@ export class AiAnalyzer {
245248
allTools,
246249
callTool,
247250
onProgress,
248-
10,
251+
undefined,
249252
signal,
250253
);
251254
} else {
@@ -350,9 +353,9 @@ export class AiAnalyzer {
350353
{ role: 'user' as const, content: userMessage },
351354
]
352355

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+
// Deterministic compaction avoids an extra summarization request and keeps
357+
// follow-up requests on the same provider path as analysis.
358+
const compactedMessages = compactChatMessages(messages, DEFAULT_CHAT_CONTEXT_CHARS);
356359

357360
const router = new LLMRouter(config, this.createLogCallback(sessionId, reportId ?? null, 'chat', config))
358361

@@ -379,7 +382,7 @@ export class AiAnalyzer {
379382
const callTool = async (name: string, args: Record<string, unknown>): Promise<string> => {
380383
let result: string;
381384
if (name === 'get_request_detail') {
382-
const seq = args.seq as number;
385+
const seq = typeof args.seq === 'number' ? args.seq : Number(args.seq);
383386
const req = requestMap.get(seq);
384387
if (!req) {
385388
result = `Error: 未找到序号为 ${seq} 的请求`;
@@ -394,22 +397,24 @@ export class AiAnalyzer {
394397

395398
// Record tool interaction for context preservation across turns
396399
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}`);
400+
toolInteractions.push(`[${name}](${argsStr})\n${result}`);
401401

402402
return result;
403403
};
404404

405405
let replyContent: string;
406406

407407
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;
408+
try {
409+
const result = await router.completeWithTools(compactedMessages, allTools, callTool, onProgress);
410+
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'chat', result.promptTokens, result.completionTokens);
411+
replyContent = result.content;
412+
} catch (error) {
413+
const message = error instanceof Error ? error.message : String(error);
414+
throw new Error(`追问工具调用失败:${message}`);
415+
}
411416
} else {
412-
const result = await router.complete(messages, onProgress);
417+
const result = await router.complete(compactedMessages, onProgress);
413418
this.aiRequestLogRepo.updateLatestTokens(sessionId, 'chat', result.promptTokens, result.completionTokens);
414419
replyContent = result.content;
415420
}
@@ -423,95 +428,4 @@ export class AiAnalyzer {
423428
return replyContent;
424429
}
425430

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-
}
517431
}

0 commit comments

Comments
 (0)