v0.1.2: 全项目代码审查修复 + 子代理编排器 + 上下文压缩 + 并行工具执行

后端修复: Ollama adapter 移除死代码/修复超时硬编码/iteration硬编码/pullModel无超时/流异常断开补发DONE; SSE解析器支持非字符串arguments; Sandbox fail-closed安全加固; IPC移除未使用变量

新增功能: TaskOrchestrator子任务编排器; DelegateTaskTool委派工具; Agent Loop并行工具执行; 上下文自动压缩; 记忆检索注入System Prompt

前端修复: useAgentStream text_delta thought累积bug; 工具调用状态正确流转; 修复重复stateChange事件; TraceStep.thought正确填充
This commit is contained in:
thzxx
2026-07-06 22:48:33 +08:00
parent 8cdb93f8bf
commit 6f08759f63
25 changed files with 1044 additions and 675 deletions
+28 -28
View File
@@ -20,6 +20,7 @@ import {
buildSearXNGAuthHeaders,
logTool,
} from './network-utils';
import type { WebFetchTool } from './web-fetch';
// ===== 类型定义 =====
@@ -240,7 +241,10 @@ export class WebSearchTool implements IMetonaTool {
timeoutMs: 300_000,
};
constructor(private configService: ConfigService) {}
constructor(
private configService: ConfigService,
private webFetchTool: WebFetchTool,
) {}
async execute(args: Record<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
const query = args.query as string;
@@ -463,7 +467,7 @@ export class WebSearchTool implements IMetonaTool {
return Array.from(seen.values());
}
// ===== 摘要增强 =====
// ===== 摘要增强(委托给 WebFetchTool =====
private async enhanceSnippets(results: SearchResult[], maxEnhance: number): Promise<void> {
let enhanced = 0;
@@ -471,14 +475,13 @@ export class WebSearchTool implements IMetonaTool {
if (enhanced >= maxEnhance) break;
if (r.snippet.length < 30 && r.reachable) {
try {
const resp = await fetchWithTimeout(r.url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36' },
}, 5_000);
if (resp.ok) {
const html = await resp.text();
// 延迟导入 htmlToText 以避免循环依赖
const { htmlToText } = await import('./network-utils');
const text = htmlToText(html).slice(0, 200);
const fetchResult = await this.webFetchTool.execute(
{ url: r.url },
{ sessionId: '', workspacePath: '', iteration: 0, requestId: '' },
) as { success: boolean; content?: string };
if (fetchResult.success && fetchResult.content) {
const text = fetchResult.content.slice(0, 200);
if (text.length > r.snippet.length) {
r.snippet = text;
r._enhanced = true;
@@ -492,7 +495,7 @@ export class WebSearchTool implements IMetonaTool {
}
}
// ===== 自动抓取完整内容 =====
// ===== 自动抓取完整内容(委托给 WebFetchTool =====
private async autoFetch(
query: string,
@@ -524,21 +527,17 @@ export class WebSearchTool implements IMetonaTool {
}
const fetched: Array<{ url: string; title: string; content: string }> = [];
const { htmlToText } = await import('./network-utils');
for (const item of toFetch) {
try {
const resp = await fetchWithTimeout(item.result.url, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36',
'Accept-Language': 'zh-CN,zh;q=0.9',
},
}, 15_000);
// 委托给 WebFetchTool — 享受三阶段回退策略(HTTP + 反爬 + 浏览器渲染)
const fetchResult = await this.webFetchTool.execute(
{ url: item.result.url },
{ sessionId: '', workspacePath: '', iteration: 0, requestId: '' },
) as { success: boolean; content?: string; method?: string };
if (resp.ok) {
const html = await resp.text();
const content = htmlToText(html);
fetched.push({ url: item.result.url, title: item.result.title, content });
if (fetchResult.success && fetchResult.content) {
fetched.push({ url: item.result.url, title: item.result.title, content: fetchResult.content });
}
} catch (err) {
logTool('web_search', `Auto-fetch failed for ${item.result.url}: ${(err as Error).message}`);
@@ -547,12 +546,13 @@ export class WebSearchTool implements IMetonaTool {
if (remaining.length > 0) {
const randomPick = remaining[Math.floor(Math.random() * remaining.length)];
try {
const resp2 = await fetchWithTimeout(randomPick.result.url, {
headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/131.0.0.0 Safari/537.36' },
}, 10_000);
if (resp2.ok) {
const html2 = await resp2.text();
fetched.push({ url: randomPick.result.url, title: randomPick.result.title, content: htmlToText(html2) });
const fetchResult2 = await this.webFetchTool.execute(
{ url: randomPick.result.url },
{ sessionId: '', workspacePath: '', iteration: 0, requestId: '' },
) as { success: boolean; content?: string };
if (fetchResult2.success && fetchResult2.content) {
fetched.push({ url: randomPick.result.url, title: randomPick.result.title, content: fetchResult2.content });
}
} catch {
// 忽略补充失败