dsh-free-search 0.8.2 → 0.8.3

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
@@ -73,7 +73,12 @@ window.__ModuleLoader__.load({
73
73
  ".dshfs-priorityHandle:active{cursor:grabbing}",
74
74
  ".dshfs-priorityKey{flex:1;min-width:110px;max-width:200px;height:26px;padding:0 8px;font-size:12px;border:1px solid rgba(0,0,0,.12);border-radius:8px;background:transparent;color:inherit}",
75
75
  "@media (prefers-color-scheme: dark){.dshfs-priorityKey{border-color:rgba(255,255,255,.14)}}",
76
- ".dshfs-priorityKey::placeholder{color:#8e8e93}",
76
+ ".dshfs-priorityKey::placeholder{color:#8e8e93}",
77
+ ".dshfs-keyStatus{flex:none;width:8px;height:8px;border-radius:50%;display:inline-block}",
78
+ ".dshfs-keyStatus-ok{background:#34c759}",
79
+ ".dshfs-keyStatus-miss{background:#ff9500}",
80
+ ".dshfs-keyStatus-opt{background:#c7c7cc}",
81
+ "@media (prefers-color-scheme: dark){.dshfs-keyStatus-opt{background:#48484a}}",
77
82
  ".dshfs-priorityRow[draggable='true']{cursor:grab}",
78
83
  ".dshfs-priorityRowDragging{opacity:.45;border-style:dashed}",
79
84
  "@media (prefers-color-scheme: dark){.dshfs-platform input{accent-color:#0a84ff}}",
@@ -162,8 +167,13 @@ window.__ModuleLoader__.load({
162
167
  geminiPh: (c) => c ? "Gemini 密钥(已配置)" : "Gemini 密钥(GEMINI_API_KEY / GOOGLE_API_KEY,按次计费,仅显式选用)",
163
168
  claudePh: (c) => c ? "Claude 密钥(已配置)" : "Anthropic 密钥(ANTHROPIC_API_KEY,按次计费,仅显式选用)",
164
169
  zhihuPh: (c) => c ? "知乎密钥(已配置)" : "知乎密钥(ZHIHU_API_KEY,知乎全网/站内共用)",
170
+ tinyfishPh: (c) => c ? "TinyFish 密钥(已配置)" : "TinyFish 密钥(TINYFISH_API_KEY,免费 12000 次/天)",
171
+ jinaPh: (c) => c ? "Jina 密钥(已配置)" : "Jina 密钥(JINA_API_KEY)",
165
172
  billedLabel: "按次计费",
166
173
  billedHint: "模型内置搜索(OpenAI/Gemini/Claude):每次调用按量计费,仅在显式选中时运行,不参与自动回退。",
174
+ keyStatusConfigured: "密钥已配置",
175
+ keyStatusMissing: "缺少必需密钥",
176
+ keyStatusOptional: "密钥可选(未配置也可用)",
167
177
  keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
168
178
  keyStorage: "Key 存储位置",
169
179
  keyStorageCred: "凭据中心(推荐)",
@@ -263,8 +273,13 @@ window.__ModuleLoader__.load({
263
273
  geminiPh: (c) => c ? "Gemini key (configured)" : "Gemini key (GEMINI_API_KEY / GOOGLE_API_KEY, billed per search, explicit use only)",
264
274
  claudePh: (c) => c ? "Claude key (configured)" : "Anthropic key (ANTHROPIC_API_KEY, billed per search, explicit use only)",
265
275
  zhihuPh: (c) => c ? "Zhihu key (configured)" : "Zhihu key (ZHIHU_API_KEY, shared by Zhihu web-wide/site)",
276
+ tinyfishPh: (c) => c ? "TinyFish key (configured)" : "TinyFish key (TINYFISH_API_KEY, 12000 free/day)",
277
+ jinaPh: (c) => c ? "Jina key (configured)" : "Jina key (JINA_API_KEY)",
266
278
  billedLabel: "Per-search billing",
267
279
  billedHint: "Model-based search (OpenAI/Gemini/Claude): billed per call, runs only when explicitly selected, never in automatic fallback.",
280
+ keyStatusConfigured: "Key configured",
281
+ keyStatusMissing: "Required key missing",
282
+ keyStatusOptional: "Key optional (works without)",
268
283
  keysHint: "Key resolution: .credentials.yaml credential center > here > environment variables. Recommended: store keys in the credential center (same as official LLM providers, one place for all).",
269
284
  keyStorage: "Key storage",
270
285
  keyStorageCred: "Credential center (recommended)",
@@ -340,7 +355,7 @@ window.__ModuleLoader__.load({
340
355
  };
341
356
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
342
357
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
343
- const PLUGIN_VERSION = "0.8.2";
358
+ const PLUGIN_VERSION = "0.8.3";
344
359
  const ENGINES = [
345
360
  { id: "auto", label: "Auto · 智能路由", badge: "AUTO", link: null },
346
361
  { id: "multi", label: "Multi Search · 多引擎", badge: "MULTI", link: null },
@@ -363,6 +378,8 @@ window.__ModuleLoader__.load({
363
378
  { id: "kimi", label: "Kimi · Moonshot", badge: "API KEY", link: "https://platform.moonshot.cn" },
364
379
  { id: "aliyun", label: "Aliyun · 百炼", badge: "API KEY", link: "https://bailian.console.aliyun.com" },
365
380
  { id: "doubao", label: "Doubao · 豆包搜索", badge: "API KEY", link: "https://console.volcengine.com/search-infinity/web-search" },
381
+ { id: "tinyfish", label: "TinyFish", badge: "API KEY", link: "https://agent.tinyfish.ai/api-keys" },
382
+ { id: "jina", label: "Jina · 搜索", badge: "API KEY", link: "https://jina.ai/" },
366
383
  { id: "zhihu_global", label: "Zhihu · 知乎全网", badge: "API KEY", link: "https://developer.zhihu.com" },
367
384
  { id: "zhihu_site", label: "Zhihu · 知乎站内", badge: "API KEY", link: "https://developer.zhihu.com" },
368
385
  { id: "openai", label: "OpenAI · 内置搜索", badge: "API KEY", link: "https://platform.openai.com/api-keys" },
@@ -373,7 +390,7 @@ window.__ModuleLoader__.load({
373
390
  // 与 lib/index.js 的 EXPLICIT_ONLY_ENGINES 同源:按次计费、只在显式选中时运行,因此不提供入池候选。
374
391
  const EXPLICIT_ONLY_ENGINES = ["openai", "gemini", "claude"];
375
392
  const MODEL_ENGINES = ENGINES.filter((engine) => EXPLICIT_ONLY_ENGINES.includes(engine.id));
376
- const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
393
+ const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
377
394
  const normalizeFallbackOrder = (value) => {
378
395
  const configured = Array.isArray(value) ? value.filter((id) => ENGINE_IDS.includes(id)) : [];
379
396
  return [...new Set([...configured, ...DEFAULT_FALLBACK_ORDER, ...ENGINE_IDS.filter((id) => !EXPLICIT_ONLY_ENGINES.includes(id))])].filter((id) => !EXPLICIT_ONLY_ENGINES.includes(id));
@@ -524,6 +541,8 @@ window.__ModuleLoader__.load({
524
541
  const [geminiKey, setGeminiKey] = react.useState("");
525
542
  const [claudeKey, setClaudeKey] = react.useState("");
526
543
  const [zhihuKey, setZhihuKey] = react.useState("");
544
+ const [tinyfishKey, setTinyfishKey] = react.useState("");
545
+ const [jinaKey, setJinaKey] = react.useState("");
527
546
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"]);
528
547
  const [cacheTtl, setCacheTtl] = react.useState(5);
529
548
  const [keysConfigured, setKeysConfigured] = react.useState({});
@@ -559,12 +578,34 @@ window.__ModuleLoader__.load({
559
578
  kimi: [kimiKey, setKimiKey],
560
579
  aliyun: [aliyunKey, setAliyunKey],
561
580
  doubao: [doubaoKey, setDoubaoKey],
581
+ tinyfish: [tinyfishKey, setTinyfishKey],
582
+ jina: [jinaKey, setJinaKey],
562
583
  zhihu_global: [zhihuKey, setZhihuKey],
563
584
  zhihu_site: [zhihuKey, setZhihuKey],
564
585
  openai: [openaiKey, setOpenaiKey],
565
586
  gemini: [geminiKey, setGeminiKey],
566
587
  claude: [claudeKey, setClaudeKey],
567
588
  };
589
+ // #68 密钥状态可视化:引擎 id → 配置中心键名(少数引擎不同名),三态:已配置/可选/缺失
590
+ const CONFIGURED_KEY_ID = { "deepseek-official": "deepseek", you: "youcom", zhihu_global: "zhihu", zhihu_site: "zhihu" };
591
+ const NO_KEY_ENGINES = ["ddg", "ddg-lite", "bing", "searxng"];
592
+ const OPTIONAL_KEY_ENGINES = ["anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel"];
593
+ const hasConfiguredKey = (id) => {
594
+ const k = CONFIGURED_KEY_ID[id] ?? id;
595
+ return Boolean(keysConfigured[k] || credConfigured[k]);
596
+ };
597
+ const renderKeyStatus = (id) => {
598
+ if (NO_KEY_ENGINES.includes(id)) return null;
599
+ const configured = hasConfiguredKey(id);
600
+ const optional = OPTIONAL_KEY_ENGINES.includes(id);
601
+ const state = configured ? "ok" : optional ? "opt" : "miss";
602
+ const title = configured ? t.keyStatusConfigured : optional ? t.keyStatusOptional : t.keyStatusMissing;
603
+ return react_jsx_runtime.jsx("span", {
604
+ className: "dshfs-keyStatus dshfs-keyStatus-" + state,
605
+ title,
606
+ "aria-label": title,
607
+ });
608
+ };
568
609
 
569
610
  const load = react.useCallback(async () => {
570
611
  try {
@@ -605,6 +646,8 @@ window.__ModuleLoader__.load({
605
646
  setGeminiKey(v.geminiApiKey ?? "");
606
647
  setClaudeKey(v.claudeApiKey ?? "");
607
648
  setZhihuKey(v.zhihuApiKey ?? "");
649
+ setTinyfishKey(v.tinyfishApiKey ?? "");
650
+ setJinaKey(v.jinaApiKey ?? "");
608
651
  setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm", "youtube", "vimeo"]);
609
652
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
610
653
  // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
@@ -631,6 +674,8 @@ window.__ModuleLoader__.load({
631
674
  if (path === "geminiApiKey") configured.gemini = true;
632
675
  if (path === "claudeApiKey") configured.claude = true;
633
676
  if (path === "zhihuApiKey") configured.zhihu = true;
677
+ if (path === "tinyfishApiKey") configured.tinyfish = true;
678
+ if (path === "jinaApiKey") configured.jina = true;
634
679
  }
635
680
  }
636
681
  setKeysConfigured(configured);
@@ -642,7 +687,7 @@ window.__ModuleLoader__.load({
642
687
  const cred = await bridgeCredentialsStatus();
643
688
  if (cred.ok) {
644
689
  const cc = {};
645
- const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", serplyApiKey: "serply", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun", doubaoApiKey: "doubao", openaiApiKey: "openai", geminiApiKey: "gemini", claudeApiKey: "claude", zhihuApiKey: "zhihu" };
690
+ const map = { anysearchApiKey: "anysearch", exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", firecrawlApiKey: "firecrawl", parallelApiKey: "parallel", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek", serpbaseApiKey: "serpbase", serplyApiKey: "serply", youcomApiKey: "youcom", baiduApiKey: "baidu", kimiApiKey: "kimi", aliyunApiKey: "aliyun", doubaoApiKey: "doubao", openaiApiKey: "openai", geminiApiKey: "gemini", claudeApiKey: "claude", zhihuApiKey: "zhihu", tinyfishApiKey: "tinyfish", jinaApiKey: "jina" };
646
691
  for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
647
692
  if (map[k]) cc[map[k]] = v;
648
693
  }
@@ -698,6 +743,8 @@ window.__ModuleLoader__.load({
698
743
  ["geminiApiKey", geminiKey],
699
744
  ["claudeApiKey", claudeKey],
700
745
  ["zhihuApiKey", zhihuKey],
746
+ ["tinyfishApiKey", tinyfishKey],
747
+ ["jinaApiKey", jinaKey],
701
748
  ];
702
749
  if (keyStorage === "credentials") {
703
750
  for (const [field, value] of keyFields) {
@@ -1159,6 +1206,7 @@ react_jsx_runtime.jsx("div", {
1159
1206
  className: "dshfs-priorityName",
1160
1207
  children: `${engine.label} (${engine.badge})${isDisabled ? " · " + t.priorityDisabled : ""}`,
1161
1208
  }),
1209
+ renderKeyStatus(engineId),
1162
1210
  (() => {
1163
1211
  const k = KEY_INPUTS[engineId];
1164
1212
  if (!k) return null;
@@ -1228,6 +1276,7 @@ react_jsx_runtime.jsx("div", {
1228
1276
  className: "dshfs-priorityName",
1229
1277
  children: `${engine.label} (${engine.badge})`,
1230
1278
  }),
1279
+ renderKeyStatus(engine.id),
1231
1280
  k
1232
1281
  ? react_jsx_runtime.jsx("input", {
1233
1282
  className: "dshfs-priorityKey",
package/lib/index.js CHANGED
@@ -44,6 +44,8 @@ const KEENABLE_MCP_URL = "https://api.keenable.ai/mcp";
44
44
  const SERPBASE_URL = "https://api.serpbase.dev/google/search";
45
45
  const SERPLY_URL = "https://api.serply.io/v1/search";
46
46
  const YOUCOM_URL = "https://ydc-index.io/v1/search";
47
+ const TINYFISH_URL = "https://api.search.tinyfish.ai";
48
+ const JINA_URL = "https://s.jina.ai/";
47
49
  const DOUBAO_SEARCH_URL = "https://open.feedcoopapi.com/search_api/web_search";
48
50
  const ZHIHU_MCP_URL = "https://developer.zhihu.com/api/mcp/v1";
49
51
  const USER_AGENT =
@@ -84,7 +86,7 @@ const FREE_SEARCH_NS = "web-search-free";
84
86
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
85
87
  // 意见 A:zhihu_global / zhihu_site 需要 ZHIHU_API_KEY,不属于免 key 引擎(不得进入 FREE_ENGINES)
86
88
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
87
- const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "zhihu_global", "zhihu_site", "openai", "gemini", "claude"];
89
+ const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "zhihu_global", "zhihu_site", "openai", "gemini", "claude"];
88
90
 
89
91
  // 模型内置搜索(按次计费):只在你显式选中该引擎时使用;自动回退链与 auto 路由都不会兜底到它们。
90
92
  const EXPLICIT_ONLY_ENGINES = ["openai", "gemini", "claude"];
@@ -160,12 +162,12 @@ const FAILURE_CLASS_LABELS = {
160
162
 
161
163
 
162
164
  // 支持 time_range 过滤的引擎(zhihu_global:global_search 的 filter 支持 publish_time>=unix秒,已实测;zhihu_site 未验证,不加)
163
- const TIME_ENGINES = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite", "baidu", "doubao", "zhihu_global"];
165
+ const TIME_ENGINES = ["tavily", "exa", "keenable", "firecrawl", "parallel", "searxng", "ddg", "ddg-lite", "baidu", "doubao", "tinyfish", "zhihu_global"];
164
166
 
165
167
  // 虚拟搜索模式:不是单个引擎,而是 provider.search 内部的路由/并发策略
166
168
  const SEARCH_MODES = ["auto", "multi"];
167
169
 
168
- const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
170
+ const DEFAULT_FALLBACK_ORDER = ["exa", "tavily", "keenable", "firecrawl", "parallel", "perplexity", "serpbase", "serply", "deepseek-official", "you", "baidu", "kimi", "aliyun", "doubao", "tinyfish", "jina", "bing", "anysearch", "ddg", "ddg-lite", "searxng", "zhihu_global", "zhihu_site"];
169
171
 
170
172
  function normalizeDisabledEngines(value) {
171
173
  if (!Array.isArray(value)) return [];
@@ -238,7 +240,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
238
240
  }
239
241
 
240
242
  // 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
241
- const PLUGIN_VERSION = "0.8.2";
243
+ const PLUGIN_VERSION = "0.8.3";
242
244
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
243
245
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
244
246
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -1918,6 +1920,86 @@ async function searchYoucom(query, maxResults, apiKey, signal) {
1918
1920
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
1919
1921
  }
1920
1922
 
1923
+ // TinyFish Search(api.search.tinyfish.ai):GET + X-API-Key;免费 12000 请求/天。
1924
+ // 支持 after_date / before_date(YYYY-MM-DD)时间过滤。
1925
+ async function searchTinyfish(query, maxResults, apiKey, timeRange, signal) {
1926
+ if (!apiKey) throw new Error("TinyFish search requires TINYFISH_API_KEY");
1927
+ const params = new URLSearchParams({ query });
1928
+ if (timeRange) {
1929
+ if (timeRange.after) {
1930
+ params.set("after_date", timeRange.after);
1931
+ } else if (timeRange.days !== undefined) {
1932
+ params.set("after_date", new Date(Date.now() - timeRange.days * 86400000).toISOString().slice(0, 10));
1933
+ }
1934
+ }
1935
+ const response = await fetch(`${TINYFISH_URL}?${params}`, {
1936
+ method: "GET",
1937
+ redirect: "error",
1938
+ headers: {
1939
+ "X-API-Key": apiKey,
1940
+ accept: "application/json",
1941
+ "user-agent": "deepseek-harness/free-search",
1942
+ },
1943
+ signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(15000)]),
1944
+ });
1945
+ if (!response.ok) {
1946
+ const detail = await response.text().catch(() => "");
1947
+ if (response.status === 401 || response.status === 403) {
1948
+ throw new Error(`TinyFish API key is invalid (HTTP ${response.status}) - update it in Settings > Plugins > Free Search`);
1949
+ }
1950
+ if (response.status === 429) {
1951
+ throw new Error("TinyFish rate limit exceeded (HTTP 429) - free tier is 30 req/min, 500 req/hour");
1952
+ }
1953
+ throw new Error(`TinyFish API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
1954
+ }
1955
+ const data = await response.json();
1956
+ // 响应:{ query, results: [ { position, site_name, title, snippet, url } ], total_results, page }
1957
+ const sources = (data.results ?? [])
1958
+ .filter((r) => r.url)
1959
+ .map((r) => ({
1960
+ url: r.url,
1961
+ ...(r.title ? { title: String(r.title) } : {}),
1962
+ ...(r.snippet ? { snippet: String(r.snippet).slice(0, 300) } : {}),
1963
+ }));
1964
+ return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
1965
+ }
1966
+
1967
+ // Jina Search(s.jina.ai):GET + Bearer;需 JINA_API_KEY。
1968
+ async function searchJina(query, maxResults, apiKey, signal) {
1969
+ if (!apiKey) throw new Error("Jina search requires JINA_API_KEY");
1970
+ const params = new URLSearchParams({ q: query });
1971
+ const response = await fetch(`${JINA_URL}?${params}`, {
1972
+ method: "GET",
1973
+ redirect: "error",
1974
+ headers: {
1975
+ authorization: `Bearer ${apiKey}`,
1976
+ accept: "application/json",
1977
+ "user-agent": "deepseek-harness/free-search",
1978
+ },
1979
+ signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(20000)]),
1980
+ });
1981
+ if (!response.ok) {
1982
+ const detail = await response.text().catch(() => "");
1983
+ if (response.status === 401 || response.status === 403) {
1984
+ throw new Error(`Jina API key is invalid (HTTP ${response.status}) - update it in Settings > Plugins > Free Search`);
1985
+ }
1986
+ if (response.status === 429) {
1987
+ throw new Error("Jina rate limit exceeded (HTTP 429) - check your plan at jina.ai");
1988
+ }
1989
+ throw new Error(`Jina API error (HTTP ${response.status}): ${detail.slice(0, 200)}`);
1990
+ }
1991
+ const data = await response.json();
1992
+ // 响应:{ code, status, data: [ { title, url, description, content } ] }
1993
+ const sources = (data.data ?? [])
1994
+ .filter((r) => r.url)
1995
+ .map((r) => ({
1996
+ url: r.url,
1997
+ ...(r.title ? { title: String(r.title) } : {}),
1998
+ ...(r.description ? { snippet: String(r.description).slice(0, 300) } : {}),
1999
+ }));
2000
+ return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
2001
+ }
2002
+
1921
2003
  // 百度千帆 AI 搜索(v2/ai_search/web_search)
1922
2004
  // key = 环境变量 BAIDU_API_KEY
1923
2005
  async function searchBaidu(query, maxResults, apiKey, timeRange, signal) {
@@ -2949,6 +3031,8 @@ const KEY_REF_MAP = {
2949
3031
  kimiApiKey: "MOONSHOT_API_KEY",
2950
3032
  aliyunApiKey: "DASHSCOPE_API_KEY",
2951
3033
  doubaoApiKey: "DOUBAO_SEARCH_API_KEY",
3034
+ tinyfishApiKey: "TINYFISH_API_KEY",
3035
+ jinaApiKey: "JINA_API_KEY",
2952
3036
  zhihuApiKey: "ZHIHU_API_KEY",
2953
3037
  openaiApiKey: "OPENAI_API_KEY",
2954
3038
  geminiApiKey: "GEMINI_API_KEY",
@@ -2988,6 +3072,8 @@ const Config = z.object({
2988
3072
  kimiApiKey: z.string().role("secret").volatile(),
2989
3073
  aliyunApiKey: z.string().role("secret").volatile(),
2990
3074
  doubaoApiKey: z.string().role("secret").volatile(),
3075
+ tinyfishApiKey: z.string().role("secret").volatile(),
3076
+ jinaApiKey: z.string().role("secret").volatile(),
2991
3077
  zhihuApiKey: z.string().role("secret").volatile(),
2992
3078
  // 语言分池(评审意见 B):auto 的 routeHead 与 multi 的并发池按查询语言二选一;未配置用默认两组
2993
3079
  zhPool: z.array(z.string()).volatile(),
@@ -3042,6 +3128,8 @@ const LEGACY_MIGRATABLE_KEYS = [
3042
3128
  "kimiApiKey",
3043
3129
  "aliyunApiKey",
3044
3130
  "doubaoApiKey",
3131
+ "tinyfishApiKey",
3132
+ "jinaApiKey",
3045
3133
  "openaiApiKey",
3046
3134
  "geminiApiKey",
3047
3135
  "claudeApiKey",
@@ -3413,6 +3501,26 @@ function apply(ctx, config) {
3413
3501
  continue; // 无 key 跳过
3414
3502
  }
3415
3503
  result = await searchYoucom(request.query, request.maxResults, key, effSignal);
3504
+ } else if (engine === "tinyfish") {
3505
+ // tinyfish:必须配置 TINYFISH_API_KEY(无 key 跳过);免费 12000 请求/天
3506
+ const key = await resolveApiKey("TINYFISH_API_KEY", "tinyfishApiKey");
3507
+ if (!key) {
3508
+ lastError = new Error("TinyFish requires TINYFISH_API_KEY (get one at agent.tinyfish.ai/api-keys)");
3509
+ if (engine === preferred) preferredFailure = "TINYFISH_API_KEY is not configured";
3510
+ logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
3511
+ continue; // 无 key 跳过
3512
+ }
3513
+ result = await searchTinyfish(request.query, request.maxResults, key, timeRange, effSignal);
3514
+ } else if (engine === "jina") {
3515
+ // jina:必须配置 JINA_API_KEY(无 key 跳过)
3516
+ const key = await resolveApiKey("JINA_API_KEY", "jinaApiKey");
3517
+ if (!key) {
3518
+ lastError = new Error("Jina requires JINA_API_KEY (get one at jina.ai)");
3519
+ if (engine === preferred) preferredFailure = "JINA_API_KEY is not configured";
3520
+ logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
3521
+ continue; // 无 key 跳过
3522
+ }
3523
+ result = await searchJina(request.query, request.maxResults, key, effSignal);
3416
3524
  } else if (engine === "baidu") {
3417
3525
  const key = await resolveApiKey("BAIDU_API_KEY", "baiduApiKey");
3418
3526
  if (!key) {
@@ -3720,6 +3828,16 @@ function apply(ctx, config) {
3720
3828
  if (!key) return { ok: false, error: "SERPLY_API_KEY not configured" };
3721
3829
  return await searchSerply(q, n, key, cfg);
3722
3830
  }
3831
+ case "tinyfish": {
3832
+ const key = await resolveApiKey("TINYFISH_API_KEY", "tinyfishApiKey");
3833
+ if (!key) return { ok: false, error: "TINYFISH_API_KEY not configured" };
3834
+ return await searchTinyfish(q, n, key, tr);
3835
+ }
3836
+ case "jina": {
3837
+ const key = await resolveApiKey("JINA_API_KEY", "jinaApiKey");
3838
+ if (!key) return { ok: false, error: "JINA_API_KEY not configured" };
3839
+ return await searchJina(q, n, key);
3840
+ }
3723
3841
  case "baidu": {
3724
3842
  const key = await resolveApiKey("BAIDU_API_KEY", "baiduApiKey");
3725
3843
  if (!key) return { ok: false, error: "BAIDU_API_KEY not configured" };
@@ -3935,7 +4053,7 @@ function apply(ctx, config) {
3935
4053
  parameters: {
3936
4054
  engines: {
3937
4055
  type: "array",
3938
- description: "Which engines to test (default: all enabled engines). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, serply, deepseek-official, you, baidu, kimi, aliyun, doubao, zhihu_global, zhihu_site, openai, gemini, claude; zhihu_global (web-wide, supports time filter) and zhihu_site (Zhihu-only) need ZHIHU_API_KEY. The virtual modes auto and multi are also accepted and run the real routing/concurrent path. openai/gemini/claude are model-based and bill per search, so they are only used when explicitly named.",
4056
+ description: "Which engines to test (default: all enabled engines). Options: ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, serply, deepseek-official, you, baidu, kimi, aliyun, doubao, tinyfish, jina, zhihu_global, zhihu_site, openai, gemini, claude; zhihu_global (web-wide, supports time filter) and zhihu_site (Zhihu-only) need ZHIHU_API_KEY. The virtual modes auto and multi are also accepted and run the real routing/concurrent path. openai/gemini/claude are model-based and bill per search, so they are only used when explicitly named.",
3939
4057
  items: { type: "string" },
3940
4058
  },
3941
4059
  query: {
@@ -4260,7 +4378,7 @@ function apply(ctx, config) {
4260
4378
  },
4261
4379
  engine: {
4262
4380
  type: "string",
4263
- description: "Optional search mode/engine: multi (parallel full language-pool merge), auto, ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, serply, deepseek-official, you, baidu, kimi, aliyun, doubao, zhihu_global, zhihu_site, openai, gemini, claude (model-based; explicit selection only).",
4381
+ description: "Optional search mode/engine: multi (parallel full language-pool merge), auto, ddg, ddg-lite, bing, searxng, anysearch, exa, tavily, keenable, firecrawl, parallel, perplexity, serpbase, serply, deepseek-official, you, baidu, kimi, aliyun, doubao, tinyfish, jina, zhihu_global, zhihu_site, openai, gemini, claude (model-based; explicit selection only).",
4264
4382
  },
4265
4383
  },
4266
4384
  output: {
@@ -4520,6 +4638,8 @@ export {
4520
4638
  searchSearxng,
4521
4639
  searchSerpbase,
4522
4640
  searchSerply,
4641
+ searchTinyfish,
4642
+ searchJina,
4523
4643
  searchYoucom,
4524
4644
  searchZhihuGlobal,
4525
4645
  searchZhihuSite,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Free web search for DeepSeek Harness: 24 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Zhihu global/site via MCP and Perplexity/SerpBase/DeepSeek/You.com/Baidu/Kimi/Aliyun/Doubao with key; OpenAI/Gemini/Claude model built-in search, explicit-only) + auto smart routing + global engine enable/disable & fallback priority + editable zh/en language pools + multi mode (parallel full language-pool merge) + multi_search cross-source tool + failure-aware fallback (quota/auth cooldown, transient retry, fallbackOn) + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",