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 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.38";
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.38";
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),结构化 JSON 结果
483
- async function searchAnysearch(query, maxResults, signal) {
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
- let response;
489
- try {
490
- response = await fetch(ANYSEARCH_URL, {
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: { "content-type": "application/json" },
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
- result = await searchAnysearch(request.query, request.maxResults, effSignal);
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
- return await searchAnysearch(q, 2);
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, no key",
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.38",
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": {