Files
metona-ai-desktop/electron/harness/tools/built-in/network.ts
T
thzxx 1d185db6b3 feat: MetonaAI Desktop 初始项目
- Electron + React + TypeScript 架构
- 三栏布局: Sidebar | ChatPanel | DetailPanel
- 9 个内置工具 (文件系统/网络/记忆/命令)
- SQLite 持久化 (better-sqlite3)
- MUI 暗色/亮色主题系统
- Agent Loop ReAct 状态机引擎
- DeepSeek / Agnes AI / Ollama Provider 适配器
- MCP 协议集成
- 系统托盘 + 全局快捷键
- Tailwind CSS v4 + Tailwind Merge
- 修复: Sidebar 缺失 TextField 导入导致黑屏
2026-06-27 21:33:27 +08:00

152 lines
5.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 网络工具(2 个)
*
* web_search, web_extract
*
* @see docs/MetonaAI-Desktop 架构与交互设计.html — 9 个基础工具
* @see standard/开发规范.md — 使用原生 fetch(允许在工具层使用)
*/
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(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/\s+/g, ' ')
.trim();
}
}