dsh-free-search 0.4.38 → 0.4.39
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/lib/client.js +23 -4
- package/lib/index.js +46 -11
- package/package.json +2 -2
package/lib/client.js
CHANGED
|
@@ -96,6 +96,7 @@ window.__ModuleLoader__.load({
|
|
|
96
96
|
getKey: "获取 API Key →",
|
|
97
97
|
engineHint: "Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
|
|
98
98
|
apiKeys: "API 密钥(可选)",
|
|
99
|
+
anysearchPh: (c) => c ? "AnySearch API 密钥(已配置)" : "AnySearch API 密钥(可选,不填免费匿名,填了提额)",
|
|
99
100
|
exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
|
|
100
101
|
tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
|
|
101
102
|
keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
|
|
@@ -108,7 +109,7 @@ window.__ModuleLoader__.load({
|
|
|
108
109
|
keyStorage: "Key 存储位置",
|
|
109
110
|
keyStorageCred: "凭据中心(推荐)",
|
|
110
111
|
keyStorageSettings: "设置页(兼容)",
|
|
111
|
-
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
112
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
112
113
|
keyStorageSettingsHint: "保存后写入当前 profile 的插件条目 config(cordis.patch.yml,优先级低于凭据中心)。",
|
|
113
114
|
platformSearch: "平台搜索(platform_search 工具)",
|
|
114
115
|
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
@@ -164,6 +165,7 @@ window.__ModuleLoader__.load({
|
|
|
164
165
|
getKey: "Get API Key →",
|
|
165
166
|
engineHint: "Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
|
|
166
167
|
apiKeys: "API keys (optional)",
|
|
168
|
+
anysearchPh: (c) => c ? "AnySearch API key (configured)" : "AnySearch API key (optional, free anonymous without; key raises quota)",
|
|
167
169
|
exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
|
|
168
170
|
tavilyPh: (c) => c ? "Tavily API key (configured)" : "Tavily API key (optional, free without)",
|
|
169
171
|
keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
|
|
@@ -176,7 +178,7 @@ window.__ModuleLoader__.load({
|
|
|
176
178
|
keyStorage: "Key storage",
|
|
177
179
|
keyStorageCred: "Credential center (recommended)",
|
|
178
180
|
keyStorageSettings: "Settings page (legacy)",
|
|
179
|
-
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
181
|
+
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
180
182
|
keyStorageSettingsHint: "Saved to the active profile plugin entry config (cordis.patch.yml; lower priority than the credential center).",
|
|
181
183
|
platformSearch: "Platform search (platform_search tool)",
|
|
182
184
|
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
@@ -227,7 +229,7 @@ window.__ModuleLoader__.load({
|
|
|
227
229
|
};
|
|
228
230
|
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
229
231
|
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
230
|
-
const PLUGIN_VERSION = "0.4.
|
|
232
|
+
const PLUGIN_VERSION = "0.4.39";
|
|
231
233
|
const ENGINES = [
|
|
232
234
|
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
233
235
|
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
@@ -344,6 +346,7 @@ window.__ModuleLoader__.load({
|
|
|
344
346
|
const [provider, setProvider] = react.useState("bing");
|
|
345
347
|
const [safeSearch, setSafeSearch] = react.useState("off");
|
|
346
348
|
const [bingMarket, setBingMarket] = react.useState("zh-CN");
|
|
349
|
+
const [anysearchKey, setAnysearchKey] = react.useState("");
|
|
347
350
|
const [exaKey, setExaKey] = react.useState("");
|
|
348
351
|
const [tavilyKey, setTavilyKey] = react.useState("");
|
|
349
352
|
const [keenableKey, setKeenableKey] = react.useState("");
|
|
@@ -382,6 +385,7 @@ window.__ModuleLoader__.load({
|
|
|
382
385
|
setSafeSearch(v.safeSearch === "strict" || v.safeSearch === "moderate" ? v.safeSearch : "off");
|
|
383
386
|
setBingMarket(v.bingMarket === undefined ? "zh-CN" : v.bingMarket);
|
|
384
387
|
setLang(v.lang === "en" ? "en" : "zh");
|
|
388
|
+
setAnysearchKey(v.anysearchApiKey ?? "");
|
|
385
389
|
setExaKey(v.exaApiKey ?? "");
|
|
386
390
|
setTavilyKey(v.tavilyApiKey ?? "");
|
|
387
391
|
setKeenableKey(v.keenableApiKey ?? "");
|
|
@@ -397,6 +401,7 @@ window.__ModuleLoader__.load({
|
|
|
397
401
|
for (const secret of view.secrets ?? []) {
|
|
398
402
|
if (secret.set) {
|
|
399
403
|
const path = secret.path.join(".");
|
|
404
|
+
if (path === "anysearchApiKey") configured.anysearch = true;
|
|
400
405
|
if (path === "exaApiKey") configured.exa = true;
|
|
401
406
|
if (path === "tavilyApiKey") configured.tavily = true;
|
|
402
407
|
if (path === "keenableApiKey") configured.keenable = true;
|
|
@@ -416,7 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
416
421
|
const cred = await bridgeCredentialsStatus();
|
|
417
422
|
if (cred.ok) {
|
|
418
423
|
const cc = {};
|
|
419
|
-
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase" };
|
|
424
|
+
const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase" };
|
|
420
425
|
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
421
426
|
if (map[k]) cc[map[k]] = v;
|
|
422
427
|
}
|
|
@@ -453,6 +458,7 @@ window.__ModuleLoader__.load({
|
|
|
453
458
|
try {
|
|
454
459
|
// key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
|
|
455
460
|
const keyFields = [
|
|
461
|
+
["anysearchApiKey", anysearchKey],
|
|
456
462
|
["exaApiKey", exaKey],
|
|
457
463
|
["tavilyApiKey", tavilyKey],
|
|
458
464
|
["keenableApiKey", keenableKey],
|
|
@@ -746,6 +752,19 @@ react_jsx_runtime.jsx("div", {
|
|
|
746
752
|
className: "dshfs-label",
|
|
747
753
|
children: t.apiKeys,
|
|
748
754
|
}),
|
|
755
|
+
react_jsx_runtime.jsx("input", {
|
|
756
|
+
className: "dshfs-input",
|
|
757
|
+
type: "password",
|
|
758
|
+
placeholder: t.anysearchPh(keysConfigured.anysearch),
|
|
759
|
+
value: anysearchKey,
|
|
760
|
+
disabled: !ready || saving,
|
|
761
|
+
onChange: (e) => {
|
|
762
|
+
setAnysearchKey(e.target.value);
|
|
763
|
+
setDirty(true);
|
|
764
|
+
setFailed(false);
|
|
765
|
+
setSaveError("");
|
|
766
|
+
},
|
|
767
|
+
}),
|
|
749
768
|
react_jsx_runtime.jsx("input", {
|
|
750
769
|
className: "dshfs-input",
|
|
751
770
|
type: "password",
|
package/lib/index.js
CHANGED
|
@@ -55,7 +55,7 @@ const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
|
|
|
55
55
|
const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "deepseek-official"];
|
|
56
56
|
|
|
57
57
|
// 当前插件版本(发布时与 package.json 同步)
|
|
58
|
-
const PLUGIN_VERSION = "0.4.
|
|
58
|
+
const PLUGIN_VERSION = "0.4.39";
|
|
59
59
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
60
60
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
61
61
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -479,20 +479,39 @@ async function searchSearxng(query, maxResults, options, signal) {
|
|
|
479
479
|
const ANYSEARCH_URL = "https://api.anysearch.com/v1/search";
|
|
480
480
|
const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
|
|
481
481
|
|
|
482
|
-
// AnySearch: 免费匿名额度(无 key
|
|
483
|
-
|
|
482
|
+
// AnySearch: 免费匿名额度(无 key),可选 Bearer key 提额;结构化 JSON 结果
|
|
483
|
+
// 401/403:本进程忽略该 key 值(不删除存储),改回匿名免费;不自动清空用户配置
|
|
484
|
+
let ignoredAnysearchKey = "";
|
|
485
|
+
const ANYSEARCH_KEY_INVALID_NOTE = "AnySearch key 无效,本次已忽略该 key,改回免费匿名";
|
|
486
|
+
|
|
487
|
+
async function searchAnysearch(query, maxResults, signal, apiKey) {
|
|
488
|
+
const trimmedKey = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
489
|
+
// 本进程已判定无效的 key 不再发送;若配置里仍有该值,结果仍带忽略提示
|
|
490
|
+
const keyIgnoredSticky = Boolean(trimmedKey) && trimmedKey === ignoredAnysearchKey;
|
|
491
|
+
const sendKey = trimmedKey && !keyIgnoredSticky ? trimmedKey : "";
|
|
484
492
|
const controller = new AbortController();
|
|
485
493
|
const timer = setTimeout(() => controller.abort(), 12000);
|
|
486
494
|
const onAbort = () => controller.abort();
|
|
487
495
|
signal?.addEventListener("abort", onAbort);
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
496
|
+
const doFetch = async (key) => {
|
|
497
|
+
const headers = { "content-type": "application/json" };
|
|
498
|
+
if (key) headers.authorization = `Bearer ${key}`;
|
|
499
|
+
return await fetch(ANYSEARCH_URL, {
|
|
491
500
|
method: "POST",
|
|
492
|
-
headers
|
|
501
|
+
headers,
|
|
493
502
|
body: JSON.stringify({ query, max_results: maxResults ?? 5 }),
|
|
494
503
|
signal: controller.signal,
|
|
495
504
|
});
|
|
505
|
+
};
|
|
506
|
+
let response;
|
|
507
|
+
let keyRejected = false;
|
|
508
|
+
try {
|
|
509
|
+
response = await doFetch(sendKey);
|
|
510
|
+
if ((response.status === 401 || response.status === 403) && sendKey) {
|
|
511
|
+
ignoredAnysearchKey = sendKey;
|
|
512
|
+
keyRejected = true;
|
|
513
|
+
response = await doFetch("");
|
|
514
|
+
}
|
|
496
515
|
} catch (error) {
|
|
497
516
|
if (signal?.aborted) throw error;
|
|
498
517
|
throw new Error(`AnySearch request failed: ${error?.message ?? String(error)}`);
|
|
@@ -500,6 +519,9 @@ async function searchAnysearch(query, maxResults, signal) {
|
|
|
500
519
|
clearTimeout(timer);
|
|
501
520
|
signal?.removeEventListener("abort", onAbort);
|
|
502
521
|
}
|
|
522
|
+
if (response.status === 401 || response.status === 403) {
|
|
523
|
+
throw new Error(keyRejected ? `${ANYSEARCH_KEY_INVALID_NOTE} (HTTP ${response.status})` : `AnySearch API error (HTTP ${response.status})`);
|
|
524
|
+
}
|
|
503
525
|
if (!response.ok) throw new Error(`AnySearch API error (HTTP ${response.status})`);
|
|
504
526
|
const data = await response.json();
|
|
505
527
|
if (data.code !== 0) throw new Error(`AnySearch API error: ${data.message ?? data.code}`);
|
|
@@ -513,6 +535,7 @@ async function searchAnysearch(query, maxResults, signal) {
|
|
|
513
535
|
...(r.snippet ? { snippet: String(r.snippet).slice(0, 300) } : {}),
|
|
514
536
|
})),
|
|
515
537
|
truncated: false,
|
|
538
|
+
...(keyRejected || keyIgnoredSticky ? { content: ANYSEARCH_KEY_INVALID_NOTE } : {}),
|
|
516
539
|
};
|
|
517
540
|
}
|
|
518
541
|
|
|
@@ -1611,6 +1634,10 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
|
|
|
1611
1634
|
const expectedRevision = typeof body.expectedRevision === "number" ? body.expectedRevision : undefined;
|
|
1612
1635
|
try {
|
|
1613
1636
|
await settings.mutate(ns, body.ops, expectedRevision);
|
|
1637
|
+
// 设置页保存了 AnySearch key:允许下次再试(不删除存储,仅解除本进程忽略)
|
|
1638
|
+
if (body.ops.some((op) => op && op.op === "set" && Array.isArray(op.path) && op.path[0] === "anysearchApiKey")) {
|
|
1639
|
+
ignoredAnysearchKey = "";
|
|
1640
|
+
}
|
|
1614
1641
|
} catch (error) {
|
|
1615
1642
|
if (error instanceof SettingsConflictError) {
|
|
1616
1643
|
return { ok: false, code: "settings-conflict", message: error.message };
|
|
@@ -1651,6 +1678,8 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
|
|
|
1651
1678
|
}
|
|
1652
1679
|
try {
|
|
1653
1680
|
await credentials.set(ref, value.trim());
|
|
1681
|
+
// 用户写入新 AnySearch key:允许下次请求再试(仅忽略同值坏 key,不删除存储)
|
|
1682
|
+
if (key === "anysearchApiKey") ignoredAnysearchKey = "";
|
|
1654
1683
|
return { ok: true, value: { ref, set: true } };
|
|
1655
1684
|
} catch (error) {
|
|
1656
1685
|
return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
|
|
@@ -1778,6 +1807,7 @@ const inject = ["web"];
|
|
|
1778
1807
|
|
|
1779
1808
|
// 引擎 key 与凭据中心 ref 的映射(白名单:只允许这些 ref 被 UI 读写凭据中心)
|
|
1780
1809
|
const KEY_REF_MAP = {
|
|
1810
|
+
anysearchApiKey: "ANYSEARCH_API_KEY",
|
|
1781
1811
|
exaApiKey: "EXA_API_KEY",
|
|
1782
1812
|
tavilyApiKey: "TAVILY_API_KEY",
|
|
1783
1813
|
keenableApiKey: "KEENABLE_API_KEY",
|
|
@@ -1803,6 +1833,7 @@ const Config = z.object({
|
|
|
1803
1833
|
safeSearch: z.string().default("off").volatile(),
|
|
1804
1834
|
searxngInstances: z.array(z.string()).volatile(),
|
|
1805
1835
|
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]).volatile(),
|
|
1836
|
+
anysearchApiKey: z.string().role("secret").volatile(),
|
|
1806
1837
|
exaApiKey: z.string().role("secret").volatile(),
|
|
1807
1838
|
tavilyApiKey: z.string().role("secret").volatile(),
|
|
1808
1839
|
keenableApiKey: z.string().role("secret").volatile(),
|
|
@@ -1945,7 +1976,8 @@ function apply(ctx, config) {
|
|
|
1945
1976
|
} else if (engine === "searxng") {
|
|
1946
1977
|
result = await searchSearxng(request.query, request.maxResults, { ...cfg, timeRange }, effSignal);
|
|
1947
1978
|
} else if (engine === "anysearch") {
|
|
1948
|
-
|
|
1979
|
+
const key = await resolveApiKey("ANYSEARCH_API_KEY", "anysearchApiKey");
|
|
1980
|
+
result = await searchAnysearch(request.query, request.maxResults, effSignal, key);
|
|
1949
1981
|
} else if (engine === "exa") {
|
|
1950
1982
|
// exa:有 key 走 REST,无 key 走 keyless MCP(免费)
|
|
1951
1983
|
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
@@ -2105,8 +2137,10 @@ function apply(ctx, config) {
|
|
|
2105
2137
|
return await searchBing(q, 2, cfg);
|
|
2106
2138
|
case "searxng":
|
|
2107
2139
|
return await searchSearxng(q, 2, { ...cfg, timeRange: tr });
|
|
2108
|
-
case "anysearch":
|
|
2109
|
-
|
|
2140
|
+
case "anysearch": {
|
|
2141
|
+
const key = await resolveApiKey("ANYSEARCH_API_KEY", "anysearchApiKey");
|
|
2142
|
+
return await searchAnysearch(q, 2, undefined, key);
|
|
2143
|
+
}
|
|
2110
2144
|
case "exa": {
|
|
2111
2145
|
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
2112
2146
|
if (key) return await searchExa(q, 2, key, tr);
|
|
@@ -2163,6 +2197,7 @@ function apply(ctx, config) {
|
|
|
2163
2197
|
s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
|
|
2164
2198
|
),
|
|
2165
2199
|
truncated: result.truncated ?? false,
|
|
2200
|
+
...(typeof result.content === "string" && result.content ? { content: result.content } : {}),
|
|
2166
2201
|
};
|
|
2167
2202
|
} catch (error) {
|
|
2168
2203
|
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
@@ -2459,7 +2494,7 @@ function apply(ctx, config) {
|
|
|
2459
2494
|
"- ddg-lite (DuckDuckGo Lite) - FREE, no key (may be rate-limited)",
|
|
2460
2495
|
"- bing (Bing) - FREE, no key (most stable)",
|
|
2461
2496
|
"- searxng (meta-search, multi-instance) - FREE, no key",
|
|
2462
|
-
"- anysearch (AI search) - FREE
|
|
2497
|
+
"- anysearch (AI search) - FREE no key, or optional ANYSEARCH_API_KEY for higher limits",
|
|
2463
2498
|
"- exa - FREE keyless (MCP) or EXA_API_KEY for higher limits",
|
|
2464
2499
|
"- tavily - FREE keyless or TAVILY_API_KEY for higher limits",
|
|
2465
2500
|
"- keenable - FREE keyless (MCP) or KEENABLE_API_KEY for higher limits",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Free web search for DeepSeek Harness: 13 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek with key) + time filtering + platform search + web_fetch, with web settings UI.",
|
|
3
|
+
"version": "0.4.39",
|
|
4
|
+
"description": "Free web search for DeepSeek Harness: 13 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Perplexity/SerpBase/DeepSeek with key) + optional AnySearch key for higher quota + time filtering + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|