@epoch-agent/plugin-web 0.10.0 → 0.11.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/README.md +26 -8
- package/dist/index.d.ts +137 -10
- package/dist/index.js +371 -29
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# @epoch-agent/plugin-web
|
|
2
2
|
|
|
3
3
|
网页抓取与搜索插件。**两个工具**:`web_fetch`(总是有)和 `web_search`
|
|
4
|
-
|
|
4
|
+
(**有可用后端才注册** —— 缺省状态下就有,见下)。
|
|
5
5
|
|
|
6
6
|
- ✅ **做**:HTTP GET → HTML→Markdown/Text/HTML、SSRF 校验、逐跳重定向校验、大小限制;
|
|
7
|
-
web 搜索(tavily / brave / searxng
|
|
7
|
+
web 搜索(native / tavily / brave / searxng / keyless 五个后端)
|
|
8
8
|
- ❌ **不做**:不执行 JS、不渲染页面、不带浏览器;**不抓搜索引擎的 HTML 结果页**
|
|
9
|
-
(违反 ToS、随时会碎、要处理验证码 ——
|
|
9
|
+
(违反 ToS、随时会碎、要处理验证码 —— 要搜就走后端,不走爬虫)
|
|
10
10
|
- **依赖**:只有 node 内置模块。[protocol](../../protocol) 是 peer,
|
|
11
11
|
HTML 转换是手写的(无第三方库)
|
|
12
12
|
|
|
@@ -25,18 +25,33 @@
|
|
|
25
25
|
**只返回标题 / URL / 摘要,不返回正文。** 一次搜索 5 条结果全抓正文轻松几万 token,
|
|
26
26
|
而其中多半是模型看一眼摘要就丢掉的。分工是:搜 → 模型挑 → `web_fetch` 抓那一条。
|
|
27
27
|
|
|
28
|
-
###
|
|
28
|
+
### 五个后端,按凭据自动挑
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
五档(`native` / `tavily` / `brave` / `searxng` / `keyless`)各要什么凭据见
|
|
31
31
|
[docs/TOOLS.md](../../../docs/TOOLS.md#web_search-要一个搜索后端)。
|
|
32
32
|
|
|
33
|
-
不配 `search.provider`
|
|
33
|
+
不配 `search.provider` 时按那张表的顺序探测,第一个可用的赢。**显式配了哪家就只认哪家**
|
|
34
34
|
—— 缺 key 时报诊断而不是悄悄换一家:换一家意味着查询发去了另一家公司,
|
|
35
35
|
那是用户没同意的事。
|
|
36
36
|
|
|
37
|
-
一个后端都没有时 `web_search` **不注册**,启动诊断给 `skipped`
|
|
37
|
+
一个后端都没有时 `web_search` **不注册**,启动诊断给 `skipped` 并逐档列出缺什么。
|
|
38
38
|
注册一个一调就报错的工具比不注册更糟:模型看见工具表里有它就会反复试,每次烧一轮。
|
|
39
39
|
|
|
40
|
+
#### 两档不用办账号的(2026-09-15)
|
|
41
|
+
|
|
42
|
+
上面那句「没有后端就不注册」原来的实际效果是**大多数嵌入宿主的用户都没有搜索** ——
|
|
43
|
+
三家老后端全要办账号,而宿主多半不会去配。这一档漏配是静默的:模型不知道自己本该
|
|
44
|
+
有搜索,就改用浏览器去抓搜索引擎结果页(正是本包第一节说「不做」的那件事)。
|
|
45
|
+
|
|
46
|
+
- **`native`**(`search/native.ts`)借 `provider.apiKey`,让当前 LLM provider 自己的
|
|
47
|
+
服务端搜索去干活。首版认 `deepseek` / `anthropic`(共用 Anthropic Messages 协议)。
|
|
48
|
+
它的结果项**没有明文摘要**(正文是 `encrypted_content`),信息走
|
|
49
|
+
`SearchOutcome.summary`。
|
|
50
|
+
- **`keyless`**(`search/keyless.ts`)打 Exa / Parallel 的匿名 MCP 端点,
|
|
51
|
+
带游标轮转摊平两家免费额度。**查询词会发给第三方**,所以它排在最后、且能整档关掉。
|
|
52
|
+
|
|
53
|
+
两个开关缺省开:`search.native: false` / `search.keyless: false`。
|
|
54
|
+
|
|
40
55
|
> SearXNG **默认没开** JSON 输出,实例的 `settings.yml` 里要有
|
|
41
56
|
> `search: { formats: [html, json] }`。没开时它返回 403,我们把这句话写进了错误里 ——
|
|
42
57
|
> 否则用户会以为是自己地址写错了。
|
|
@@ -90,7 +105,10 @@ annotations: { readOnlyHint: true, idempotentHint: false, openWorldHint: true }
|
|
|
90
105
|
| `tools/web-search.ts` | 搜索工具:参数钳制 + 会话内缓存 + 失败不炸轮 |
|
|
91
106
|
| `search/provider.ts` | 后端接口 + 共用的 HTTP / 错误分类 |
|
|
92
107
|
| `search/registry.ts` | 按配置 + 凭据挑后端(挑不到就不注册) |
|
|
93
|
-
| `search/{tavily,brave,searxng}.ts` |
|
|
108
|
+
| `search/{tavily,brave,searxng}.ts` | 三个要办账号的后端各自的请求 / 响应翻译 |
|
|
109
|
+
| `search/native.ts` | 借当前 LLM provider 的服务端搜索 |
|
|
110
|
+
| `search/keyless.ts` | Exa / Parallel 免费端点环 + 游标轮转 |
|
|
111
|
+
| `search/mcp.ts` | MCP `tools/call`(纯 JSON 和 SSE 两种回法) |
|
|
94
112
|
| `utils/url-safety.ts` | 三层 SSRF 校验 |
|
|
95
113
|
| `utils/html-to-markdown.ts` | 手写 HTML→Markdown / →Text |
|
|
96
114
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SearchProviderType, EpochConfig, ToolContext, ToolResult, EpochTool, EpochPlugin } from '@epoch-agent/protocol';
|
|
1
|
+
import { ProviderType, SearchProviderType, EpochConfig, ToolContext, ToolResult, EpochTool, EpochPlugin } from '@epoch-agent/protocol';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* 搜索后端的接口(方案 37)。
|
|
@@ -50,6 +50,26 @@ interface SearchOutcome {
|
|
|
50
50
|
* 于是会反复重试同一个查询。
|
|
51
51
|
*/
|
|
52
52
|
ignored?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* 后端自己合成的一段答案,**排在结果列表前面**给模型看。
|
|
55
|
+
*
|
|
56
|
+
* ## 为什么要有这一格(2026-09-15)
|
|
57
|
+
*
|
|
58
|
+
* `native` 那一档(搜索发生在 LLM provider 的服务端)返回的结果项里
|
|
59
|
+
* **没有明文摘要**:Anthropic 系的 `web_search_tool_result` 给的是
|
|
60
|
+
* `{title, url, encrypted_content, page_age}`,正文那一格是**加密的**,
|
|
61
|
+
* 只有发它的那一方解得开。
|
|
62
|
+
*
|
|
63
|
+
* 而本文件开头写着 snippet 的职责是「让模型判断这条值不值得抓正文」——
|
|
64
|
+
* 没有这一格,native 档交给模型的就是一串裸标题加 URL,它只能盲抓。
|
|
65
|
+
* 服务端合成的那段话正好补上这个缺口,而且它是**读过正文之后**写的,
|
|
66
|
+
* 比任何一家的 snippet 都准。
|
|
67
|
+
*
|
|
68
|
+
* ⚠️ **不是「摘要的替代品」**:有 snippet 的后端(tavily / brave / searxng /
|
|
69
|
+
* keyless)照常填 snippet、不填这一格。两者同时存在时都给模型,
|
|
70
|
+
* 它们回答的是不同的问题(这一格是「答案大概是什么」,snippet 是「这条讲什么」)。
|
|
71
|
+
*/
|
|
72
|
+
summary?: string;
|
|
53
73
|
}
|
|
54
74
|
interface SearchProvider {
|
|
55
75
|
readonly name: string;
|
|
@@ -73,11 +93,11 @@ declare class SearchError extends Error {
|
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
/**
|
|
76
|
-
* 按配置 + 凭据挑一个搜索后端(方案 37
|
|
96
|
+
* 按配置 + 凭据挑一个搜索后端(方案 37;2026-09-15 加 native / keyless 两档)。
|
|
77
97
|
*
|
|
78
98
|
* ```
|
|
79
99
|
* 显式配了 provider → 只认那一家。缺 key 就报出来,**不静默回落到别家**
|
|
80
|
-
* 没配 →
|
|
100
|
+
* 没配 → 按 AUTO_ORDER 顺序试,第一个能用的赢
|
|
81
101
|
* 一个都不能用 → 返回 null,工具**不注册**
|
|
82
102
|
* ```
|
|
83
103
|
*
|
|
@@ -87,13 +107,32 @@ declare class SearchError extends Error {
|
|
|
87
107
|
* 悄悄换成 tavily 意味着他的查询发去了另一家公司 —— 那是他没同意的事。
|
|
88
108
|
* 报出来让他自己选,是唯一诚实的做法。
|
|
89
109
|
*
|
|
90
|
-
* ##
|
|
110
|
+
* ## 「一个都没有」这一档 2026-09-15 之后几乎不会出现了
|
|
91
111
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
112
|
+
* 原话是「注册一个一调就失败的工具比不注册更糟」,那条判据本身没变。
|
|
113
|
+
* 变的是**现在几乎总有一家能用**:`native` 跟着已经配好的 LLM provider 走、
|
|
114
|
+
* `keyless` 连 key 都不要。于是 `null` 这一支退化成「两档都被显式关掉、
|
|
115
|
+
* 且三家带 key 的也都没配」这种明确的用户选择。
|
|
116
|
+
*
|
|
117
|
+
* 这么改的理由是实测到的一次事故(nebular 2026-09-15):宿主从没配过 `search`,
|
|
118
|
+
* 于是它的**每一个**用户都没有 `web_search`,而这件事对模型是静默的 ——
|
|
119
|
+
* 它不知道自己本该有搜索,就改用浏览器去抓搜索引擎结果页,
|
|
120
|
+
* 把 SEO 广告和 AI 生成的模板文当成行业数据写进了交付物。
|
|
95
121
|
*/
|
|
96
122
|
|
|
123
|
+
/**
|
|
124
|
+
* `native` 那一档要的东西 —— 它借的是**当前 LLM provider** 的凭据和端点,
|
|
125
|
+
* 而那几样不在 `EpochConfig['search']` 里。
|
|
126
|
+
*
|
|
127
|
+
* 整段缺席时 `native` 会如实报「不知道当前是哪个 provider」然后落到下一档,
|
|
128
|
+
* 不是报错 —— 这让老的调用点(以及只测搜索配置的用例)不用全改一遍。
|
|
129
|
+
*/
|
|
130
|
+
interface NativeContext {
|
|
131
|
+
providerType?: ProviderType;
|
|
132
|
+
apiKey?: string;
|
|
133
|
+
baseUrl?: string;
|
|
134
|
+
model?: string;
|
|
135
|
+
}
|
|
97
136
|
/** 一次后端选择的结果 */
|
|
98
137
|
type SearchSelection = {
|
|
99
138
|
provider: SearchProvider;
|
|
@@ -104,15 +143,17 @@ type SearchSelection = {
|
|
|
104
143
|
provider: null;
|
|
105
144
|
detail: string;
|
|
106
145
|
};
|
|
107
|
-
declare function createSearchProvider(type: SearchProviderType, config: EpochConfig['search']): SearchProvider;
|
|
146
|
+
declare function createSearchProvider(type: SearchProviderType, config: EpochConfig['search'], native?: NativeContext): SearchProvider;
|
|
108
147
|
/**
|
|
109
148
|
* 挑一个能用的后端。
|
|
110
149
|
*
|
|
111
150
|
* @param config `EpochConfig.search`。整段缺席 = 用户没开这个功能,
|
|
112
151
|
* 但**仍然会按凭据自动探测** —— 环境里已经有 `TAVILY_API_KEY` 的人
|
|
113
152
|
* 不该被要求再写一行配置才能用上。
|
|
153
|
+
* @param native 当前 LLM provider 的类型 / 凭据 / 端点。不给的话 `native`
|
|
154
|
+
* 那一档自动不可用,选择会落到后面几档 —— 老调用点不传也不会崩。
|
|
114
155
|
*/
|
|
115
|
-
declare function selectSearchProvider(config: EpochConfig['search']): SearchSelection;
|
|
156
|
+
declare function selectSearchProvider(config: EpochConfig['search'], native?: NativeContext): SearchSelection;
|
|
116
157
|
|
|
117
158
|
/**
|
|
118
159
|
* SearXNG —— **自托管、无 key** 的那一档(方案 37)。
|
|
@@ -166,6 +207,92 @@ interface BraveOptions {
|
|
|
166
207
|
}
|
|
167
208
|
declare function createBraveProvider(opts?: BraveOptions): SearchProvider;
|
|
168
209
|
|
|
210
|
+
/**
|
|
211
|
+
* native —— 让**当前这个 LLM provider 自己**去搜(2026-09-15)。
|
|
212
|
+
*
|
|
213
|
+
* codex 和 gemini-cli 走的都是这条路:前者把 `{type:"web_search"}` 交给 OpenAI 的
|
|
214
|
+
* Responses API,后者读 Gemini grounding 的 `groundingMetadata`。共同点是
|
|
215
|
+
* **搜索发生在模型那一侧**,于是不需要第二个 API key、也不需要第二个账号。
|
|
216
|
+
*
|
|
217
|
+
* ## 为什么排在自动挑选的第一位
|
|
218
|
+
*
|
|
219
|
+
* 1. **不要新凭据** —— 用的就是已经配好的那把 provider key。
|
|
220
|
+
* 2. **隐私暴露面≈0** —— 查询词发给的是本来就持有整段对话的那一方。
|
|
221
|
+
* 相比之下 tavily / brave / keyless 都是把用户的问题交给一个新的第三方。
|
|
222
|
+
* 3. **有付费关系就有 SLA** —— 不看免费档的脸色。
|
|
223
|
+
*
|
|
224
|
+
* ⚠️ 代价是**一次搜索 = 一个完整的模型轮次**(实测 deepseek-flash:
|
|
225
|
+
* 5776 in / 583 out token),比纯检索端点慢也贵。要换回检索型后端就显式写
|
|
226
|
+
* `search.provider: tavily`;整档关掉是 `search.native: false`。
|
|
227
|
+
*
|
|
228
|
+
* ## 为什么结果里没有摘要,得靠 `summary`
|
|
229
|
+
*
|
|
230
|
+
* Anthropic 系的 `web_search_tool_result` 每一条是
|
|
231
|
+
* `{title, url, encrypted_content, page_age}` —— 正文那一格**是加密的**,
|
|
232
|
+
* 只有发它的那一方解得开。所以这一档填的是 `SearchOutcome.summary`
|
|
233
|
+
* (服务端读完正文之后合成的那段话),判据全文在 `provider.ts` 那一格上。
|
|
234
|
+
*
|
|
235
|
+
* ## 覆盖面
|
|
236
|
+
*
|
|
237
|
+
* 首版只做 **deepseek + anthropic** —— 两家共用 Anthropic Messages 协议,
|
|
238
|
+
* 一份实现就够。`openai`(Responses API)和 `google`(grounding)的传输层
|
|
239
|
+
* 完全不同,是**另外两份实现**,不在这一版里;能力表留着扩展位,
|
|
240
|
+
* 表里没有的 provider 会如实报原因然后落到下一档,不是报错。
|
|
241
|
+
*/
|
|
242
|
+
|
|
243
|
+
interface NativeOptions {
|
|
244
|
+
/** 当前 LLM provider 的类型。不给 = 这一档不可用 */
|
|
245
|
+
providerType?: ProviderType;
|
|
246
|
+
/** 已解析好的 provider key(`config.provider.apiKey`) */
|
|
247
|
+
apiKey?: string;
|
|
248
|
+
/** 用户自定义的 baseUrl;不给就用能力表里的默认值 */
|
|
249
|
+
baseUrl?: string;
|
|
250
|
+
/** 用哪个模型去搜(`config.model`) */
|
|
251
|
+
model?: string;
|
|
252
|
+
/** `search.native`。显式 `false` 才关 */
|
|
253
|
+
enabled?: boolean;
|
|
254
|
+
}
|
|
255
|
+
declare function createNativeProvider(opts?: NativeOptions): SearchProvider;
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* keyless —— 免费公开端点组成的环,**零 key、零服务器**(2026-09-15)。
|
|
259
|
+
*
|
|
260
|
+
* 这一档存在的理由是本仓库原来的一条判据反过来咬了自己:`web_search` 没后端就
|
|
261
|
+
* 不注册,而三家已有后端**全都要凭据**,于是「什么都没配」的用户拿到的不是
|
|
262
|
+
* 「一个可能限流的搜索」,是**根本没有搜索**。实测后果(nebular 2026-09-15):
|
|
263
|
+
* 模型改去用浏览器抓百度结果页,把 SEO 广告和 AI 模板文当成行业数据写进了交付物。
|
|
264
|
+
*
|
|
265
|
+
* ## 为什么可以不要 key
|
|
266
|
+
*
|
|
267
|
+
* Exa 和 Parallel 都对外开着**匿名的 MCP 端点**。这不是钻空子:opencode 和
|
|
268
|
+
* hermes-agent 两个互不相干的项目(一个 TS 一个 Python)各自独立收敛到了
|
|
269
|
+
* 同一组 URL,说明这是厂商有意留的获客入口。给了 key 就是付费档,不给就是免费档,
|
|
270
|
+
* 同一个端点。
|
|
271
|
+
*
|
|
272
|
+
* ## 为什么要轮转,而不是「排个序、第一家优先」
|
|
273
|
+
*
|
|
274
|
+
* 免费档是**按 IP 限流**的。固定先打一家等于把那一家的额度打爆之后才用另一家,
|
|
275
|
+
* 而限流恢复是有冷却的 —— 结果是两家的额度都没吃满,用户却已经在看「搜索不可用」。
|
|
276
|
+
* 模块级游标每次调用挪一格,把负载摊平(照 hermes 的 `_ring_cursor`)。
|
|
277
|
+
*
|
|
278
|
+
* ## 隐私(照抄 hermes 的分寸,不是可选项)
|
|
279
|
+
*
|
|
280
|
+
* - **不发任何用户标识**。
|
|
281
|
+
* - Parallel 的 `session_id` 是**每进程随机**的一个 hex,它只用于那边的限流关联,
|
|
282
|
+
* 不落盘、不跨进程复用。
|
|
283
|
+
* - Parallel 还收一个可选的 `model_name` 分析字段 —— **故意不传**。
|
|
284
|
+
*
|
|
285
|
+
* 即便如此,查询词本身仍然会发给第三方。要整档关掉:`search.keyless: false`。
|
|
286
|
+
*/
|
|
287
|
+
|
|
288
|
+
/** 给用例用:把游标拨回 0,否则用例之间会互相影响 */
|
|
289
|
+
declare function resetKeylessCursor(): void;
|
|
290
|
+
interface KeylessOptions {
|
|
291
|
+
/** `search.keyless`。显式 `false` 才关 —— 缺省是开着的 */
|
|
292
|
+
enabled?: boolean;
|
|
293
|
+
}
|
|
294
|
+
declare function createKeylessProvider(opts?: KeylessOptions): SearchProvider;
|
|
295
|
+
|
|
169
296
|
/**
|
|
170
297
|
* web_search —— 搜一串结果回来,**不抓正文**(方案 37)。
|
|
171
298
|
*
|
|
@@ -226,4 +353,4 @@ declare function createWebSearchTool(provider: SearchProvider, opts?: {
|
|
|
226
353
|
defaultMaxResults?: number;
|
|
227
354
|
}): EpochTool;
|
|
228
355
|
|
|
229
|
-
export { SearchError, type SearchOptions, type SearchOutcome, type SearchProvider, type SearchResult, type SearchSelection, clearSearchCache, createBraveProvider, createSearchProvider, createSearxngProvider, createTavilyProvider, createWebSearchExec, createWebSearchTool, selectSearchProvider, webPlugin };
|
|
356
|
+
export { type NativeContext, SearchError, type SearchOptions, type SearchOutcome, type SearchProvider, type SearchResult, type SearchSelection, clearSearchCache, createBraveProvider, createKeylessProvider, createNativeProvider, createSearchProvider, createSearxngProvider, createTavilyProvider, createWebSearchExec, createWebSearchTool, resetKeylessCursor, selectSearchProvider, webPlugin };
|
package/dist/index.js
CHANGED
|
@@ -11,8 +11,8 @@ function htmlToMarkdown(html) {
|
|
|
11
11
|
);
|
|
12
12
|
cleaned = cleaned.replace(/<br\s*\/?>/gi, "\n");
|
|
13
13
|
cleaned = cleaned.replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, (_, url, text) => {
|
|
14
|
-
const
|
|
15
|
-
return
|
|
14
|
+
const t8 = stripTags(text).trim();
|
|
15
|
+
return t8 ? `[${t8}](${url})` : url;
|
|
16
16
|
});
|
|
17
17
|
cleaned = cleaned.replace(
|
|
18
18
|
/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*>/gi,
|
|
@@ -338,18 +338,20 @@ function clearSearchCache() {
|
|
|
338
338
|
function cacheKey(sessionId, query, opts) {
|
|
339
339
|
return `${sessionId}\0${query.trim().toLowerCase()}\0${JSON.stringify(opts)}`;
|
|
340
340
|
}
|
|
341
|
-
function format(results, ignored) {
|
|
341
|
+
function format(results, ignored, summary) {
|
|
342
342
|
const lines = results.map((r, i) => {
|
|
343
343
|
const date = r.publishedAt ? ` \xB7 ${r.publishedAt}` : "";
|
|
344
344
|
const snippet = r.snippet.length > MAX_SNIPPET ? `${r.snippet.slice(0, MAX_SNIPPET)}\u2026` : r.snippet;
|
|
345
|
-
return `${i + 1}. ${r.title}${date}
|
|
345
|
+
return snippet ? `${i + 1}. ${r.title}${date}
|
|
346
346
|
${r.url}
|
|
347
|
-
${snippet}
|
|
347
|
+
${snippet}` : `${i + 1}. ${r.title}${date}
|
|
348
|
+
${r.url}`;
|
|
348
349
|
});
|
|
349
350
|
const head = `${results.length} \u6761\u7ED3\u679C\uFF08\u53EA\u6709\u6458\u8981\uFF0C\u8981\u6B63\u6587\u8BF7\u5BF9\u5177\u4F53 URL \u8C03 web_fetch\uFF09`;
|
|
351
|
+
const lead = summary ? ["", `\u540E\u7AEF\u7EFC\u8FF0\uFF08\u636E\u4E0B\u5217\u6765\u6E90\u5408\u6210\uFF0C\u5F15\u7528\u65F6\u8BF7\u843D\u5230\u5177\u4F53 URL\uFF09\uFF1A`, summary] : [];
|
|
350
352
|
const tail = ignored.length > 0 ? [`
|
|
351
353
|
\u6CE8\u610F\uFF1A${ignored.join("\uFF1B")}`] : [];
|
|
352
|
-
return [head, "", ...lines, ...tail].join("\n");
|
|
354
|
+
return [head, ...lead, "", ...lines, ...tail].join("\n");
|
|
353
355
|
}
|
|
354
356
|
function stringArray(value) {
|
|
355
357
|
if (!Array.isArray(value)) return void 0;
|
|
@@ -385,7 +387,7 @@ function createWebSearchExec(deps) {
|
|
|
385
387
|
if (cached) {
|
|
386
388
|
return {
|
|
387
389
|
success: true,
|
|
388
|
-
output: format(cached, ["\u672C\u6B21\u7ED3\u679C\u6765\u81EA\u672C\u4F1A\u8BDD\u7F13\u5B58\uFF0C\u672A\u91CD\u65B0\u8054\u7F51"]),
|
|
390
|
+
output: format(cached.results, ["\u672C\u6B21\u7ED3\u679C\u6765\u81EA\u672C\u4F1A\u8BDD\u7F13\u5B58\uFF0C\u672A\u91CD\u65B0\u8054\u7F51"], cached.summary),
|
|
389
391
|
duration: Date.now() - started
|
|
390
392
|
};
|
|
391
393
|
}
|
|
@@ -393,7 +395,10 @@ function createWebSearchExec(deps) {
|
|
|
393
395
|
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
394
396
|
try {
|
|
395
397
|
const outcome = await deps.provider.search(query, { ...options, signal: controller.signal });
|
|
396
|
-
cache.set(key,
|
|
398
|
+
cache.set(key, {
|
|
399
|
+
results: outcome.results,
|
|
400
|
+
...outcome.summary ? { summary: outcome.summary } : {}
|
|
401
|
+
});
|
|
397
402
|
if (outcome.results.length === 0) {
|
|
398
403
|
return {
|
|
399
404
|
success: true,
|
|
@@ -403,7 +408,7 @@ function createWebSearchExec(deps) {
|
|
|
403
408
|
}
|
|
404
409
|
return {
|
|
405
410
|
success: true,
|
|
406
|
-
output: format(outcome.results, outcome.ignored ?? []),
|
|
411
|
+
output: format(outcome.results, outcome.ignored ?? [], outcome.summary),
|
|
407
412
|
duration: Date.now() - started
|
|
408
413
|
};
|
|
409
414
|
} catch (err) {
|
|
@@ -424,7 +429,7 @@ function createWebSearchExec(deps) {
|
|
|
424
429
|
};
|
|
425
430
|
}
|
|
426
431
|
// src/search/registry.ts
|
|
427
|
-
import { t as
|
|
432
|
+
import { t as t7 } from "@epoch-agent/infra";
|
|
428
433
|
// src/search/brave.ts
|
|
429
434
|
import { t } from "@epoch-agent/infra";
|
|
430
435
|
var ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
|
|
@@ -489,22 +494,336 @@ function toFreshness(days) {
|
|
|
489
494
|
if (days <= 366) return "py";
|
|
490
495
|
return void 0;
|
|
491
496
|
}
|
|
492
|
-
// src/search/
|
|
497
|
+
// src/search/keyless.ts
|
|
498
|
+
import { randomUUID } from "crypto";
|
|
499
|
+
import { t as t3 } from "@epoch-agent/infra";
|
|
500
|
+
// src/search/mcp.ts
|
|
493
501
|
import { t as t2 } from "@epoch-agent/infra";
|
|
502
|
+
function pickText(payload) {
|
|
503
|
+
const trimmed = payload.trim();
|
|
504
|
+
if (!trimmed.startsWith("{")) return void 0;
|
|
505
|
+
let data;
|
|
506
|
+
try {
|
|
507
|
+
data = JSON.parse(trimmed);
|
|
508
|
+
} catch {
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
if (data.error && typeof data.error.message === "string") {
|
|
512
|
+
throw new SearchError(data.error.message);
|
|
513
|
+
}
|
|
514
|
+
const content = data.result?.content;
|
|
515
|
+
if (!Array.isArray(content)) return void 0;
|
|
516
|
+
for (const item of content) {
|
|
517
|
+
if (typeof item !== "object" || item === null) continue;
|
|
518
|
+
const text = item.text;
|
|
519
|
+
if (typeof text === "string" && text.length > 0) {
|
|
520
|
+
if (data.result?.isError === true) throw new SearchError(text);
|
|
521
|
+
return text;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return void 0;
|
|
525
|
+
}
|
|
526
|
+
function parseMcpResponse(body) {
|
|
527
|
+
const direct = pickText(body);
|
|
528
|
+
if (direct !== void 0) return direct;
|
|
529
|
+
for (const line of body.split("\n")) {
|
|
530
|
+
if (!line.startsWith("data: ")) continue;
|
|
531
|
+
const text = pickText(line.slice(6));
|
|
532
|
+
if (text !== void 0) return text;
|
|
533
|
+
}
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
async function mcpCall(init) {
|
|
537
|
+
let response;
|
|
538
|
+
try {
|
|
539
|
+
response = await fetch(init.url, {
|
|
540
|
+
method: "POST",
|
|
541
|
+
headers: {
|
|
542
|
+
"Content-Type": "application/json",
|
|
543
|
+
Accept: "application/json, text/event-stream",
|
|
544
|
+
...init.headers
|
|
545
|
+
},
|
|
546
|
+
body: JSON.stringify({
|
|
547
|
+
jsonrpc: "2.0",
|
|
548
|
+
id: 1,
|
|
549
|
+
method: "tools/call",
|
|
550
|
+
params: { name: init.tool, arguments: init.args }
|
|
551
|
+
}),
|
|
552
|
+
...init.signal ? { signal: init.signal } : {}
|
|
553
|
+
});
|
|
554
|
+
} catch (err) {
|
|
555
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
556
|
+
throw new SearchError(t2("web_search_setup.err_timeout", { who: init.who }));
|
|
557
|
+
}
|
|
558
|
+
throw new SearchError(
|
|
559
|
+
t2("web_search_setup.err_unreachable", {
|
|
560
|
+
who: init.who,
|
|
561
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
562
|
+
})
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
if (!response.ok) {
|
|
566
|
+
const hint = response.status === 429 ? t2("web_search_setup.err_rate_limited_hint") : "";
|
|
567
|
+
throw new SearchError(
|
|
568
|
+
t2("web_search_setup.err_http", { who: init.who, status: response.status, hint }),
|
|
569
|
+
response.status
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
const text = parseMcpResponse(await response.text());
|
|
573
|
+
if (text === void 0)
|
|
574
|
+
throw new SearchError(t2("web_search_setup.err_no_content", { who: init.who }));
|
|
575
|
+
return text;
|
|
576
|
+
}
|
|
577
|
+
// src/search/keyless.ts
|
|
578
|
+
var EXA_URL = "https://mcp.exa.ai/mcp";
|
|
579
|
+
var PARALLEL_URL = "https://search.parallel.ai/mcp";
|
|
580
|
+
var RATE_LIMIT_ID = randomUUID().replace(/-/gu, "");
|
|
581
|
+
var RING = ["exa", "parallel"];
|
|
582
|
+
var cursor = 0;
|
|
583
|
+
function resetKeylessCursor() {
|
|
584
|
+
cursor = 0;
|
|
585
|
+
}
|
|
586
|
+
function ringOrder() {
|
|
587
|
+
const start = cursor % RING.length;
|
|
588
|
+
cursor = (cursor + 1) % RING.length;
|
|
589
|
+
return [...RING.slice(start), ...RING.slice(0, start)];
|
|
590
|
+
}
|
|
591
|
+
function parseExa(text, max) {
|
|
592
|
+
const out = [];
|
|
593
|
+
let current = null;
|
|
594
|
+
let inHighlights = false;
|
|
595
|
+
const flush = () => {
|
|
596
|
+
if (current && current.url) {
|
|
597
|
+
out.push({
|
|
598
|
+
title: current.title || current.url,
|
|
599
|
+
url: current.url,
|
|
600
|
+
snippet: current.lines.join(" ").trim(),
|
|
601
|
+
...current.date && current.date !== "N/A" ? { publishedAt: current.date } : {}
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
current = null;
|
|
605
|
+
inHighlights = false;
|
|
606
|
+
};
|
|
607
|
+
for (const line of text.split("\n")) {
|
|
608
|
+
if (line.startsWith("Title: ")) {
|
|
609
|
+
flush();
|
|
610
|
+
current = { title: line.slice(7).trim(), url: "", date: "", lines: [] };
|
|
611
|
+
} else if (!current) {
|
|
612
|
+
continue;
|
|
613
|
+
} else if (line.startsWith("URL: ")) {
|
|
614
|
+
current.url = line.slice(5).trim();
|
|
615
|
+
} else if (line.startsWith("Published: ")) {
|
|
616
|
+
current.date = line.slice(11).trim();
|
|
617
|
+
} else if (line.startsWith("Author: ")) {
|
|
618
|
+
} else if (line.startsWith("Highlights:")) {
|
|
619
|
+
inHighlights = true;
|
|
620
|
+
} else if (inHighlights) {
|
|
621
|
+
const piece = line.trim();
|
|
622
|
+
if (piece && piece !== "...") current.lines.push(piece);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
flush();
|
|
626
|
+
return out.slice(0, max);
|
|
627
|
+
}
|
|
628
|
+
function parseParallel(text, max) {
|
|
629
|
+
let data;
|
|
630
|
+
try {
|
|
631
|
+
data = JSON.parse(text);
|
|
632
|
+
} catch {
|
|
633
|
+
throw new SearchError(t3("web_search_setup.err_bad_json", { who: "parallel" }));
|
|
634
|
+
}
|
|
635
|
+
if (!Array.isArray(data.results)) return [];
|
|
636
|
+
const out = [];
|
|
637
|
+
for (const item of data.results) {
|
|
638
|
+
if (typeof item !== "object" || item === null) continue;
|
|
639
|
+
const row = item;
|
|
640
|
+
if (typeof row.url !== "string" || !row.url) continue;
|
|
641
|
+
const excerpts = Array.isArray(row.excerpts) ? row.excerpts.filter((e) => typeof e === "string") : [];
|
|
642
|
+
out.push({
|
|
643
|
+
title: typeof row.title === "string" && row.title ? row.title : row.url,
|
|
644
|
+
url: row.url,
|
|
645
|
+
snippet: excerpts.join(" ").trim(),
|
|
646
|
+
...typeof row.publish_date === "string" && row.publish_date ? { publishedAt: row.publish_date } : {}
|
|
647
|
+
});
|
|
648
|
+
if (out.length >= max) break;
|
|
649
|
+
}
|
|
650
|
+
return out;
|
|
651
|
+
}
|
|
652
|
+
async function searchOne(vendor, query, options) {
|
|
653
|
+
if (vendor === "exa") {
|
|
654
|
+
const text2 = await mcpCall({
|
|
655
|
+
url: EXA_URL,
|
|
656
|
+
tool: "web_search_exa",
|
|
657
|
+
args: {
|
|
658
|
+
query,
|
|
659
|
+
type: "auto",
|
|
660
|
+
numResults: options.maxResults,
|
|
661
|
+
livecrawl: "fallback"
|
|
662
|
+
},
|
|
663
|
+
who: "exa",
|
|
664
|
+
...options.signal ? { signal: options.signal } : {}
|
|
665
|
+
});
|
|
666
|
+
return parseExa(text2, options.maxResults);
|
|
667
|
+
}
|
|
668
|
+
const text = await mcpCall({
|
|
669
|
+
url: PARALLEL_URL,
|
|
670
|
+
tool: "web_search",
|
|
671
|
+
args: {
|
|
672
|
+
objective: query,
|
|
673
|
+
search_queries: [query],
|
|
674
|
+
session_id: RATE_LIMIT_ID
|
|
675
|
+
},
|
|
676
|
+
who: "parallel",
|
|
677
|
+
...options.signal ? { signal: options.signal } : {}
|
|
678
|
+
});
|
|
679
|
+
return parseParallel(text, options.maxResults);
|
|
680
|
+
}
|
|
681
|
+
function createKeylessProvider(opts = {}) {
|
|
682
|
+
return {
|
|
683
|
+
name: "keyless",
|
|
684
|
+
unavailableReason() {
|
|
685
|
+
return opts.enabled === false ? t3("web_search_setup.keyless_disabled") : null;
|
|
686
|
+
},
|
|
687
|
+
async search(query, options) {
|
|
688
|
+
const failures = [];
|
|
689
|
+
for (const vendor of ringOrder()) {
|
|
690
|
+
try {
|
|
691
|
+
const results = await searchOne(vendor, query, options);
|
|
692
|
+
return {
|
|
693
|
+
results,
|
|
694
|
+
...ignoredOf(options).length > 0 ? { ignored: ignoredOf(options) } : {}
|
|
695
|
+
};
|
|
696
|
+
} catch (err) {
|
|
697
|
+
if (options.signal?.aborted) throw err;
|
|
698
|
+
failures.push(err instanceof Error ? err.message : String(err));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throw new SearchError(
|
|
702
|
+
t3("web_search_setup.keyless_all_failed", {
|
|
703
|
+
tried: failures.join(t3("web_search_setup.tried_sep"))
|
|
704
|
+
})
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
function ignoredOf(options) {
|
|
710
|
+
const ignored = [];
|
|
711
|
+
if (options.recencyDays !== void 0)
|
|
712
|
+
ignored.push(t3("web_search_setup.ignored_recency_keyless"));
|
|
713
|
+
if (options.domains?.length) ignored.push(t3("web_search_setup.ignored_domains_keyless"));
|
|
714
|
+
if (options.excludeDomains?.length) ignored.push(t3("web_search_setup.ignored_exclude_keyless"));
|
|
715
|
+
return ignored;
|
|
716
|
+
}
|
|
717
|
+
// src/search/native.ts
|
|
718
|
+
import { t as t4 } from "@epoch-agent/infra";
|
|
719
|
+
var ANTHROPIC_STYLE = {
|
|
720
|
+
deepseek: { defaultBaseUrl: "https://api.deepseek.com", path: "/anthropic/v1/messages" },
|
|
721
|
+
anthropic: { defaultBaseUrl: "https://api.anthropic.com", path: "/v1/messages" }
|
|
722
|
+
};
|
|
723
|
+
var TOOL_TYPE = "web_search_20250305";
|
|
724
|
+
var MAX_TOKENS = 1500;
|
|
725
|
+
function buildPrompt(query, opts) {
|
|
726
|
+
const sep = t4("web_search_setup.prompt_domain_sep");
|
|
727
|
+
const lines = [t4("web_search_setup.prompt_search", { query })];
|
|
728
|
+
if (opts.domains?.length)
|
|
729
|
+
lines.push(t4("web_search_setup.prompt_only_domains", { domains: opts.domains.join(sep) }));
|
|
730
|
+
if (opts.excludeDomains?.length)
|
|
731
|
+
lines.push(
|
|
732
|
+
t4("web_search_setup.prompt_avoid_domains", { domains: opts.excludeDomains.join(sep) })
|
|
733
|
+
);
|
|
734
|
+
lines.push(t4("web_search_setup.prompt_wrap_up"));
|
|
735
|
+
return lines.join("\n");
|
|
736
|
+
}
|
|
737
|
+
function collectResults(blocks, max) {
|
|
738
|
+
const out = [];
|
|
739
|
+
for (const block of blocks) {
|
|
740
|
+
if (block.type !== "web_search_tool_result" || !Array.isArray(block.content)) continue;
|
|
741
|
+
for (const item of block.content) {
|
|
742
|
+
if (typeof item !== "object" || item === null) continue;
|
|
743
|
+
const row = item;
|
|
744
|
+
if (typeof row.url !== "string" || !row.url) continue;
|
|
745
|
+
out.push({
|
|
746
|
+
title: typeof row.title === "string" && row.title ? row.title : row.url,
|
|
747
|
+
snippet: "",
|
|
748
|
+
url: row.url,
|
|
749
|
+
...typeof row.page_age === "string" && row.page_age ? { publishedAt: row.page_age } : {}
|
|
750
|
+
});
|
|
751
|
+
if (out.length >= max) return out;
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
function collectSummary(blocks) {
|
|
757
|
+
return blocks.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text.trim()).filter((s) => s.length > 0).join("\n\n");
|
|
758
|
+
}
|
|
759
|
+
function createNativeProvider(opts = {}) {
|
|
760
|
+
const spec = opts.providerType ? ANTHROPIC_STYLE[opts.providerType] : void 0;
|
|
761
|
+
return {
|
|
762
|
+
name: "native",
|
|
763
|
+
unavailableReason() {
|
|
764
|
+
if (opts.enabled === false) return t4("web_search_setup.native_disabled");
|
|
765
|
+
if (!opts.providerType) return t4("web_search_setup.native_no_provider");
|
|
766
|
+
if (!spec) return t4("web_search_setup.native_unsupported", { provider: opts.providerType });
|
|
767
|
+
if (!opts.apiKey?.trim()) {
|
|
768
|
+
return t4("web_search_setup.native_need_key", { provider: opts.providerType });
|
|
769
|
+
}
|
|
770
|
+
if (!opts.model?.trim()) return t4("web_search_setup.native_no_model");
|
|
771
|
+
return null;
|
|
772
|
+
},
|
|
773
|
+
async search(query, options) {
|
|
774
|
+
const base = (opts.baseUrl?.trim() || spec.defaultBaseUrl).replace(/\/+$/u, "");
|
|
775
|
+
const json = await searchFetch(
|
|
776
|
+
`${base}${spec.path}`,
|
|
777
|
+
{
|
|
778
|
+
method: "POST",
|
|
779
|
+
headers: {
|
|
780
|
+
"Content-Type": "application/json",
|
|
781
|
+
"x-api-key": opts.apiKey,
|
|
782
|
+
"anthropic-version": "2023-06-01"
|
|
783
|
+
},
|
|
784
|
+
body: JSON.stringify({
|
|
785
|
+
model: opts.model,
|
|
786
|
+
max_tokens: MAX_TOKENS,
|
|
787
|
+
tools: [{ type: TOOL_TYPE, name: "web_search", max_uses: 3 }],
|
|
788
|
+
messages: [{ role: "user", content: buildPrompt(query, options) }]
|
|
789
|
+
}),
|
|
790
|
+
...options.signal ? { signal: options.signal } : {}
|
|
791
|
+
},
|
|
792
|
+
"native"
|
|
793
|
+
);
|
|
794
|
+
const blocks = Array.isArray(json.content) ? json.content ?? [] : [];
|
|
795
|
+
const results = collectResults(blocks, options.maxResults);
|
|
796
|
+
if (results.length === 0) {
|
|
797
|
+
throw new SearchError(
|
|
798
|
+
t4("web_search_setup.native_no_search", { provider: opts.providerType })
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
const summary = collectSummary(blocks);
|
|
802
|
+
const ignored = options.recencyDays === void 0 ? [] : [t4("web_search_setup.ignored_recency_native")];
|
|
803
|
+
return {
|
|
804
|
+
results,
|
|
805
|
+
...summary ? { summary } : {},
|
|
806
|
+
...ignored.length > 0 ? { ignored } : {}
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
// src/search/searxng.ts
|
|
812
|
+
import { t as t5 } from "@epoch-agent/infra";
|
|
494
813
|
function createSearxngProvider(opts) {
|
|
495
814
|
return {
|
|
496
815
|
name: "searxng",
|
|
497
816
|
unavailableReason() {
|
|
498
817
|
if (!opts.baseUrl?.trim()) {
|
|
499
|
-
return
|
|
818
|
+
return t5("web_search_setup.searxng_need_base_url");
|
|
500
819
|
}
|
|
501
820
|
try {
|
|
502
821
|
const url = new URL(opts.baseUrl);
|
|
503
822
|
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
504
|
-
return
|
|
823
|
+
return t5("web_search_setup.searxng_bad_protocol", { protocol: url.protocol });
|
|
505
824
|
}
|
|
506
825
|
} catch {
|
|
507
|
-
return
|
|
826
|
+
return t5("web_search_setup.searxng_bad_url", { url: opts.baseUrl });
|
|
508
827
|
}
|
|
509
828
|
return null;
|
|
510
829
|
},
|
|
@@ -566,7 +885,7 @@ function timeRange(days) {
|
|
|
566
885
|
return void 0;
|
|
567
886
|
}
|
|
568
887
|
// src/search/tavily.ts
|
|
569
|
-
import { t as
|
|
888
|
+
import { t as t6 } from "@epoch-agent/infra";
|
|
570
889
|
var ENDPOINT2 = "https://api.tavily.com/search";
|
|
571
890
|
var MAX_RESULTS2 = 20;
|
|
572
891
|
function createTavilyProvider(opts = {}) {
|
|
@@ -574,7 +893,7 @@ function createTavilyProvider(opts = {}) {
|
|
|
574
893
|
return {
|
|
575
894
|
name: "tavily",
|
|
576
895
|
unavailableReason() {
|
|
577
|
-
return key() ? null :
|
|
896
|
+
return key() ? null : t6("web_search_setup.need_tavily_key");
|
|
578
897
|
},
|
|
579
898
|
async search(query, options) {
|
|
580
899
|
const body = {
|
|
@@ -617,34 +936,54 @@ function createTavilyProvider(opts = {}) {
|
|
|
617
936
|
};
|
|
618
937
|
}
|
|
619
938
|
// src/search/registry.ts
|
|
620
|
-
var AUTO_ORDER = [
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
939
|
+
var AUTO_ORDER = [
|
|
940
|
+
"native",
|
|
941
|
+
"tavily",
|
|
942
|
+
"brave",
|
|
943
|
+
"searxng",
|
|
944
|
+
"keyless"
|
|
945
|
+
];
|
|
946
|
+
function createSearchProvider(type, config, native) {
|
|
947
|
+
switch (type) {
|
|
948
|
+
case "searxng":
|
|
949
|
+
return createSearxngProvider(config?.baseUrl ? { baseUrl: config.baseUrl } : {});
|
|
950
|
+
case "brave":
|
|
951
|
+
return createBraveProvider();
|
|
952
|
+
case "keyless":
|
|
953
|
+
return createKeylessProvider(config?.keyless === false ? { enabled: false } : {});
|
|
954
|
+
case "native":
|
|
955
|
+
return createNativeProvider({
|
|
956
|
+
...native?.providerType ? { providerType: native.providerType } : {},
|
|
957
|
+
...native?.apiKey ? { apiKey: native.apiKey } : {},
|
|
958
|
+
...native?.baseUrl ? { baseUrl: native.baseUrl } : {},
|
|
959
|
+
...native?.model ? { model: native.model } : {},
|
|
960
|
+
...config?.native === false ? { enabled: false } : {}
|
|
961
|
+
});
|
|
962
|
+
default:
|
|
963
|
+
return createTavilyProvider();
|
|
624
964
|
}
|
|
625
|
-
return type === "brave" ? createBraveProvider() : createTavilyProvider();
|
|
626
965
|
}
|
|
627
|
-
function selectSearchProvider(config) {
|
|
966
|
+
function selectSearchProvider(config, native) {
|
|
628
967
|
const explicit = config?.provider;
|
|
629
968
|
if (explicit) {
|
|
630
|
-
const provider = createSearchProvider(explicit, config);
|
|
969
|
+
const provider = createSearchProvider(explicit, config, native);
|
|
631
970
|
const reason = provider.unavailableReason();
|
|
632
971
|
return reason ? {
|
|
633
972
|
provider: null,
|
|
634
|
-
detail:
|
|
635
|
-
} : { provider, detail:
|
|
973
|
+
detail: t7("web_search_setup.explicit_unavailable", { provider: explicit, reason })
|
|
974
|
+
} : { provider, detail: t7("web_search_setup.explicit_ok", { provider: explicit }) };
|
|
636
975
|
}
|
|
637
976
|
const tried = [];
|
|
638
977
|
for (const type of AUTO_ORDER) {
|
|
639
|
-
const provider = createSearchProvider(type, config);
|
|
978
|
+
const provider = createSearchProvider(type, config, native);
|
|
640
979
|
const reason = provider.unavailableReason();
|
|
641
|
-
if (!reason) return { provider, detail:
|
|
980
|
+
if (!reason) return { provider, detail: t7("web_search_setup.auto", { provider: type }) };
|
|
642
981
|
tried.push(reason);
|
|
643
982
|
}
|
|
644
983
|
return {
|
|
645
984
|
provider: null,
|
|
646
|
-
detail:
|
|
647
|
-
tried: tried.join(
|
|
985
|
+
detail: t7("web_search_setup.none", {
|
|
986
|
+
tried: tried.join(t7("web_search_setup.tried_sep"))
|
|
648
987
|
})
|
|
649
988
|
};
|
|
650
989
|
}
|
|
@@ -702,11 +1041,14 @@ export {
|
|
|
702
1041
|
SearchError,
|
|
703
1042
|
clearSearchCache,
|
|
704
1043
|
createBraveProvider,
|
|
1044
|
+
createKeylessProvider,
|
|
1045
|
+
createNativeProvider,
|
|
705
1046
|
createSearchProvider,
|
|
706
1047
|
createSearxngProvider,
|
|
707
1048
|
createTavilyProvider,
|
|
708
1049
|
createWebSearchExec,
|
|
709
1050
|
createWebSearchTool,
|
|
1051
|
+
resetKeylessCursor,
|
|
710
1052
|
selectSearchProvider,
|
|
711
1053
|
webPlugin
|
|
712
1054
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epoch-agent/plugin-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Web 抓取插件 — URL 安全校验 + HTML→Markdown + SSRF 防护",
|
|
6
6
|
"repository": {
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"dist"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@epoch-agent/infra": "0.
|
|
26
|
-
"@epoch-agent/protocol": "0.
|
|
25
|
+
"@epoch-agent/infra": "0.11.0",
|
|
26
|
+
"@epoch-agent/protocol": "0.11.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^22.0.0",
|