@240xu/dsh-websearch 2.3.0 → 2.7.2

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.
@@ -1,121 +0,0 @@
1
- /**
2
- * Unified backend registry for dsh-websearch.
3
- * Auto-discovers and exports all backend implementations.
4
- */
5
-
6
- import { exaBackend } from "./exa.js";
7
- import { parallelBackend } from "./parallel.js";
8
- import { ddgBackend } from "./ddg.js";
9
- import { deepseekBackend, anthropicBackend } from "./anthropic-like.js";
10
- import { openaiBackend } from "./openai.js";
11
- import { braveBackend } from "./brave.js";
12
- import { tavilyBackend } from "./tavily.js";
13
- import { serperBackend } from "./serper.js";
14
- import { searxngBackend } from "./searxng.js";
15
- import { mojeekBackend } from "./mojeek.js";
16
-
17
- // Individual backend exports for direct import
18
- export { exaBackend } from "./exa.js";
19
- export { parallelBackend } from "./parallel.js";
20
- export { ddgBackend } from "./ddg.js";
21
- export { deepseekBackend, anthropicBackend } from "./anthropic-like.js";
22
- export { openaiBackend } from "./openai.js";
23
- export { braveBackend } from "./brave.js";
24
- export { tavilyBackend } from "./tavily.js";
25
- export { serperBackend } from "./serper.js";
26
- export { searxngBackend } from "./searxng.js";
27
- export { mojeekBackend } from "./mojeek.js";
28
-
29
-
30
- /** All registered backends. Order = default fan-out priority. */
31
- export const BACKENDS = {
32
- exa: exaBackend,
33
- parallel: parallelBackend,
34
- ddg: ddgBackend,
35
- searxng: searxngBackend, // keyless, new
36
- brave: braveBackend, // key-gated, new
37
- tavily: tavilyBackend, // key-gated, new
38
- serper: serperBackend, // key-gated, new
39
- mojeek: mojeekBackend, // key-gated, new
40
- deepseek: deepseekBackend,
41
- anthropic: anthropicBackend,
42
- openai: openaiBackend,
43
- };
44
-
45
- /** All backend IDs in registry order. */
46
- export const ALL_BACKEND_IDS = Object.keys(BACKENDS);
47
-
48
- /** Default enabled backends (keyless only). */
49
- export const DEFAULT_ENABLED = ["exa", "parallel", "ddg", "searxng"];
50
-
51
- /** Environment variable names for key-gated backends. */
52
- export const DEFAULT_KEY_ENV = {
53
- deepseek: "DEEPSEEK_API_KEY",
54
- anthropic: "ANTHROPIC_API_KEY",
55
- openai: "OPENAI_API_KEY",
56
- brave: "BRAVE_API_KEY",
57
- tavily: "TAVILY_API_KEY",
58
- serper: "SERPER_API_KEY",
59
- mojeek: "MOJEEK_API_KEY",
60
- };
61
-
62
- /** Default base URLs for each backend. */
63
- export const DEFAULT_BASE_URLS = {
64
- exa: "https://mcp.exa.ai/mcp",
65
- parallel: "https://search.parallel.ai/mcp",
66
- ddg: "https://html.duckduckgo.com/html/",
67
- searxng: "https://searx.be",
68
- brave: "https://api.search.brave.com/res/v1/web/search",
69
- tavily: "https://api.tavily.com/search",
70
- serper: "https://google.serper.dev/search",
71
- mojeek: "https://api.mojeek.com/v1/search",
72
- deepseek: "https://api.deepseek.com/anthropic/v1",
73
- anthropic: "https://api.anthropic.com/v1",
74
- openai: "https://api.openai.com/v1",
75
- };
76
-
77
- /** Backend type classification for GUI. */
78
- export const BACKEND_TYPES = {
79
- exa: "mcp",
80
- parallel: "mcp",
81
- ddg: "keyless",
82
- searxng: "keyless",
83
- brave: "credential",
84
- tavily: "credential",
85
- serper: "credential",
86
- mojeek: "credential",
87
- deepseek: "credential",
88
- anthropic: "credential",
89
- openai: "credential",
90
- };
91
-
92
- /** Human-readable labels for GUI. */
93
- export const BACKEND_LABELS = {
94
- exa: "Exa (MCP, 免费无 Key)",
95
- parallel: "Parallel (MCP, 免费无 Key)",
96
- ddg: "DuckDuckGo (HTML 抓取, 兜底)",
97
- searxng: "SearXNG (元搜索, 免费无 Key)",
98
- brave: "Brave Search (独立索引, 2000/月免费)",
99
- tavily: "Tavily (AI 专用, 含答案摘要)",
100
- serper: "Serper.dev (Google 抓取, 2500/月免费)",
101
- mojeek: "Mojeek (独立索引, 1000/天免费)",
102
- deepseek: "DeepSeek (原生 web_search, 需 Key)",
103
- anthropic: "Anthropic (Claude 原生 web_search, 需 Key)",
104
- openai: "OpenAI (Responses API web_search, 需 Key)",
105
- };
106
-
107
- /** Backend descriptions for GUI tooltips. */
108
- export const BACKEND_DESCRIPTIONS = {
109
- exa: "流式 HTTP MCP 协议,原生 web_search_exa 工具,结果含标题/URL/高亮摘要",
110
- parallel: "流式 HTTP MCP 协议,原生 web_search 工具,返回结构化 JSON 含摘要数组",
111
- ddg: "HTML 页面抓取解析,通过 uddg= 参数还原真实 URL,零依赖兜底",
112
- searxng: "开源元搜索引擎聚合,可指向公共实例或自托管,隐私友好",
113
- brave: "Brave 独立搜索索引,REST API,免费 2000 次/月,结果质量高",
114
- tavily: "AI 专用搜索,可返回 AI 生成答案 + 结构化结果,支持深度搜索模式",
115
- serper: "抓取 Google SERP,结构化 organic[] 结果,免费 2500 次/月,速度极快",
116
- mojeek: "英国独立搜索引擎,无追踪,免费 1000 次/天,英文为主",
117
- deepseek: "DeepSeek Anthropic 兼容 API + 原生 web_search_20250305 工具",
118
- anthropic: "Claude 官方 Messages API + web_search_20250305 原生工具",
119
- openai: "OpenAI Responses API + 原生 web_search 工具,解析 url_citation 注释",
120
- };
121
-
package/lib/util/rpc.js DELETED
@@ -1,262 +0,0 @@
1
- /**
2
- * @module dsh-websearch/util/rpc
3
- *
4
- * Minimal MCP JSON-RPC 2.0 client for the streamable-http transport used by
5
- * Exa (https://mcp.exa.ai/mcp) and Parallel (https://search.parallel.ai/mcp).
6
- *
7
- * Streamable-HTTP MCP servers require a 2-step handshake per session:
8
- * 1. POST initialize → server replies with its capabilities and (for Exa)
9
- * a Mcp-Session-Id header. Parallel also returns this header.
10
- * 2. POST notifications/initialized → notify the server the client is ready.
11
- * (Exa rejects tools/call without this; Parallel tolerates its absence
12
- * but sending it is harmless.)
13
- * 3. POST tools/call with the Mcp-Session-Id header → returns either a plain
14
- * JSON body OR a text/event-stream of "data: {…}\n\n" frames.
15
- *
16
- * We cache the Mcp-Session-Id per server URL inside the process so subsequent
17
- * searches skip the handshake (and one cache entry survives across calls).
18
- * Sessions are NOT shared across processes (each dsh-web worker owns its own).
19
- *
20
- * Failures are zh-folded into WebError so the provider fan-out can demote them
21
- * to soft nulls when only one backend fails.
22
- */
23
- import { WebError } from "@deepseek-ai/dsh-web";
24
- import { throwIfSearchAborted, searchAborted, isAbortError } from "./abort.js";
25
-
26
- /** User-Agent header; bumped with the package version. */
27
- export const UA = "dsh-websearch/2.1";
28
-
29
- /** MCP protocol version we negotiate. */
30
- const PROTOCOL_VERSION = "2025-06-18";
31
-
32
- /** Per-URL session cache: url → { sessionId, initializedAt }. */
33
- const sessionCache = new Map();
34
- const SESSION_TTL_MS = 10 * 60 * 1000; // Exa sessions live indefinitely; this is a courtesy cap.
35
-
36
- /** Monotonic request id so concurrent inits don't collide. */
37
- let nextReqId = 1;
38
-
39
- /**
40
- * Initialize (or reuse) a streamable-http MCP session.
41
- * Sends initialize + notifications/initialized. Returns the Mcp-Session-Id,
42
- * caching it so callers don't pay the handshake more than once per TTL.
43
- *
44
- * @param {string} url
45
- * @param {AbortSignal} [signal]
46
- * @returns {Promise<string|null>} Mcp-Session-Id (or null if the server omits it).
47
- * @throws {WebError} on transport error or abort.
48
- */
49
- async function ensureSession(url, signal) {
50
- throwIfSearchAborted(signal);
51
- const cached = sessionCache.get(url);
52
- if (cached && Date.now() - cached.initializedAt < SESSION_TTL_MS && cached.sessionId) {
53
- return cached.sessionId;
54
- }
55
-
56
- const initId = nextReqId++;
57
- const initBody = JSON.stringify({
58
- jsonrpc: "2.0",
59
- id: initId,
60
- method: "initialize",
61
- params: {
62
- protocolVersion: PROTOCOL_VERSION,
63
- capabilities: {},
64
- clientInfo: { name: "dsh-websearch", version: "1.0" },
65
- },
66
- });
67
-
68
- let res;
69
- try {
70
- res = await fetch(url, {
71
- method: "POST",
72
- redirect: "error",
73
- headers: {
74
- "Content-Type": "application/json",
75
- "Accept": "application/json, text/event-stream",
76
- "User-Agent": UA,
77
- },
78
- body: initBody,
79
- ...(signal !== undefined ? { signal } : {}),
80
- });
81
- } catch (error) {
82
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
83
- throw new WebError(
84
- `mcp initialize fetch failed for ${url}: ${String(error?.message ?? error)}`,
85
- "WEB_PROVIDER_ERROR",
86
- { cause: error },
87
- );
88
- }
89
- throwIfSearchAborted(signal);
90
- if (!res.ok) {
91
- let detail = "";
92
- try { detail = (await res.text()).slice(0, 300); } catch {}
93
- throw new WebError(
94
- `mcp initialize HTTP ${res.status} for ${url}${detail ? ": " + detail : ""}`,
95
- "WEB_PROVIDER_ERROR",
96
- );
97
- }
98
-
99
- // Drain the initialize response body (we don't need its content).
100
- await res.text();
101
-
102
- const sessionId = res.headers.get("Mcp-Session-Id") || null;
103
-
104
- // Send notifications/initialized (some servers reject tools/call without it).
105
- if (sessionId) {
106
- try {
107
- await fetch(url, {
108
- method: "POST",
109
- redirect: "error",
110
- headers: {
111
- "Content-Type": "application/json",
112
- "Accept": "application/json, text/event-stream",
113
- "User-Agent": UA,
114
- "Mcp-Session-Id": sessionId,
115
- },
116
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
117
- ...(signal !== undefined ? { signal } : {}),
118
- });
119
- } catch (error) {
120
- // notifications/initialized is best-effort; ignore transport errors here.
121
- // (If the session is gone we'll discover that via tools/call.) Re-raise aborts.
122
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
123
- }
124
- }
125
-
126
- sessionCache.set(url, { sessionId, initializedAt: Date.now() });
127
- return sessionId;
128
- }
129
-
130
- /**
131
- * Parse a streamable-HTTP MCP response body into the first JSON-RPC envelope.
132
- *
133
- * Servers respond with EITHER:
134
- * - a single JSON object (Parallel: {"jsonrpc":"2.0","id":N,"result":{…}} or {...,"error":{…}}), OR
135
- * - a text/event-stream of "event: <type>\n data: {…}\n\n" frames (Exa).
136
- *
137
- * We scan line-by-line; SSE data: frames take precedence, otherwise we fall back
138
- * to scanning whole-line JSON.
139
- *
140
- * @param {string} text
141
- * @param {number} reqId the request id we expect to be echoed back
142
- * @returns {{result?:object, error?:object}}
143
- * @throws if no parseable envelope is found.
144
- */
145
- function parseEnvelope(text, reqId) {
146
- const lines = String(text).split("\n");
147
- let lastJson = null;
148
- for (const raw of lines) {
149
- const line = raw.trim();
150
- if (!line) continue;
151
- let json;
152
- if (line.startsWith("data: ")) {
153
- json = line.slice(6).trim();
154
- } else if (line.startsWith("{")) {
155
- json = line;
156
- } else {
157
- continue;
158
- }
159
- if (!json.startsWith("{")) continue;
160
- let obj;
161
- try { obj = JSON.parse(json); } catch { continue; }
162
- // Prefer the envelope matching our request id; otherwise keep the last seen.
163
- if (typeof obj.id === "number" && obj.id === reqId) return obj;
164
- lastJson = obj;
165
- }
166
- return lastJson;
167
- }
168
-
169
- /**
170
- * JSON-RPC 2.0 tools/call against a streamable-http MCP server (after the
171
- * initialize + notifications/initialized handshake is established or reused).
172
- *
173
- * @param {string} url the MCP server endpoint
174
- * @param {string} toolName the tool to invoke (e.g. "web_search_exa", "web_search")
175
- * @param {object} args the tool's arguments (per its input schema)
176
- * @param {object} [extraHeaders]
177
- * @param {AbortSignal} [signal]
178
- * @returns {Promise<object>} the RPC `result` payload (the `content` array etc.)
179
- * @throws {WebError} on HTTP failure, RPC error, abort, or unparseable response.
180
- */
181
- export async function rpcCall(url, toolName, args, extraHeaders, signal) {
182
- const sessionId = await ensureSession(url, signal);
183
- const reqId = nextReqId++;
184
-
185
- const headers = {
186
- "Content-Type": "application/json",
187
- "Accept": "application/json, text/event-stream",
188
- "User-Agent": UA,
189
- ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
190
- ...(extraHeaders || {}),
191
- };
192
- const body = JSON.stringify({
193
- jsonrpc: "2.0",
194
- id: reqId,
195
- method: "tools/call",
196
- params: { name: toolName, arguments: args },
197
- });
198
-
199
- let res;
200
- try {
201
- res = await fetch(url, {
202
- method: "POST",
203
- redirect: "error",
204
- headers,
205
- body,
206
- ...(signal !== undefined ? { signal } : {}),
207
- });
208
- } catch (error) {
209
- if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
210
- throw new WebError(
211
- `mcp rpc ${toolName} fetch failed: ${String(error?.message ?? error)}`,
212
- "WEB_PROVIDER_ERROR",
213
- { cause: error },
214
- );
215
- }
216
- throwIfSearchAborted(signal);
217
-
218
- if (!res.ok) {
219
- let detail = "";
220
- try { detail = (await res.text()).slice(0, 300); } catch {}
221
- // If the server rejected the request id/session, nuke the cached session
222
- // so the next call re-initializes. 404/401/403 typically mean session death.
223
- if (res.status === 401 || res.status === 403 || res.status === 404) {
224
- sessionCache.delete(url);
225
- }
226
- throw new WebError(
227
- `mcp rpc ${toolName} HTTP ${res.status}${detail ? ": " + detail : ""}`,
228
- "WEB_PROVIDER_ERROR",
229
- );
230
- }
231
-
232
- const text = await res.text();
233
- throwIfSearchAborted(signal);
234
-
235
- const env = parseEnvelope(text, reqId);
236
- if (!env) {
237
- throw new WebError(
238
- `mcp rpc ${toolName} returned an unparseable response`,
239
- "WEB_PROVIDER_ERROR",
240
- );
241
- }
242
- if (env.error) {
243
- throw new WebError(
244
- `mcp rpc ${toolName} error: ${env.error.message || JSON.stringify(env.error)}`,
245
- "WEB_PROVIDER_ERROR",
246
- );
247
- }
248
- if (!env.result) {
249
- throw new WebError(
250
- `mcp rpc ${toolName} returned a response with no result`,
251
- "WEB_PROVIDER_ERROR",
252
- );
253
- }
254
- return env.result;
255
- }
256
-
257
- /** Drop the cached session for a url (used by backends after a fatal error). */
258
- export function invalidateSession(url) {
259
- sessionCache.delete(url);
260
- }
261
-
262
- export { throwIfSearchAborted, searchAborted, isAbortError };