superagent-ai-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +27 -0
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/README.zh-CN.md +147 -0
- package/agent-config.json +4 -0
- package/agent-persona.md +67 -0
- package/bin/postinstall.js +26 -0
- package/bin/superagent.js +283 -0
- package/package.json +43 -0
- package/src/agent.js +114 -0
- package/src/config.js +103 -0
- package/src/public/index.html +1889 -0
- package/src/query.js +239 -0
- package/src/server.js +303 -0
- package/src/web-tool.js +174 -0
package/src/web-tool.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { createSdkMcpServer, tool } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { chromium } from 'playwright';
|
|
4
|
+
|
|
5
|
+
// 复用同一个浏览器实例,避免每次调用都冷启动
|
|
6
|
+
let browserPromise = null;
|
|
7
|
+
async function getBrowser() {
|
|
8
|
+
if (!browserPromise) {
|
|
9
|
+
browserPromise = chromium.launch({ headless: true });
|
|
10
|
+
}
|
|
11
|
+
return browserPromise;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// 关闭浏览器(进程退出时清理)
|
|
15
|
+
export async function closeBrowser() {
|
|
16
|
+
if (browserPromise) {
|
|
17
|
+
try {
|
|
18
|
+
const b = await browserPromise;
|
|
19
|
+
await b.close();
|
|
20
|
+
} catch { /* 忽略 */ }
|
|
21
|
+
browserPromise = null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
process.on('exit', () => {
|
|
26
|
+
browserPromise?.then((b) => b.close().catch(() => {})).catch(() => {});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// HTML 主内容提取:去掉 script/style/nav 等噪声,保留正文文本
|
|
30
|
+
function extractMainText(document) {
|
|
31
|
+
// 常见噪声标签
|
|
32
|
+
const noise = document.querySelectorAll(
|
|
33
|
+
'script, style, noscript, iframe, nav, header, footer, aside, form, button, svg, [role="navigation"], [role="banner"], [role="contentinfo"]'
|
|
34
|
+
);
|
|
35
|
+
noise.forEach((n) => n.remove());
|
|
36
|
+
|
|
37
|
+
// 优先取 article / main,否则用 body
|
|
38
|
+
const root = document.querySelector('article') || document.querySelector('main') || document.body;
|
|
39
|
+
if (!root) return '';
|
|
40
|
+
|
|
41
|
+
// 把块级元素后面补换行,保留结构
|
|
42
|
+
const blocks = root.querySelectorAll('h1,h2,h3,h4,h5,h6,p,li,pre,blockquote,td,th,section,div');
|
|
43
|
+
blocks.forEach((b) => { b.append('\n'); });
|
|
44
|
+
|
|
45
|
+
const text = root.textContent || '';
|
|
46
|
+
// 折叠多余空白
|
|
47
|
+
return text.replace(/ /g, ' ').replace(/[ \t]+/g, ' ').replace(/\n{3,}/g, '\n\n').trim();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 把页面内容转成给模型看的 markdown-ish 文本
|
|
51
|
+
function pageToMarkdown(url, title, text, maxChars = 20000) {
|
|
52
|
+
const truncated = text.length > maxChars
|
|
53
|
+
? text.slice(0, maxChars) + `\n\n[... 内容已截断,原文共 ${text.length} 字符 ...]`
|
|
54
|
+
: text;
|
|
55
|
+
return `URL: ${url}\nTitle: ${title}\n\n${truncated}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchPage(url, timeout = 30000) {
|
|
59
|
+
const browser = await getBrowser();
|
|
60
|
+
const context = await browser.newContext({
|
|
61
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
62
|
+
locale: 'zh-CN',
|
|
63
|
+
javaScriptEnabled: true,
|
|
64
|
+
});
|
|
65
|
+
const page = await context.newPage();
|
|
66
|
+
try {
|
|
67
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
|
|
68
|
+
// 等动态内容渲染一下
|
|
69
|
+
await page.waitForTimeout(800).catch(() => {});
|
|
70
|
+
// 触发懒加载
|
|
71
|
+
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)).catch(() => {});
|
|
72
|
+
await page.waitForTimeout(500).catch(() => {});
|
|
73
|
+
const result = await page.evaluate(extractMainText);
|
|
74
|
+
const title = await page.title();
|
|
75
|
+
return pageToMarkdown(url, title, result || '');
|
|
76
|
+
} finally {
|
|
77
|
+
await context.close().catch(() => {});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 搜索:Brave Search(Bing 在国内 IP 下强制中文结果,Google/DuckDuckGo 挡无头浏览器)
|
|
82
|
+
// Brave 不限地区、不挡无头,返回结构化结果且链接是真实 URL 不需解码
|
|
83
|
+
async function searchWeb(query, maxResults = 8, timeout = 30000) {
|
|
84
|
+
const browser = await getBrowser();
|
|
85
|
+
const context = await browser.newContext({
|
|
86
|
+
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
87
|
+
locale: 'en-US',
|
|
88
|
+
javaScriptEnabled: true,
|
|
89
|
+
});
|
|
90
|
+
// 反无头检测
|
|
91
|
+
await context.addInitScript(() => {
|
|
92
|
+
Object.defineProperty(navigator, 'webdriver', { get: () => false });
|
|
93
|
+
});
|
|
94
|
+
const page = await context.newPage();
|
|
95
|
+
try {
|
|
96
|
+
const url = `https://search.brave.com/search?q=${encodeURIComponent(query)}`;
|
|
97
|
+
await page.goto(url, { waitUntil: 'domcontentloaded', timeout });
|
|
98
|
+
await page.waitForTimeout(2000).catch(() => {});
|
|
99
|
+
const results = await page.evaluate((limit) => {
|
|
100
|
+
// Brave 结果块是 article 或 div.snippet
|
|
101
|
+
const items = Array.from(document.querySelectorAll('article, div.snippet'));
|
|
102
|
+
return items.slice(0, limit).map((it) => {
|
|
103
|
+
const titleEl = it.querySelector('a.title, .snippet-title, h3, a[href]');
|
|
104
|
+
const snippetEl = it.querySelector('.snippet-content, .snippet-description, p, .text-gray');
|
|
105
|
+
return {
|
|
106
|
+
title: (titleEl?.textContent || '').trim(),
|
|
107
|
+
url: titleEl?.href || titleEl?.closest('a')?.href || '',
|
|
108
|
+
snippet: (snippetEl?.textContent || '').trim(),
|
|
109
|
+
};
|
|
110
|
+
}).filter((r) => (r.title || r.url) && !r.url.includes('search.brave.com'));
|
|
111
|
+
}, maxResults);
|
|
112
|
+
return results;
|
|
113
|
+
} finally {
|
|
114
|
+
await context.close().catch(() => {});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatSearchResults(query, results) {
|
|
119
|
+
if (!results.length) return `搜索 "${query}" 没有返回结果。`;
|
|
120
|
+
const lines = results.map((r, i) =>
|
|
121
|
+
`${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`
|
|
122
|
+
);
|
|
123
|
+
return `搜索 "${query}" 的结果:\n\n${lines.join('\n\n')}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const webMcpServer = createSdkMcpServer({
|
|
127
|
+
name: 'web',
|
|
128
|
+
version: '1.0.0',
|
|
129
|
+
tools: [
|
|
130
|
+
tool(
|
|
131
|
+
'WebFetch',
|
|
132
|
+
'抓取指定 URL 的网页内容并转成文本。用于:需要读取某个网页、文档、API 文档、博客文章内容时。输入 url(必须 http(s)://)和可选的 prompt(想从页面里提取什么信息)。返回页面正文文本。',
|
|
133
|
+
{
|
|
134
|
+
url: z.string().url().describe('要抓取的完整 URL,以 http:// 或 https:// 开头'),
|
|
135
|
+
prompt: z.string().optional().describe('想从页面里提取什么信息(可选,仅作提示,工具仍返回整页正文)'),
|
|
136
|
+
},
|
|
137
|
+
async (args) => {
|
|
138
|
+
try {
|
|
139
|
+
const content = await fetchPage(args.url);
|
|
140
|
+
return {
|
|
141
|
+
content: [{ type: 'text', text: content }],
|
|
142
|
+
};
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return {
|
|
145
|
+
content: [{ type: 'text', text: `抓取失败: ${e?.message ?? String(e)}` }],
|
|
146
|
+
isError: true,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
{ alwaysLoad: true }
|
|
151
|
+
),
|
|
152
|
+
tool(
|
|
153
|
+
'WebSearch',
|
|
154
|
+
'用搜索引擎搜索关键词,返回标题+URL+摘要列表。用于:需要查最新资讯、找某网站、查某个事实、不知道具体 URL 时。输入 query(搜索词)。先用这个找到相关链接,再用 WebFetch 抓具体页面看细节。',
|
|
155
|
+
{
|
|
156
|
+
query: z.string().describe('搜索关键词'),
|
|
157
|
+
},
|
|
158
|
+
async (args) => {
|
|
159
|
+
try {
|
|
160
|
+
const results = await searchWeb(args.query);
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: 'text', text: formatSearchResults(args.query, results) }],
|
|
163
|
+
};
|
|
164
|
+
} catch (e) {
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: 'text', text: `搜索失败: ${e?.message ?? String(e)}` }],
|
|
167
|
+
isError: true,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
{ alwaysLoad: true }
|
|
172
|
+
),
|
|
173
|
+
],
|
|
174
|
+
});
|