feat: v0.4.0 四阶段迭代 — 安全加固 + 工程基线 + 架构重构 + 双 Provider 扩展
P0 安全修复: - API Key 加密存储(safeStorage 密钥链,版本化前缀,历史明文平滑兼容) - 间接提示注入防护(SecurityScanHook 工具结果深扫描,网络工具脱敏/本地工具警示分级) - error:report IPC 断链修复(渲染进程错误上报落 electron-log + 审计) - abort 信号贯通工具层(run_command/dev-tools 子进程随会话中断终止) - run_command 沙箱加固(cd 系统目录/敏感文件读取拦截 + chcp 前缀剥离防解析退化) - .env 真实生效(dotenv 回退加载,应用内配置优先) P1 工程基础: - ESLint 9 flat config + 全部 34 条存量 warnings 清零(零容忍基线) - 测试基线 118 用例 11 文件(token/文件防护/权限/沙箱/注入/命令/引擎/注册表/审计链/摘要分层) - test:electron 双模式(ELECTRON_RUN_AS_NODE 跑 Electron ABI,SQLite 套件全执行) - SessionRecorder 多会话隔离 + 9 种 TRACE 事件补全(含最终轮 iteration_end) - Provider 故障转移(重试耗尽/不可重试一次性切换 fallback + 前端通知) - MCP 真就绪(等待全部连接完成再广播 tools:ready) - SLO/HealthChecker 真实接入(60s 巡检 + 托盘状态) - CONFIG_DEFAULTS 单一来源(消除 SEED 双源漂移) P2 架构升级: - handlers.ts 1940 行拆分为 13 个 IPC 域模块(防重入注册 + 多窗口广播) - AgentEngineManager 每会话独立引擎(LRU 30 + adapter 工厂隔离 abort 信号) - TaskOrchestrator EngineProvider 改造 + abortByParent 联动中断 SubAgent - 会话摘要分层上下文(session_summaries 滚动摘要 + 截断游标清理防因果污染) - 消息编辑重发/重新生成(truncateAfter IPC + store 动作 + UI) - Markdown 导出 / WebSearch 并行抓取(并发 3)/ 记忆 TF 缓存 / 版本构建期注入 P3 能力扩展: - OpenAI Adapter(o 系列推理模型 reasoning_effort/max_completion_tokens) - Anthropic Adapter(原生 Messages API:tool_use 块/角色合并/thinking budget/图片 base64/SSE 事件机) - 设置页/Onboarding 六 Provider 全链路接入
This commit is contained in:
@@ -189,7 +189,7 @@ async function checkReachability(urls: string[], concurrency = 5): Promise<Map<s
|
||||
|
||||
// ===== 智能排序 =====
|
||||
|
||||
function smartSort(results: SearchResult[], reachabilityMap: Map<string, boolean>): SearchResult[] {
|
||||
function smartSort(results: SearchResult[]): SearchResult[] {
|
||||
for (const r of results) {
|
||||
const reachability = r.reachable ? 30 : -20;
|
||||
const snippetQuality = Math.min(r.snippet.length, 100) / 100 * 20;
|
||||
@@ -302,7 +302,7 @@ export class WebSearchTool implements IMetonaTool {
|
||||
}
|
||||
|
||||
// 智能排序
|
||||
const sorted = smartSort(deduped, reachabilityMap).slice(0, maxResults);
|
||||
const sorted = smartSort(deduped).slice(0, maxResults);
|
||||
|
||||
// 摘要增强
|
||||
if (enhanceSnippets) {
|
||||
@@ -375,7 +375,7 @@ export class WebSearchTool implements IMetonaTool {
|
||||
const searchUrl = `${baseUrl}/search?${params.toString()}`;
|
||||
logTool('web_search', `[SearXNG] Fetching page ${page}: ${searchUrl}`);
|
||||
|
||||
let pageResults: SearchResult[] = [];
|
||||
const pageResults: SearchResult[] = [];
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(searchUrl, { headers }, 15_000);
|
||||
@@ -523,6 +523,13 @@ export class WebSearchTool implements IMetonaTool {
|
||||
|
||||
// ===== 自动抓取完整内容(委托给 WebFetchTool) =====
|
||||
|
||||
/**
|
||||
* P2-12: 自动抓取改为并行(批次并发 3)
|
||||
*
|
||||
* 原实现逐条串行抓取(单个 web_fetch 最长 120s 超时),top5 结果最坏耗时
|
||||
* 逼近 web_search 的 300s 工具超时上限。并行批次化后总耗时约降至 1/3。
|
||||
* 失败结果直接跳过(原"随机补充重试"逻辑收益边际,复杂度高,已移除)。
|
||||
*/
|
||||
private async autoFetch(
|
||||
query: string,
|
||||
results: SearchResult[],
|
||||
@@ -554,36 +561,31 @@ export class WebSearchTool implements IMetonaTool {
|
||||
|
||||
const fetched: Array<{ url: string; title: string; content: string }> = [];
|
||||
|
||||
for (const item of toFetch) {
|
||||
const fetchOne = async (item: { result: SearchResult }): Promise<{ url: string; title: string; content: string } | null> => {
|
||||
try {
|
||||
// 委托给 WebFetchTool — 享受三阶段回退策略(HTTP + 反爬 + 浏览器渲染)
|
||||
const fetchResult = await this.webFetchTool.execute(
|
||||
{ url: item.result.url },
|
||||
{ sessionId: '', workspacePath: '', iteration: 0, requestId: '' },
|
||||
) as { success: boolean; content?: string; method?: string };
|
||||
) as { success: boolean; content?: string };
|
||||
|
||||
if (fetchResult.success && fetchResult.content) {
|
||||
fetched.push({ url: item.result.url, title: item.result.title, content: fetchResult.content });
|
||||
return { url: item.result.url, title: item.result.title, content: fetchResult.content };
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
logTool('web_search', `Auto-fetch failed for ${item.result.url}: ${(err as Error).message}`);
|
||||
// 从剩余结果中随机补充
|
||||
const remaining = withRelevance.filter((x) => !toFetch.includes(x) && !fetched.some((f) => f.url === x.result.url));
|
||||
if (remaining.length > 0) {
|
||||
const randomPick = remaining[Math.floor(Math.random() * remaining.length)];
|
||||
try {
|
||||
const fetchResult2 = await this.webFetchTool.execute(
|
||||
{ url: randomPick.result.url },
|
||||
{ sessionId: '', workspacePath: '', iteration: 0, requestId: '' },
|
||||
) as { success: boolean; content?: string };
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (fetchResult2.success && fetchResult2.content) {
|
||||
fetched.push({ url: randomPick.result.url, title: randomPick.result.title, content: fetchResult2.content });
|
||||
}
|
||||
} catch {
|
||||
// 忽略补充失败
|
||||
}
|
||||
}
|
||||
// 并行批次抓取(并发 3)
|
||||
const CONCURRENCY = 3;
|
||||
for (let i = 0; i < toFetch.length; i += CONCURRENCY) {
|
||||
const batch = toFetch.slice(i, i + CONCURRENCY);
|
||||
const settled = await Promise.allSettled(batch.map((item) => fetchOne(item)));
|
||||
for (const r of settled) {
|
||||
if (r.status === 'fulfilled' && r.value) fetched.push(r.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user