feat: MEMORY.md 访问保护、格式简化及预存问题修复
MEMORY.md 保护: 新建 file-guard.ts 共享守卫模块; read_file/write_file/search_files/run_command 禁止访问根目录 MEMORY.md; permissions.ts 增加 deniedPatterns 深度防御。格式简化: 元数据从 # 注释改为 > 引用语法; 校验规则从 6 条简化为 3 条; context-builder extractContent 适配。预存问题: 修复路径遍历前缀碰撞漏洞; 移除 browser_extract 内容截断; SearXNG 配置实时读取; web_search/web_fetch 移除截断; 侧边栏动态获取工具列表; 版本号 0.1.1
This commit is contained in:
@@ -1,151 +1,11 @@
|
||||
/**
|
||||
* 网络工具(2 个)
|
||||
* 网络工具导出
|
||||
*
|
||||
* web_search, web_extract
|
||||
* web_search — 双模式搜索(SearXNG / 内置四引擎)
|
||||
* web_fetch — 三阶段回退抓取(HTTP / SPA 升级 / 浏览器渲染)
|
||||
*
|
||||
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具
|
||||
* @see standard/开发规范.md — 使用原生 fetch(允许在工具层使用)
|
||||
* @see docs/Agent网络工具通用设计-v2.md
|
||||
*/
|
||||
|
||||
import type { IMetonaTool, ToolExecutionContext } from '../../types/metona-tool';
|
||||
import type { MetonaToolDef } from '../../../harness/types';
|
||||
import { MetonaToolCategory, MetonaRiskLevel } from '../../../harness/types';
|
||||
|
||||
// ===== 5. web_search =====
|
||||
|
||||
export class WebSearchTool implements IMetonaTool {
|
||||
readonly definition: MetonaToolDef = {
|
||||
name: 'web_search',
|
||||
description: 'Search the web for information. Returns titles, snippets, and URLs. Supports search operators (site:, filetype:, etc.).',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query. Supports operators like site:domain filetype:pdf' },
|
||||
limit: { type: 'number', description: 'Number of results (default 5, max 100)' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
category: MetonaToolCategory.NETWORK,
|
||||
riskLevel: MetonaRiskLevel.LOW,
|
||||
requiresPermission: false,
|
||||
timeoutMs: 30_000,
|
||||
};
|
||||
|
||||
async execute(args: Record<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
|
||||
const query = args.query as string;
|
||||
const limit = Math.min(100, Math.max(1, (args.limit as number) ?? 5));
|
||||
|
||||
// 使用 DuckDuckGo Instant Answer API(免费,无需 API Key)
|
||||
try {
|
||||
const url = `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`;
|
||||
const response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: { 'User-Agent': 'MetonaAI/1.0' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Search API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json() as Record<string, unknown>;
|
||||
const results: Array<{ title: string; snippet: string; url: string }> = [];
|
||||
|
||||
// 提取 AbstractText
|
||||
if (data.AbstractText) {
|
||||
results.push({
|
||||
title: (data.Heading as string) ?? query,
|
||||
snippet: (data.AbstractText as string).slice(0, 300),
|
||||
url: (data.AbstractURL as string) ?? '',
|
||||
});
|
||||
}
|
||||
|
||||
// 提取 RelatedTopics
|
||||
const related = data.RelatedTopics as Array<Record<string, unknown>> | undefined;
|
||||
if (related) {
|
||||
for (const topic of related.slice(0, limit - results.length)) {
|
||||
if (topic.Text && topic.FirstURL) {
|
||||
results.push({
|
||||
title: ((topic.Text as string) ?? '').slice(0, 100),
|
||||
snippet: (topic.Text as string) ?? '',
|
||||
url: (topic.FirstURL as string) ?? '',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { query, results, count: results.length };
|
||||
} catch (error) {
|
||||
return { query, results: [], count: 0, error: (error as Error).message };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 6. web_extract =====
|
||||
|
||||
export class WebExtractTool implements IMetonaTool {
|
||||
readonly definition: MetonaToolDef = {
|
||||
name: 'web_extract',
|
||||
description: 'Fetch web page content and convert to plain text. Supports HTML pages. Content over 5000 chars is auto-summarized.',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
urls: { type: 'array', description: 'URL list to fetch (max 5)', items: { type: 'string', description: 'URL to fetch' } },
|
||||
},
|
||||
required: ['urls'],
|
||||
},
|
||||
category: MetonaToolCategory.NETWORK,
|
||||
riskLevel: MetonaRiskLevel.LOW,
|
||||
requiresPermission: false,
|
||||
timeoutMs: 30_000,
|
||||
};
|
||||
|
||||
async execute(args: Record<string, unknown>, _context: ToolExecutionContext): Promise<unknown> {
|
||||
const urls = (args.urls as string[]).slice(0, 5);
|
||||
const results: Array<{ url: string; content: string; success: boolean; error?: string }> = [];
|
||||
|
||||
for (const url of urls) {
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
signal: AbortSignal.timeout(15_000),
|
||||
headers: {
|
||||
'User-Agent': 'MetonaAI/1.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
results.push({ url, content: '', success: false, error: `HTTP ${response.status}` });
|
||||
continue;
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const text = this.htmlToText(html);
|
||||
const truncated = text.length > 5000 ? text.slice(0, 5000) + '\n\n[Content truncated at 5000 characters]' : text;
|
||||
|
||||
results.push({ url, content: truncated, success: true });
|
||||
} catch (error) {
|
||||
results.push({ url, content: '', success: false, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
return { results, count: results.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单 HTML → 文本转换
|
||||
* @see standard/开发规范.md — < 20 行纯函数,允许自写
|
||||
*/
|
||||
private htmlToText(html: string): string {
|
||||
return html
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
}
|
||||
export { WebSearchTool } from './web-search';
|
||||
export { WebFetchTool } from './web-fetch';
|
||||
|
||||
Reference in New Issue
Block a user