@duckmind/dm-windows-x64 0.63.0 → 0.63.4
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/dm.exe +0 -0
- package/extensions/.dm-extensions.json +138 -2
- package/extensions/dm-web-access/SECURITY.md +5 -0
- package/extensions/dm-web-access/activity.js +65 -0
- package/extensions/dm-web-access/anysearch.js +158 -0
- package/extensions/dm-web-access/auth-fetch.js +131 -0
- package/extensions/dm-web-access/bocha.js +214 -0
- package/extensions/dm-web-access/brave.js +196 -0
- package/extensions/dm-web-access/brightdata-unlocker.js +202 -0
- package/extensions/dm-web-access/brightdata.js +334 -0
- package/extensions/dm-web-access/chrome-cookies.js +627 -0
- package/extensions/dm-web-access/content-find.js +114 -0
- package/extensions/dm-web-access/credential-source.js +150 -0
- package/extensions/dm-web-access/curator-page.js +3559 -0
- package/extensions/dm-web-access/curator-server.js +691 -0
- package/extensions/dm-web-access/data-uri-sanitize.js +312 -0
- package/extensions/dm-web-access/datalab-pdf-extract.js +346 -0
- package/extensions/dm-web-access/declared-web-links.js +167 -0
- package/extensions/dm-web-access/dm-web-fetch-demo.mp4 +0 -0
- package/extensions/dm-web-access/duckduckgo.js +118 -0
- package/extensions/dm-web-access/exa.js +401 -0
- package/extensions/dm-web-access/extract.js +1220 -0
- package/extensions/dm-web-access/feature-config.js +24 -0
- package/extensions/dm-web-access/fetch-params.js +81 -0
- package/extensions/dm-web-access/firecrawl.js +378 -0
- package/extensions/dm-web-access/gemini-adc.js +241 -0
- package/extensions/dm-web-access/gemini-api.js +258 -0
- package/extensions/dm-web-access/gemini-pdf-extract.js +74 -0
- package/extensions/dm-web-access/gemini-search.js +889 -0
- package/extensions/dm-web-access/gemini-url-context.js +97 -0
- package/extensions/dm-web-access/gemini-web-config.js +84 -0
- package/extensions/dm-web-access/gemini-web.js +351 -0
- package/extensions/dm-web-access/github-api.js +166 -0
- package/extensions/dm-web-access/github-extract.js +991 -0
- package/extensions/dm-web-access/github-issue-pr.js +750 -0
- package/extensions/dm-web-access/index.js +3117 -0
- package/extensions/dm-web-access/jina-search.js +242 -0
- package/extensions/dm-web-access/kagi.js +255 -0
- package/extensions/dm-web-access/kimi-search.js +214 -0
- package/extensions/dm-web-access/ollama.js +209 -0
- package/extensions/dm-web-access/openai-search.js +510 -0
- package/extensions/dm-web-access/package.json +34 -0
- package/extensions/dm-web-access/page-query.js +124 -0
- package/extensions/dm-web-access/parallel-mcp.js +223 -0
- package/extensions/dm-web-access/parallel.js +345 -0
- package/extensions/dm-web-access/pdf-extract.js +257 -0
- package/extensions/dm-web-access/perplexity.js +151 -0
- package/extensions/dm-web-access/querit.js +327 -0
- package/extensions/dm-web-access/query-rewrite.js +40 -0
- package/extensions/dm-web-access/render-search-error.js +80 -0
- package/extensions/dm-web-access/rsc-extract.js +347 -0
- package/extensions/dm-web-access/search1api.js +245 -0
- package/extensions/dm-web-access/searchinfinity.js +221 -0
- package/extensions/dm-web-access/searxng.js +223 -0
- package/extensions/dm-web-access/serpbase.js +205 -0
- package/extensions/dm-web-access/serpdive.js +238 -0
- package/extensions/dm-web-access/serper.js +200 -0
- package/extensions/dm-web-access/source-check.js +198 -0
- package/extensions/dm-web-access/ssrf-protection.js +436 -0
- package/extensions/dm-web-access/storage.js +451 -0
- package/extensions/dm-web-access/summary-model-scope.js +83 -0
- package/extensions/dm-web-access/summary-review.js +364 -0
- package/extensions/dm-web-access/tavily.js +199 -0
- package/extensions/dm-web-access/tinyfish.js +325 -0
- package/extensions/dm-web-access/utils.js +476 -0
- package/extensions/dm-web-access/valyu.js +189 -0
- package/extensions/dm-web-access/video-extract.js +336 -0
- package/extensions/dm-web-access/xai-search.js +285 -0
- package/extensions/dm-web-access/xcrawl.js +221 -0
- package/extensions/dm-web-access/youtube-extract.js +279 -0
- package/package.json +9 -1
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { activityMonitor } from "./activity.js";
|
|
2
|
+
import { redactCredential } from "./credential-source.js";
|
|
3
|
+
import { resolveParallelApiKey } from "./parallel.js";
|
|
4
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
5
|
+
const PARALLEL_MCP_URL = "https://search.parallel.ai/mcp";
|
|
6
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
7
|
+
const REQUEST_TIMEOUT_MS = 60000;
|
|
8
|
+
function requestSignal(signal) {
|
|
9
|
+
const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS);
|
|
10
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
11
|
+
}
|
|
12
|
+
function errorMessage(err) {
|
|
13
|
+
return err instanceof Error ? err.message : String(err);
|
|
14
|
+
}
|
|
15
|
+
function normalizeDomain(value) {
|
|
16
|
+
let input = value.trim().toLowerCase();
|
|
17
|
+
if (input.startsWith("-"))
|
|
18
|
+
input = input.slice(1).trim();
|
|
19
|
+
if (!input)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
input = (input.includes("://") ? new URL(input) : new URL(`https://${input}`)).hostname;
|
|
23
|
+
} catch {
|
|
24
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
25
|
+
}
|
|
26
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
27
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
28
|
+
}
|
|
29
|
+
function domainMatches(hostname, domain) {
|
|
30
|
+
return hostname === domain || hostname.endsWith(`.${domain}`);
|
|
31
|
+
}
|
|
32
|
+
function filterResults(results, domainFilter) {
|
|
33
|
+
if (!domainFilter?.length)
|
|
34
|
+
return results;
|
|
35
|
+
const included = domainFilter.filter((value) => !value.trim().startsWith("-")).map(normalizeDomain).filter((value) => value !== null);
|
|
36
|
+
const excluded = domainFilter.filter((value) => value.trim().startsWith("-")).map(normalizeDomain).filter((value) => value !== null);
|
|
37
|
+
return results.filter((result) => {
|
|
38
|
+
let hostname;
|
|
39
|
+
try {
|
|
40
|
+
hostname = new URL(result.url).hostname.toLowerCase();
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return (included.length === 0 || included.some((domain) => domainMatches(hostname, domain))) && !excluded.some((domain) => domainMatches(hostname, domain));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function buildSearchQuery(query, options) {
|
|
48
|
+
const parts = [query];
|
|
49
|
+
for (const raw of options.domainFilter ?? []) {
|
|
50
|
+
const domain = normalizeDomain(raw);
|
|
51
|
+
if (domain)
|
|
52
|
+
parts.push(raw.trim().startsWith("-") ? `-site:${domain}` : `site:${domain}`);
|
|
53
|
+
}
|
|
54
|
+
if (options.recencyFilter) {
|
|
55
|
+
const labels = { day: "past 24 hours", week: "past week", month: "past month", year: "past year" };
|
|
56
|
+
parts.push(labels[options.recencyFilter]);
|
|
57
|
+
}
|
|
58
|
+
return parts.join(" ");
|
|
59
|
+
}
|
|
60
|
+
async function callParallelMcp(toolName, args, signal) {
|
|
61
|
+
const apiKey = await resolveParallelApiKey(signal);
|
|
62
|
+
const headers = {
|
|
63
|
+
"Content-Type": "application/json",
|
|
64
|
+
Accept: "application/json, text/event-stream"
|
|
65
|
+
};
|
|
66
|
+
if (apiKey)
|
|
67
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
68
|
+
let response;
|
|
69
|
+
try {
|
|
70
|
+
response = await fetch(PARALLEL_MCP_URL, {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers,
|
|
73
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: toolName, arguments: args } }),
|
|
74
|
+
signal: requestSignal(signal)
|
|
75
|
+
});
|
|
76
|
+
} catch (err) {
|
|
77
|
+
const message = apiKey ? redactCredential(errorMessage(err), apiKey) : errorMessage(err);
|
|
78
|
+
if (message === errorMessage(err))
|
|
79
|
+
throw err;
|
|
80
|
+
throw new Error(message);
|
|
81
|
+
}
|
|
82
|
+
const body = await response.text();
|
|
83
|
+
const safeBody = apiKey ? redactCredential(body, apiKey) : body;
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
if (response.status === 429) {
|
|
86
|
+
throw new Error(`Parallel MCP rate limit reached (429). Add parallelApiKey to ${CONFIG_PATH} or set PARALLEL_API_KEY for higher limits: ${safeBody.slice(0, 200)}`);
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Parallel MCP error ${response.status}: ${safeBody.slice(0, 300)}`);
|
|
89
|
+
}
|
|
90
|
+
let parsed = null;
|
|
91
|
+
for (const candidateText of [...body.split(`
|
|
92
|
+
`).filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trim()), body]) {
|
|
93
|
+
if (!candidateText)
|
|
94
|
+
continue;
|
|
95
|
+
try {
|
|
96
|
+
const candidate = JSON.parse(candidateText);
|
|
97
|
+
if (candidate.result || candidate.error) {
|
|
98
|
+
parsed = candidate;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
}
|
|
103
|
+
if (!parsed)
|
|
104
|
+
throw new Error("Parallel MCP returned invalid JSON-RPC content");
|
|
105
|
+
if (parsed.error) {
|
|
106
|
+
const code = typeof parsed.error.code === "number" ? ` ${parsed.error.code}` : "";
|
|
107
|
+
const message = apiKey ? redactCredential(parsed.error.message || "Unknown error", apiKey) : parsed.error.message || "Unknown error";
|
|
108
|
+
throw new Error(`Parallel MCP error${code}: ${message}`);
|
|
109
|
+
}
|
|
110
|
+
if (parsed.result?.isError) {
|
|
111
|
+
const text = parsed.result.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text?.trim() || "Parallel MCP returned an error";
|
|
112
|
+
throw new Error(apiKey ? redactCredential(text, apiKey) : text);
|
|
113
|
+
}
|
|
114
|
+
if (parsed.result?.structuredContent && typeof parsed.result.structuredContent === "object")
|
|
115
|
+
return parsed.result.structuredContent;
|
|
116
|
+
const text = parsed.result?.content?.find((item) => item.type === "text" && typeof item.text === "string" && item.text.trim())?.text;
|
|
117
|
+
if (!text)
|
|
118
|
+
throw new Error("Parallel MCP returned empty content");
|
|
119
|
+
try {
|
|
120
|
+
return JSON.parse(text);
|
|
121
|
+
} catch {
|
|
122
|
+
return text;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function parseTextResults(text) {
|
|
126
|
+
return text.split(/(?=^Title: )/m).flatMap((block) => {
|
|
127
|
+
const url = block.match(/^URL: (.+)/m)?.[1]?.trim();
|
|
128
|
+
if (!url)
|
|
129
|
+
return [];
|
|
130
|
+
const title = block.match(/^Title: (.+)/m)?.[1]?.trim() ?? "";
|
|
131
|
+
const textStart = block.indexOf(`
|
|
132
|
+
Text: `);
|
|
133
|
+
const excerpt = textStart >= 0 ? block.slice(textStart + 7).replace(/\n---\s*$/, "").trim() : "";
|
|
134
|
+
return [{ url, title, excerpts: excerpt ? [excerpt] : [] }];
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function getResults(payload) {
|
|
138
|
+
if (typeof payload === "string")
|
|
139
|
+
return parseTextResults(payload);
|
|
140
|
+
if (!payload || typeof payload !== "object")
|
|
141
|
+
return [];
|
|
142
|
+
const results = payload.results;
|
|
143
|
+
if (!Array.isArray(results))
|
|
144
|
+
return [];
|
|
145
|
+
return results.filter((item) => !!item && typeof item === "object" && typeof item.url === "string");
|
|
146
|
+
}
|
|
147
|
+
function excerpts(result) {
|
|
148
|
+
return Array.isArray(result.excerpts) ? result.excerpts.filter((item) => typeof item === "string" && item.trim()) : [];
|
|
149
|
+
}
|
|
150
|
+
export function isParallelMcpAvailable() {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
export async function searchWithParallelMcp(query, options = {}) {
|
|
154
|
+
const activityId = activityMonitor.logStart({ type: "api", query });
|
|
155
|
+
try {
|
|
156
|
+
const searchQuery = buildSearchQuery(query, options);
|
|
157
|
+
const payload = await callParallelMcp("web_search", { objective: query, search_queries: [searchQuery] }, options.signal);
|
|
158
|
+
const maxResults = Math.max(1, Math.min(Math.floor(options.numResults ?? 5), 20));
|
|
159
|
+
const results = filterResults(getResults(payload), options.domainFilter).slice(0, maxResults);
|
|
160
|
+
activityMonitor.logComplete(activityId, 200);
|
|
161
|
+
const response = {
|
|
162
|
+
answer: results.flatMap((result, index) => {
|
|
163
|
+
const content = excerpts(result).join(" ").trim();
|
|
164
|
+
return content ? [`${content}
|
|
165
|
+
Source: ${result.title || `Source ${index + 1}`} (${result.url})`] : [];
|
|
166
|
+
}).join(`
|
|
167
|
+
|
|
168
|
+
`),
|
|
169
|
+
results: results.map((result, index) => ({
|
|
170
|
+
title: result.title || `Source ${index + 1}`,
|
|
171
|
+
url: result.url,
|
|
172
|
+
snippet: excerpts(result)[0]?.replace(/\s+/g, " ").trim().slice(0, 200) ?? ""
|
|
173
|
+
}))
|
|
174
|
+
};
|
|
175
|
+
if (options.includeContent) {
|
|
176
|
+
const inlineContent = results.flatMap((result) => {
|
|
177
|
+
const content = excerpts(result).join(`
|
|
178
|
+
|
|
179
|
+
`);
|
|
180
|
+
return content ? [{ url: result.url, title: result.title || "", content, error: null }] : [];
|
|
181
|
+
});
|
|
182
|
+
if (inlineContent.length)
|
|
183
|
+
response.inlineContent = inlineContent;
|
|
184
|
+
}
|
|
185
|
+
return response;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
const message = errorMessage(err);
|
|
188
|
+
if (message.toLowerCase().includes("abort"))
|
|
189
|
+
activityMonitor.logComplete(activityId, 0);
|
|
190
|
+
else
|
|
191
|
+
activityMonitor.logError(activityId, message);
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export async function extractWithParallelMcp(url, signal, options = {}) {
|
|
196
|
+
const activityId = activityMonitor.logStart({ type: "fetch", url });
|
|
197
|
+
try {
|
|
198
|
+
const objective = options.prompt?.trim();
|
|
199
|
+
const payload = await callParallelMcp("web_fetch", {
|
|
200
|
+
urls: [url],
|
|
201
|
+
...objective ? { objective: objective.slice(0, 200) } : {},
|
|
202
|
+
full_content: true
|
|
203
|
+
}, signal);
|
|
204
|
+
const result = getResults(payload).find((item) => item.url === url) ?? getResults(payload)[0];
|
|
205
|
+
if (!result) {
|
|
206
|
+
activityMonitor.logComplete(activityId, 200);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const fullContent = typeof result.full_content === "string" ? result.full_content.trim() : "";
|
|
210
|
+
const content = fullContent || excerpts(result).join(`
|
|
211
|
+
|
|
212
|
+
`);
|
|
213
|
+
activityMonitor.logComplete(activityId, 200);
|
|
214
|
+
return content ? { url: result.url, title: result.title || "", content, error: null } : null;
|
|
215
|
+
} catch (err) {
|
|
216
|
+
const message = errorMessage(err);
|
|
217
|
+
if (message.toLowerCase().includes("abort"))
|
|
218
|
+
activityMonitor.logComplete(activityId, 0);
|
|
219
|
+
else
|
|
220
|
+
activityMonitor.logError(activityId, message);
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { activityMonitor } from "./activity.js";
|
|
3
|
+
import { hasCredentialSource, redactCredential, resolveCredential } from "./credential-source.js";
|
|
4
|
+
import { getWebSearchConfigPath } from "./utils.js";
|
|
5
|
+
const PARALLEL_SEARCH_URL = "https://api.parallel.ai/v1/search";
|
|
6
|
+
const PARALLEL_EXTRACT_URL = "https://api.parallel.ai/v1/extract";
|
|
7
|
+
const CONFIG_PATH = getWebSearchConfigPath();
|
|
8
|
+
const MIN_PARALLEL_API_KEY_LENGTH = 8;
|
|
9
|
+
const MIN_USEFUL_CONTENT = 500;
|
|
10
|
+
const SEARCH_TIMEOUT_MS = 60000;
|
|
11
|
+
const PLACEHOLDER_API_KEY_DENYLIST = new Set([
|
|
12
|
+
"replace_with_your_parallel_api_key",
|
|
13
|
+
"parallel_api_key",
|
|
14
|
+
"your-key",
|
|
15
|
+
"your-key-here",
|
|
16
|
+
"your-api-key-here",
|
|
17
|
+
"dummy",
|
|
18
|
+
"placeholder",
|
|
19
|
+
"changeme",
|
|
20
|
+
"insert-your-key",
|
|
21
|
+
"insert-your-key-here",
|
|
22
|
+
"api-key",
|
|
23
|
+
"xxx"
|
|
24
|
+
]);
|
|
25
|
+
let cachedConfig = null;
|
|
26
|
+
function loadConfig() {
|
|
27
|
+
if (cachedConfig)
|
|
28
|
+
return cachedConfig;
|
|
29
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
30
|
+
cachedConfig = {};
|
|
31
|
+
return cachedConfig;
|
|
32
|
+
}
|
|
33
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
34
|
+
try {
|
|
35
|
+
cachedConfig = JSON.parse(raw);
|
|
36
|
+
return cachedConfig;
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
throw new Error(`Failed to parse ${CONFIG_PATH}: ${message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function clearParallelConfigCache() {
|
|
43
|
+
cachedConfig = null;
|
|
44
|
+
}
|
|
45
|
+
function normalizeApiKey(value) {
|
|
46
|
+
if (typeof value !== "string")
|
|
47
|
+
return null;
|
|
48
|
+
const normalized = value.trim();
|
|
49
|
+
return normalized.length > 0 ? normalized : null;
|
|
50
|
+
}
|
|
51
|
+
function isPlaceholderApiKey(key) {
|
|
52
|
+
const normalized = key.trim();
|
|
53
|
+
return normalized.length < MIN_PARALLEL_API_KEY_LENGTH || PLACEHOLDER_API_KEY_DENYLIST.has(normalized.toLowerCase());
|
|
54
|
+
}
|
|
55
|
+
export async function resolveParallelApiKey(signal) {
|
|
56
|
+
const configKey = normalizeApiKey(loadConfig().parallelApiKey);
|
|
57
|
+
if (configKey?.startsWith("$") || configKey?.startsWith("!")) {
|
|
58
|
+
const resolved = await resolveCredential({
|
|
59
|
+
provider: "Parallel",
|
|
60
|
+
configuredValue: configKey,
|
|
61
|
+
environmentValue: process.env.PARALLEL_API_KEY,
|
|
62
|
+
signal
|
|
63
|
+
});
|
|
64
|
+
return resolved && !isPlaceholderApiKey(resolved) ? resolved : null;
|
|
65
|
+
}
|
|
66
|
+
const envKey = normalizeApiKey(process.env.PARALLEL_API_KEY);
|
|
67
|
+
if (envKey && !isPlaceholderApiKey(envKey))
|
|
68
|
+
return envKey;
|
|
69
|
+
if (configKey && !isPlaceholderApiKey(configKey))
|
|
70
|
+
return configKey;
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
function hasConfiguredApiKey() {
|
|
74
|
+
const configKey = normalizeApiKey(loadConfig().parallelApiKey);
|
|
75
|
+
if (configKey?.startsWith("$") || configKey?.startsWith("!")) {
|
|
76
|
+
return hasCredentialSource({
|
|
77
|
+
provider: "Parallel",
|
|
78
|
+
configuredValue: configKey,
|
|
79
|
+
environmentValue: process.env.PARALLEL_API_KEY
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const envKey = normalizeApiKey(process.env.PARALLEL_API_KEY);
|
|
83
|
+
return envKey !== null && !isPlaceholderApiKey(envKey) || configKey !== null && !isPlaceholderApiKey(configKey);
|
|
84
|
+
}
|
|
85
|
+
async function getApiKey(signal) {
|
|
86
|
+
const key = await resolveParallelApiKey(signal);
|
|
87
|
+
if (!key) {
|
|
88
|
+
throw new Error(`Parallel API key not found. Either:
|
|
89
|
+
` + ` 1. Create ${CONFIG_PATH} with { "parallelApiKey": "your-key" }
|
|
90
|
+
` + ` 2. Set PARALLEL_API_KEY environment variable
|
|
91
|
+
` + "Get a key at https://platform.parallel.ai");
|
|
92
|
+
}
|
|
93
|
+
return key;
|
|
94
|
+
}
|
|
95
|
+
export function hasParallelApiKey() {
|
|
96
|
+
return hasConfiguredApiKey();
|
|
97
|
+
}
|
|
98
|
+
export function isParallelAvailable() {
|
|
99
|
+
return hasParallelApiKey();
|
|
100
|
+
}
|
|
101
|
+
function requestSignal(signal) {
|
|
102
|
+
const timeout = AbortSignal.timeout(SEARCH_TIMEOUT_MS);
|
|
103
|
+
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
104
|
+
}
|
|
105
|
+
function errorMessage(err) {
|
|
106
|
+
return err instanceof Error ? err.message : String(err);
|
|
107
|
+
}
|
|
108
|
+
function activityContext(url, body) {
|
|
109
|
+
if (typeof body.objective === "string" && body.objective.trim().length > 0) {
|
|
110
|
+
return { type: "api", query: body.objective };
|
|
111
|
+
}
|
|
112
|
+
const searchQueries = body.search_queries;
|
|
113
|
+
if (Array.isArray(searchQueries) && typeof searchQueries[0] === "string") {
|
|
114
|
+
return { type: "api", query: searchQueries[0] };
|
|
115
|
+
}
|
|
116
|
+
const urls = body.urls;
|
|
117
|
+
if (Array.isArray(urls) && typeof urls[0] === "string") {
|
|
118
|
+
return { type: "fetch", url: urls[0] };
|
|
119
|
+
}
|
|
120
|
+
return url.includes("/search") ? { type: "api", query: "Parallel search" } : { type: "fetch", url };
|
|
121
|
+
}
|
|
122
|
+
function recencyToAfterDate(filter) {
|
|
123
|
+
const now = new Date;
|
|
124
|
+
const offsets = { day: 1, week: 7, month: 30, year: 365 };
|
|
125
|
+
const days = offsets[filter] ?? 0;
|
|
126
|
+
return new Date(now.getTime() - days * 86400000).toISOString().slice(0, 10);
|
|
127
|
+
}
|
|
128
|
+
function normalizeDomain(value) {
|
|
129
|
+
let input = value.trim().toLowerCase();
|
|
130
|
+
if (!input)
|
|
131
|
+
return null;
|
|
132
|
+
if (input.startsWith("-"))
|
|
133
|
+
input = input.slice(1).trim();
|
|
134
|
+
if (!input)
|
|
135
|
+
return null;
|
|
136
|
+
try {
|
|
137
|
+
const parsed = input.includes("://") ? new URL(input) : new URL(`https://${input}`);
|
|
138
|
+
input = parsed.hostname;
|
|
139
|
+
} catch {
|
|
140
|
+
input = input.split("/")[0]?.split(":")[0] ?? "";
|
|
141
|
+
}
|
|
142
|
+
input = input.replace(/^\.+|\.+$/g, "");
|
|
143
|
+
return /^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}$/i.test(input) ? input : null;
|
|
144
|
+
}
|
|
145
|
+
function mapDomainFilter(domainFilter) {
|
|
146
|
+
if (!domainFilter?.length)
|
|
147
|
+
return {};
|
|
148
|
+
const include_domains = [];
|
|
149
|
+
const exclude_domains = [];
|
|
150
|
+
for (const raw of domainFilter) {
|
|
151
|
+
const domain = normalizeDomain(raw);
|
|
152
|
+
if (!domain)
|
|
153
|
+
continue;
|
|
154
|
+
const target = raw.trim().startsWith("-") ? exclude_domains : include_domains;
|
|
155
|
+
if (!target.includes(domain))
|
|
156
|
+
target.push(domain);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
...include_domains.length ? { include_domains } : {},
|
|
160
|
+
...exclude_domains.length ? { exclude_domains } : {}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function buildSearchRequestBody(query, options = {}) {
|
|
164
|
+
const numResults = Math.max(1, Math.min(Math.floor(options.numResults ?? 5), 20));
|
|
165
|
+
const sourcePolicy = {
|
|
166
|
+
...mapDomainFilter(options.domainFilter),
|
|
167
|
+
...options.recencyFilter ? { after_date: recencyToAfterDate(options.recencyFilter) } : {}
|
|
168
|
+
};
|
|
169
|
+
return {
|
|
170
|
+
objective: query,
|
|
171
|
+
search_queries: [query],
|
|
172
|
+
advanced_settings: {
|
|
173
|
+
max_results: numResults,
|
|
174
|
+
...Object.keys(sourcePolicy).length > 0 ? { source_policy: sourcePolicy } : {}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function normalizeExcerpts(value) {
|
|
179
|
+
if (!Array.isArray(value))
|
|
180
|
+
return [];
|
|
181
|
+
return value.filter((item) => typeof item === "string" && item.trim().length > 0);
|
|
182
|
+
}
|
|
183
|
+
function mapSearchResults(results) {
|
|
184
|
+
if (!Array.isArray(results))
|
|
185
|
+
return [];
|
|
186
|
+
const mapped = [];
|
|
187
|
+
for (let i = 0;i < results.length; i++) {
|
|
188
|
+
const item = results[i];
|
|
189
|
+
if (!item?.url)
|
|
190
|
+
continue;
|
|
191
|
+
const excerpts = normalizeExcerpts(item.excerpts);
|
|
192
|
+
mapped.push({
|
|
193
|
+
title: item.title || `Source ${i + 1}`,
|
|
194
|
+
url: item.url,
|
|
195
|
+
snippet: excerpts.length > 0 ? excerpts[0].replace(/\s+/g, " ").trim().slice(0, 200) : ""
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return mapped;
|
|
199
|
+
}
|
|
200
|
+
function buildAnswerFromExcerpts(results) {
|
|
201
|
+
if (!Array.isArray(results))
|
|
202
|
+
return "";
|
|
203
|
+
const parts = [];
|
|
204
|
+
for (let i = 0;i < results.length; i++) {
|
|
205
|
+
const item = results[i];
|
|
206
|
+
if (!item?.url)
|
|
207
|
+
continue;
|
|
208
|
+
const excerpts = normalizeExcerpts(item.excerpts);
|
|
209
|
+
if (excerpts.length === 0)
|
|
210
|
+
continue;
|
|
211
|
+
parts.push(`${excerpts.join(" ")}
|
|
212
|
+
Source: ${item.title || `Source ${i + 1}`} (${item.url})`);
|
|
213
|
+
}
|
|
214
|
+
return parts.join(`
|
|
215
|
+
|
|
216
|
+
`);
|
|
217
|
+
}
|
|
218
|
+
function mapInlineContent(results) {
|
|
219
|
+
if (!Array.isArray(results))
|
|
220
|
+
return [];
|
|
221
|
+
return results.flatMap((result) => {
|
|
222
|
+
if (!result?.url)
|
|
223
|
+
return [];
|
|
224
|
+
const excerpts = normalizeExcerpts(result.excerpts);
|
|
225
|
+
if (excerpts.length === 0)
|
|
226
|
+
return [];
|
|
227
|
+
return [{ url: result.url, title: result.title || "", content: excerpts.join(`
|
|
228
|
+
|
|
229
|
+
`), error: null }];
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function resolveExtractContent(result) {
|
|
233
|
+
const fullContent = typeof result.full_content === "string" ? result.full_content.trim() : "";
|
|
234
|
+
return fullContent.length > 0 ? fullContent : normalizeExcerpts(result.excerpts).join(`
|
|
235
|
+
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
function mapExtractResult(result) {
|
|
239
|
+
if (!result?.url)
|
|
240
|
+
return null;
|
|
241
|
+
const content = resolveExtractContent(result);
|
|
242
|
+
if (content.length < MIN_USEFUL_CONTENT)
|
|
243
|
+
return null;
|
|
244
|
+
return {
|
|
245
|
+
url: result.url,
|
|
246
|
+
title: typeof result.title === "string" ? result.title.trim() : "",
|
|
247
|
+
content,
|
|
248
|
+
error: null
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function buildExtractRequestBody(url, options = {}, fullContent = false) {
|
|
252
|
+
const body = { urls: [url] };
|
|
253
|
+
const prompt = options.prompt?.trim();
|
|
254
|
+
if (prompt)
|
|
255
|
+
body.objective = prompt;
|
|
256
|
+
if (fullContent)
|
|
257
|
+
body.advanced_settings = { full_content: true };
|
|
258
|
+
return body;
|
|
259
|
+
}
|
|
260
|
+
function findExtractResult(results, url) {
|
|
261
|
+
if (!Array.isArray(results))
|
|
262
|
+
return;
|
|
263
|
+
return results.find((item) => item?.url === url) ?? results[0];
|
|
264
|
+
}
|
|
265
|
+
function hasExtractUrlError(errors, url) {
|
|
266
|
+
if (!Array.isArray(errors))
|
|
267
|
+
return false;
|
|
268
|
+
return errors.some((entry) => {
|
|
269
|
+
if (typeof entry === "string")
|
|
270
|
+
return entry === url;
|
|
271
|
+
return typeof entry === "object" && entry !== null && entry.url === url;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
async function fetchAndMapExtractResult(url, body, signal) {
|
|
275
|
+
const data = await parallelFetch(PARALLEL_EXTRACT_URL, body, signal);
|
|
276
|
+
if (hasExtractUrlError(data.errors, url))
|
|
277
|
+
return { mapped: null, result: undefined };
|
|
278
|
+
const result = findExtractResult(data.results, url);
|
|
279
|
+
return { mapped: mapExtractResult(result), result };
|
|
280
|
+
}
|
|
281
|
+
export async function searchWithParallel(query, options = {}) {
|
|
282
|
+
const data = await parallelFetch(PARALLEL_SEARCH_URL, buildSearchRequestBody(query, options), options.signal);
|
|
283
|
+
const results = data.results;
|
|
284
|
+
const response = {
|
|
285
|
+
answer: buildAnswerFromExcerpts(results),
|
|
286
|
+
results: mapSearchResults(results)
|
|
287
|
+
};
|
|
288
|
+
if (options.includeContent) {
|
|
289
|
+
const inlineContent = mapInlineContent(results);
|
|
290
|
+
if (inlineContent.length > 0)
|
|
291
|
+
response.inlineContent = inlineContent;
|
|
292
|
+
}
|
|
293
|
+
return response;
|
|
294
|
+
}
|
|
295
|
+
export async function extractWithParallel(url, signal, options = {}) {
|
|
296
|
+
const initial = await fetchAndMapExtractResult(url, buildExtractRequestBody(url, options), signal);
|
|
297
|
+
if (initial.mapped)
|
|
298
|
+
return initial.mapped;
|
|
299
|
+
if (!initial.result || resolveExtractContent(initial.result).length >= MIN_USEFUL_CONTENT)
|
|
300
|
+
return null;
|
|
301
|
+
const retry = await fetchAndMapExtractResult(url, buildExtractRequestBody(url, options, true), signal);
|
|
302
|
+
return retry.mapped;
|
|
303
|
+
}
|
|
304
|
+
async function parallelFetch(url, body, signal) {
|
|
305
|
+
const apiKey = await getApiKey(signal);
|
|
306
|
+
const activityId = activityMonitor.logStart(activityContext(url, body));
|
|
307
|
+
let response;
|
|
308
|
+
try {
|
|
309
|
+
response = await fetch(url, {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: {
|
|
312
|
+
"x-api-key": apiKey,
|
|
313
|
+
"Content-Type": "application/json"
|
|
314
|
+
},
|
|
315
|
+
body: JSON.stringify(body),
|
|
316
|
+
signal: requestSignal(signal)
|
|
317
|
+
});
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const message = errorMessage(err);
|
|
320
|
+
const redactedMessage = redactCredential(message, apiKey);
|
|
321
|
+
if (redactedMessage.toLowerCase().includes("abort"))
|
|
322
|
+
activityMonitor.logComplete(activityId, 0);
|
|
323
|
+
else
|
|
324
|
+
activityMonitor.logError(activityId, redactedMessage);
|
|
325
|
+
if (redactedMessage === message)
|
|
326
|
+
throw err;
|
|
327
|
+
const redactedError = new Error(redactedMessage);
|
|
328
|
+
if (err instanceof Error)
|
|
329
|
+
redactedError.name = err.name;
|
|
330
|
+
throw redactedError;
|
|
331
|
+
}
|
|
332
|
+
if (!response.ok) {
|
|
333
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
334
|
+
const errorText = redactCredential(await response.text(), apiKey);
|
|
335
|
+
throw new Error(`Parallel API error ${response.status}: ${errorText.slice(0, 300)}`);
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
const data = await response.json();
|
|
339
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
340
|
+
return data;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
activityMonitor.logComplete(activityId, response.status);
|
|
343
|
+
throw new Error(`Parallel API returned invalid JSON: ${errorMessage(err)}`);
|
|
344
|
+
}
|
|
345
|
+
}
|