feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题, 并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。 Critical (10/10 完成): - C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配 可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过 High (11/11 完成): - 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等 Medium (55/55 完成): - 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、 React 组件 cancelled 标志、类型收窄等 Low (20/20 完成): - 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等) - nanoid 统一替代 Date.now()+Math.random() - confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等 返工审计修复 (10/10 完成): - L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert - M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死 - M-42: 脱敏短值(length <= 4)泄露修复 - M-47: tasks:update 补全 title/description 类型校验 - L-9: ollama.adapter 非流式路径 nanoid 统一 - M-45: audit:query limit 策略与 memory:listAll 一致化 - SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup - L-15: CommandPalette useMemo 补全 sessions 响应式依赖 - useAgentStream 事件类型补全 seq/timestamp 字段 新增依赖: shell-quote + @types/shell-quote 版本号: 0.3.0 -> 0.3.1
This commit is contained in:
@@ -40,6 +40,10 @@ export class WebFetchTool implements IMetonaTool {
|
||||
type: 'object',
|
||||
properties: {
|
||||
url: { type: 'string', description: 'Target URL (http/https only)' },
|
||||
// H-3/H-4 修复: 补齐规范要求的 max_chars 和 extract_mode 参数
|
||||
// @see docs/Agent网络工具通用设计-v2.md — 第 3 章 web_fetch 抓取设计
|
||||
max_chars: { type: 'number', description: 'Maximum characters to return (default 50000, truncated with notice)' },
|
||||
extract_mode: { type: 'string', enum: ['text', 'html'], description: 'Content extraction mode: "text"=plain text (default), "html"=cleaned HTML with scripts/styles removed' },
|
||||
mobile_ua: { type: 'boolean', description: 'Use mobile User-Agent (default false)' },
|
||||
retry: { type: 'boolean', description: 'Enable retry with exponential backoff (default true)' },
|
||||
},
|
||||
@@ -55,6 +59,9 @@ export class WebFetchTool implements IMetonaTool {
|
||||
const url = args.url as string;
|
||||
const mobileUA = (args.mobile_ua as boolean) ?? false;
|
||||
const enableRetry = (args.retry as boolean) ?? true;
|
||||
// H-3/H-4 修复: 读取 max_chars 和 extract_mode 参数
|
||||
const maxChars = (args.max_chars as number) ?? 50_000;
|
||||
const extractMode = ((args.extract_mode as string) ?? 'text') as 'text' | 'html';
|
||||
|
||||
if (!url || !/^https?:\/\//i.test(url)) {
|
||||
return { url, content: '', success: false, error: 'URL must start with http:// or https://' };
|
||||
@@ -64,7 +71,7 @@ export class WebFetchTool implements IMetonaTool {
|
||||
const cached = fetchCache.get(url);
|
||||
if (cached) {
|
||||
logTool('web_fetch', `Cache hit: ${url}`);
|
||||
return { url, content: cached, success: true, method: 'cache', length: cached.length };
|
||||
return this.buildSuccess(url, cached, 'cache', maxChars);
|
||||
}
|
||||
|
||||
logTool('web_fetch', `Fetching: ${url}`);
|
||||
@@ -73,24 +80,29 @@ export class WebFetchTool implements IMetonaTool {
|
||||
const phase1Result = await this.httpFetch(url, mobileUA, enableRetry);
|
||||
|
||||
if (phase1Result.success && !phase1Result.intercepted) {
|
||||
// 内容过短检测 → Phase 2 升级
|
||||
if (phase1Result.text.length < 200) {
|
||||
logTool('web_fetch', `Phase 2: Content too short (${phase1Result.text.length} chars), upgrading to browser`);
|
||||
// 根据 extract_mode 选择返回内容:'html' 模式返回清理后的 HTML,'text' 模式返回纯文本
|
||||
const phase1Content = extractMode === 'html' ? phase1Result.html : phase1Result.text;
|
||||
|
||||
// 内容过短检测 → Phase 2 升级(仅对 text 模式生效,html 模式不升级)
|
||||
if (extractMode === 'text' && phase1Content.length < 200) {
|
||||
logTool('web_fetch', `Phase 2: Content too short (${phase1Content.length} chars), upgrading to browser`);
|
||||
const browserResult = await this.browserFetch(url);
|
||||
if (browserResult) {
|
||||
return this.buildSuccess(url, browserResult, 'browser');
|
||||
return this.buildSuccess(url, browserResult, 'browser', maxChars);
|
||||
}
|
||||
}
|
||||
// 写入缓存
|
||||
fetchCache.set(url, phase1Result.text);
|
||||
return this.buildSuccess(url, phase1Result.text, 'http');
|
||||
// 写入缓存(仅缓存 text 模式的内容,html 模式不缓存以避免模式混淆)
|
||||
if (extractMode === 'text') {
|
||||
fetchCache.set(url, phase1Content);
|
||||
}
|
||||
return this.buildSuccess(url, phase1Content, 'http', maxChars, extractMode);
|
||||
}
|
||||
|
||||
// ===== Phase 3: 浏览器回退 =====
|
||||
logTool('web_fetch', `Phase 3: Falling back to browser (${phase1Result.reason})`);
|
||||
const browserResult = await this.browserFetch(url);
|
||||
if (browserResult) {
|
||||
return this.buildSuccess(url, browserResult, 'browser');
|
||||
return this.buildSuccess(url, browserResult, 'browser', maxChars);
|
||||
}
|
||||
|
||||
// 全部失败
|
||||
@@ -108,7 +120,7 @@ export class WebFetchTool implements IMetonaTool {
|
||||
url: string,
|
||||
mobileUA: boolean,
|
||||
enableRetry: boolean,
|
||||
): Promise<{ success: boolean; text: string; intercepted: boolean; reason: string }> {
|
||||
): Promise<{ success: boolean; html: string; text: string; intercepted: boolean; reason: string }> {
|
||||
const maxRetries = enableRetry ? 3 : 1;
|
||||
const backoffBase = 2_000;
|
||||
|
||||
@@ -119,7 +131,7 @@ export class WebFetchTool implements IMetonaTool {
|
||||
|
||||
// 跳过重试的状态码 → 直接进入浏览器回退
|
||||
if (SKIP_RETRY_STATUS.has(response.status)) {
|
||||
return { success: false, text: '', intercepted: true, reason: `HTTP ${response.status}` };
|
||||
return { success: false, html: '', text: '', intercepted: true, reason: `HTTP ${response.status}` };
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -128,7 +140,7 @@ export class WebFetchTool implements IMetonaTool {
|
||||
await this.sleep(backoffBase * Math.pow(2, attempt) + Math.random() * backoffBase * 0.6);
|
||||
continue;
|
||||
}
|
||||
return { success: false, text: '', intercepted: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
||||
return { success: false, html: '', text: '', intercepted: false, reason: `HTTP ${response.status} ${response.statusText}` };
|
||||
}
|
||||
|
||||
// 读取正文(10MB 限制)
|
||||
@@ -136,12 +148,13 @@ export class WebFetchTool implements IMetonaTool {
|
||||
|
||||
// 拦截检测
|
||||
if (isInterceptedPage(html)) {
|
||||
return { success: false, text: '', intercepted: true, reason: 'Intercepted page detected' };
|
||||
return { success: false, html: '', text: '', intercepted: true, reason: 'Intercepted page detected' };
|
||||
}
|
||||
|
||||
// HTML → 纯文本
|
||||
const text = htmlToText(html);
|
||||
return { success: true, text, intercepted: false, reason: '' };
|
||||
// H-3/H-4 修复: 同时保留原始 HTML,供 extract_mode='html' 使用
|
||||
return { success: true, html, text, intercepted: false, reason: '' };
|
||||
} catch (err) {
|
||||
const errorMsg = (err as Error).message;
|
||||
if (attempt < maxRetries - 1) {
|
||||
@@ -149,11 +162,11 @@ export class WebFetchTool implements IMetonaTool {
|
||||
await this.sleep(backoffBase * Math.pow(2, attempt) + Math.random() * backoffBase * 0.6);
|
||||
continue;
|
||||
}
|
||||
return { success: false, text: '', intercepted: false, reason: errorMsg };
|
||||
return { success: false, html: '', text: '', intercepted: false, reason: errorMsg };
|
||||
}
|
||||
}
|
||||
|
||||
return { success: false, text: '', intercepted: false, reason: 'All retries exhausted' };
|
||||
return { success: false, html: '', text: '', intercepted: false, reason: 'All retries exhausted' };
|
||||
}
|
||||
|
||||
// ===== Phase 2/3: 浏览器回退(使用共享 BrowserWindowManager 单例) =====
|
||||
@@ -214,13 +227,29 @@ export class WebFetchTool implements IMetonaTool {
|
||||
|
||||
// ===== 辅助方法 =====
|
||||
|
||||
private buildSuccess(url: string, text: string, method: string): unknown {
|
||||
private buildSuccess(
|
||||
url: string,
|
||||
text: string,
|
||||
method: string,
|
||||
maxChars?: number,
|
||||
extractMode?: 'text' | 'html',
|
||||
): unknown {
|
||||
// H-3/H-4 修复: 应用 max_chars 截断,防止过长内容消耗过多 token
|
||||
let content = text;
|
||||
let truncated = false;
|
||||
if (maxChars !== undefined && maxChars > 0 && text.length > maxChars) {
|
||||
content = text.slice(0, maxChars) + `\n\n[... content truncated at ${maxChars} chars ...]`;
|
||||
truncated = true;
|
||||
}
|
||||
return {
|
||||
url,
|
||||
content: text,
|
||||
content,
|
||||
success: true,
|
||||
method,
|
||||
length: text.length,
|
||||
length: content.length,
|
||||
original_length: text.length,
|
||||
truncated,
|
||||
extract_mode: extractMode ?? 'text',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user