dsh-free-search 0.4.18 → 0.4.20

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.
Files changed (3) hide show
  1. package/lib/client.js +80 -80
  2. package/lib/index.js +126 -124
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -1,4 +1,4 @@
1
- window.__ModuleLoader__.load({
1
+ window.__ModuleLoader__.load({
2
2
  id: "dsh-free-search",
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
@@ -30,8 +30,8 @@ window.__ModuleLoader__.load({
30
30
  ".dshfs-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}",
31
31
  ".dshfs-select{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
32
32
  ".dshfs-select:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
33
- // 下拉选项列表配色锁死:不随皮肤变量变化(皮肤只影响 select 框体本身)
34
- // color-scheme 让浏览器原生下拉按主题渲染;option 显式固定底色/文字色兜底
33
+ // 涓嬫媺閫夐」鍒楄〃閰嶈壊閿佹锛氫笉闅忕毊鑲ゅ彉閲忓彉鍖栵紙鐨偆鍙奖鍝?select 妗嗕綋鏈韩锛?
34
+ // color-scheme 璁╂祻瑙堝櫒鍘熺敓涓嬫媺鎸変富棰樻覆鏌擄紱option 鏄惧紡鍥哄畾搴曡壊/鏂囧瓧鑹插厹搴?
35
35
  ".dshfs-select{color-scheme:light dark}",
36
36
  ".dshfs-select option,.dshfs-select optgroup{background-color:#ffffff;color:#1f2328}",
37
37
  "@media (prefers-color-scheme:dark){.dshfs-select{color-scheme:dark}.dshfs-select option,.dshfs-select optgroup{background-color:#1e1f24;color:#e8e8ea}}",
@@ -78,57 +78,57 @@ window.__ModuleLoader__.load({
78
78
  const NS = "free-search";
79
79
  const I18N = {
80
80
  zh: {
81
- description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
82
- unsaved: "未保存",
83
- searchEngine: "搜索引擎",
84
- visit: "访问官网 →",
85
- getKey: "获取 API Key →",
86
- engineHint: "Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
87
- apiKeys: "API 密钥(可选)",
88
- exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
89
- tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
90
- keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
91
- perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
92
- deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
93
- keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
94
- keyStorage: "Key 存储位置",
95
- keyStorageCred: "凭据中心(推荐)",
96
- keyStorageSettings: "设置页(兼容)",
97
- keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
98
- keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
99
- platformSearch: "平台搜索(platform_search 工具)",
100
- platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
101
- cacheTtl: "结果缓存时长(分钟)",
102
- cacheTtlHint: "0 关闭缓存,最长 5 分钟。缩短可加快时效,延长可防限流、省额度。",
103
- unavailable: "设置不可用 —— free-search 桥接未暴露。",
104
- saveFailed: "保存失败",
105
- testing: "测试中…",
106
- testEngine: "测试引擎",
107
- useBing: "恢复 Bing 默认",
108
- discard: "撤销",
109
- saving: "保存中…",
110
- save: "保存",
111
- testOk: (r) => `✓ ${r.count} 条结果(引擎: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · 例如 "${r.sample.slice(0, 40)}"` : ""}`,
112
- testFail: (e) => `✗ ${e}`,
81
+ description: "鍏嶈垂鎼滅储 鈥斺€?鏃犻渶 API key锛圔ing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable锛?,
82
+ unsaved: "鏈繚瀛?,
83
+ searchEngine: "鎼滅储寮曟搸",
84
+ visit: "璁块棶瀹樼綉 鈫?,
85
+ getKey: "鑾峰彇 API Key 鈫?,
86
+ engineHint: "Bing 鏄渶绋冲畾鐨勫厤璐瑰紩鎿庛€侱uckDuckGo 鍦ㄥ叡浜?IP 涓婂彲鑳介檺娴併€侫PI KEY 寮曟搸闇€鍦ㄤ笅鏂瑰~鍐欏嚟鎹€?,
87
+ apiKeys: "API 瀵嗛挜锛堝彲閫夛級",
88
+ exaPh: (c) => c ? "Exa API 瀵嗛挜锛堝凡閰嶇疆锛? : "Exa API 瀵嗛挜锛堝彲閫夛紝涓嶅~涔熷彲鍏嶈垂浣跨敤锛?,
89
+ tavilyPh: (c) => c ? "Tavily API 瀵嗛挜锛堝凡閰嶇疆锛? : "Tavily API 瀵嗛挜锛堝彲閫夛紝涓嶅~涔熷彲鍏嶈垂浣跨敤锛?,
90
+ keenablePh: (c) => c ? "Keenable API 瀵嗛挜锛堝凡閰嶇疆锛? : "Keenable API 瀵嗛挜锛堝彲閫夛紝涓嶅~涔熷彲鍏嶈垂浣跨敤锛?,
91
+ perplexityPh: (c) => c ? "Perplexity API 瀵嗛挜锛堝凡閰嶇疆锛? : "Perplexity API 瀵嗛挜锛坧plx-...锛?,
92
+ deepseekPh: (c) => c ? "DeepSeek API 瀵嗛挜锛堝凡閰嶇疆锛? : "DeepSeek API 瀵嗛挜锛坰k-...锛?,
93
+ keysHint: "瀵嗛挜璇诲彇浼樺厛绾э細.credentials.yaml 鍑嵁涓績 > 杩欓噷 > 鐜鍙橀噺銆傛帹鑽愭妸 key 鍐欒繘鍑嵁涓績锛堜笌瀹樻柟 LLM 涓€鑷达紝涓€澶勭鐞嗭級銆?,
94
+ keyStorage: "Key 瀛樺偍浣嶇疆",
95
+ keyStorageCred: "鍑嵁涓績锛堟帹鑽愶級",
96
+ keyStorageSettings: "璁剧疆椤碉紙鍏煎锛?,
97
+ keyStorageCredHint: (c) => `淇濆瓨鍚庡啓鍏?~/.dsh/.credentials.yaml锛堟渶楂樹紭鍏堢骇锛夈€傚綋鍓嶅凡閰嶇疆锛?{["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "鏃?}`,
98
+ keyStorageSettingsHint: "淇濆瓨鍚庡啓鍏?settings.yaml锛堝悜鍚庡吋瀹硅矾寰勶紝浼樺厛绾т綆浜庡嚟鎹腑蹇冿級銆?,
99
+ platformSearch: "骞冲彴鎼滅储锛坧latform_search 宸ュ叿锛?,
100
+ platformHint: "涓?agent 鐨?platform_search 宸ュ叿鍚敤骞冲彴銆傜鐢ㄧ殑骞冲彴浼氳璺宠繃銆?,
101
+ cacheTtl: "缁撴灉缂撳瓨鏃堕暱锛堝垎閽燂級",
102
+ cacheTtlHint: "0 鍏抽棴缂撳瓨锛屾渶闀?5 鍒嗛挓銆傜缉鐭彲鍔犲揩鏃舵晥锛屽欢闀垮彲闃查檺娴併€佺渷棰濆害銆?,
103
+ unavailable: "璁剧疆涓嶅彲鐢?鈥斺€?free-search 妗ユ帴鏈毚闇层€?,
104
+ saveFailed: "淇濆瓨澶辫触",
105
+ testing: "娴嬭瘯涓€?,
106
+ testEngine: "娴嬭瘯寮曟搸",
107
+ useBing: "鎭㈠ Bing 榛樿",
108
+ discard: "鎾ら攢",
109
+ saving: "淇濆瓨涓€?,
110
+ save: "淇濆瓨",
111
+ testOk: (r) => `鉁?${r.count} 鏉$粨鏋滐紙寮曟搸: ${r.engine}锛?{r.content ? ` 鈥?${r.content}` : ""}${r.sample ? ` 路 渚嬪 "${r.sample.slice(0, 40)}"` : ""}`,
112
+ testFail: (e) => `鉁?${e}`,
113
113
  toggleLang: "EN",
114
- checkUpdate: "检查更新",
115
- checkingUpdate: "检查中…",
116
- updateAvailable: (c, l) => `发现新版本 v${l}(当前 v${c})`,
117
- updateLatest: (c) => `已是最新版本 v${c}`,
118
- updateCheckFailed: "检查更新失败(无法访问 npm registry)",
119
- updateView: "查看 →",
120
- upgrade: "升级",
121
- upgrading: "升级中…",
122
- upgradeLinkMode: "(本地开发模式,升级请用 git pull)",
123
- upgradeDone: (l) => `升级到 v${l} 完成,重启 dsh 后生效`,
124
- upgradeFailed: (m) => `升级失败:${m}`,
114
+ checkUpdate: "妫€鏌ユ洿鏂?,
115
+ checkingUpdate: "妫€鏌ヤ腑鈥?,
116
+ updateAvailable: (c, l) => `鍙戠幇鏂扮増鏈?v${l}锛堝綋鍓?v${c}锛塦,
117
+ updateLatest: (c) => `宸叉槸鏈€鏂扮増鏈?v${c}`,
118
+ updateCheckFailed: "妫€鏌ユ洿鏂板け璐ワ紙鏃犳硶璁块棶 npm registry锛?,
119
+ updateView: "鏌ョ湅 鈫?,
120
+ upgrade: "鍗囩骇",
121
+ upgrading: "鍗囩骇涓€?,
122
+ upgradeLinkMode: "锛堟湰鍦板紑鍙戞ā寮忥紝鍗囩骇璇风敤 git pull锛?,
123
+ upgradeDone: (l) => `鍗囩骇鍒?v${l} 瀹屾垚锛岄噸鍚?dsh 鍚庣敓鏁坄,
124
+ upgradeFailed: (m) => `鍗囩骇澶辫触锛?{m}`,
125
125
  },
126
126
  en: {
127
- description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
127
+ description: "Free web search 鈥?no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
128
128
  unsaved: "unsaved",
129
129
  searchEngine: "Search engine",
130
- visit: "Visit website →",
131
- getKey: "Get API Key →",
130
+ visit: "Visit website 鈫?,
131
+ getKey: "Get API Key 鈫?,
132
132
  engineHint: "Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
133
133
  apiKeys: "API keys (optional)",
134
134
  exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
@@ -146,39 +146,39 @@ window.__ModuleLoader__.load({
146
146
  platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
147
147
  cacheTtl: "Result cache TTL (minutes)",
148
148
  cacheTtlHint: "0 disables caching, max 5 minutes. Lower = fresher results, higher = less rate-limiting / fewer credits used.",
149
- unavailable: "Settings unavailable — the free-search bridge is not exposed.",
149
+ unavailable: "Settings unavailable 鈥?the free-search bridge is not exposed.",
150
150
  saveFailed: "save failed",
151
- testing: "Testing…",
151
+ testing: "Testing鈥?,
152
152
  testEngine: "Test engine",
153
153
  useBing: "Use Bing default",
154
154
  discard: "Discard",
155
- saving: "Saving…",
155
+ saving: "Saving鈥?,
156
156
  save: "Save",
157
- testOk: (r) => `✓ ${r.count} results (engine: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · e.g. "${r.sample.slice(0, 40)}"` : ""}`,
158
- testFail: (e) => `✗ ${e}`,
159
- toggleLang: "中文",
157
+ testOk: (r) => `鉁?${r.count} results (engine: ${r.engine})${r.content ? ` 鈥?${r.content}` : ""}${r.sample ? ` 路 e.g. "${r.sample.slice(0, 40)}"` : ""}`,
158
+ testFail: (e) => `鉁?${e}`,
159
+ toggleLang: "涓枃",
160
160
  checkUpdate: "Check update",
161
- checkingUpdate: "Checking…",
161
+ checkingUpdate: "Checking鈥?,
162
162
  updateAvailable: (c, l) => `New version v${l} available (current v${c})`,
163
163
  updateLatest: (c) => `You're on the latest version v${c}`,
164
164
  updateCheckFailed: "Update check failed (cannot reach npm registry)",
165
- updateView: "View →",
165
+ updateView: "View 鈫?,
166
166
  upgrade: "Upgrade",
167
- upgrading: "Upgrading…",
167
+ upgrading: "Upgrading鈥?,
168
168
  upgradeLinkMode: "(local dev install - use git pull to update)",
169
169
  upgradeDone: (l) => `Upgraded to v${l} - restart dsh to apply`,
170
170
  upgradeFailed: (m) => `Upgrade failed: ${m}`,
171
171
  },
172
172
  };
173
173
  const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
174
- // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
175
- const PLUGIN_VERSION = "0.4.17";
174
+ // 褰撳墠鎻掍欢鐗堟湰锛堜笌 lib/index.js 鐨?PLUGIN_VERSION 淇濇寔涓€鑷达級
175
+ const PLUGIN_VERSION = "0.4.20";
176
176
  const ENGINES = [
177
- { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
178
- { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
177
+ { id: "ddg", label: "DuckDuckGo 路 HTML", badge: "FREE", link: "https://duckduckgo.com" },
178
+ { id: "ddg-lite", label: "DuckDuckGo 路 Lite", badge: "FREE", link: "https://duckduckgo.com" },
179
179
  { id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
180
- { id: "anysearch", label: "AnySearch · AI", badge: "FREE", link: "https://anysearch.com" },
181
- { id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
180
+ { id: "anysearch", label: "AnySearch 路 AI", badge: "FREE", link: "https://anysearch.com" },
181
+ { id: "searxng", label: "SearXNG 路 鍏冩悳绱?, badge: "FREE", link: "https://github.com/searxng/searxng" },
182
182
  { id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
183
183
  { id: "tavily", label: "Tavily", badge: "FREE", link: "https://app.tavily.com/home" },
184
184
  { id: "keenable", label: "Keenable", badge: "FREE", link: "https://keenable.ai/login" },
@@ -250,7 +250,7 @@ window.__ModuleLoader__.load({
250
250
  }
251
251
 
252
252
  function FreeSearchCard(props) {
253
- // 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
253
+ // 妫€娴嬪簲鐢ㄤ富棰樻繁娴咃紙璇?body 鐨?--dsw-alias-bg-base 鍙橀噺浜害锛夛紝鐢ㄤ簬閿佸畾鍘熺敓涓嬫媺閰嶈壊
254
254
  const isDarkScheme = react.useMemo(() => {
255
255
  try {
256
256
  const root = document.body || document.documentElement;
@@ -282,9 +282,9 @@ window.__ModuleLoader__.load({
282
282
  const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
283
283
  const [cacheTtl, setCacheTtl] = react.useState(5);
284
284
  const [keysConfigured, setKeysConfigured] = react.useState({});
285
- // key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
285
+ // key 瀛樺偍浣嶇疆锛歝redentials锛堝嚟鎹腑蹇冿紝榛樿锛墊 settings锛堣缃〉锛屽吋瀹规棫琛屼负锛?
286
286
  const [keyStorage, setKeyStorage] = react.useState("credentials");
287
- // 凭据中心里已配置的 key(describe 不返回,需单独查)
287
+ // 鍑嵁涓績閲屽凡閰嶇疆鐨?key锛坉escribe 涓嶈繑鍥烇紝闇€鍗曠嫭鏌ワ級
288
288
  const [credConfigured, setCredConfigured] = react.useState({});
289
289
  const [lang, setLang] = react.useState("zh");
290
290
  const [dirty, setDirty] = react.useState(false);
@@ -313,7 +313,7 @@ window.__ModuleLoader__.load({
313
313
  setDeepseekKey(v.deepseekApiKey ?? "");
314
314
  setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
315
315
  setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
316
- // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
316
+ // secrets 瀛楁鏍囪鍝簺 key 宸查厤缃紙鍊艰鑴辨晱锛屼粎鏄剧ず"宸查厤缃?锛?
317
317
  const configured = {};
318
318
  for (const secret of view.secrets ?? []) {
319
319
  if (secret.set) {
@@ -326,10 +326,10 @@ window.__ModuleLoader__.load({
326
326
  }
327
327
  }
328
328
  setKeysConfigured(configured);
329
- // key 存储位置(默认凭据中心)
329
+ // key 瀛樺偍浣嶇疆锛堥粯璁ゅ嚟鎹腑蹇冿級
330
330
  setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
331
331
  setState({ status: "ready", writable: result.value.writable });
332
- // 查询凭据中心里各 key 的配置状态
332
+ // 鏌ヨ鍑嵁涓績閲屽悇 key 鐨勯厤缃姸鎬?
333
333
  try {
334
334
  const cred = await bridgeCredentialsStatus();
335
335
  if (cred.ok) {
@@ -366,7 +366,7 @@ window.__ModuleLoader__.load({
366
366
  setSaving(true);
367
367
  setFailed(false);
368
368
  try {
369
- // key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
369
+ // key 瀛樺偍鍒嗘祦锛氬嚟鎹腑蹇冿紙榛樿锛夎蛋 credentials-set锛涜缃〉璧?settings mutate锛堝吋瀹癸級
370
370
  const keyFields = [
371
371
  ["exaApiKey", exaKey],
372
372
  ["tavilyApiKey", tavilyKey],
@@ -387,7 +387,7 @@ window.__ModuleLoader__.load({
387
387
  ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
388
388
  ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
389
389
  if (keyStorage !== "credentials") {
390
- // settings 模式:key 仍写 settings.yaml(旧行为)
390
+ // settings 妯″紡锛歬ey 浠嶅啓 settings.yaml锛堟棫琛屼负锛?
391
391
  for (const [field, value] of keyFields) {
392
392
  if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
393
393
  }
@@ -527,7 +527,7 @@ window.__ModuleLoader__.load({
527
527
  }),
528
528
  react_jsx_runtime.jsx("span", {
529
529
  className: open ? "dshfs-chevron dshfs-chevronOpen" : "dshfs-chevron",
530
- children: "▾",
530
+ children: "鈻?,
531
531
  }),
532
532
  ],
533
533
  }),
@@ -890,8 +890,8 @@ react_jsx_runtime.jsx("div", {
890
890
  const inject = ["slots", "commandUi"];
891
891
 
892
892
  function apply(ctx) {
893
- // 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
894
- // 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
893
+ // 鎸傚畼鏂规彃妲?settings.plugin.item锛堣缃?鈫?鎻掍欢 鈫?鍙厤缃爣绛鹃〉锛夈€?
894
+ // 涓嶄緷璧?dsh-web-ui锛氶厤缃鍐欒蛋鑷缓 bridge锛?api/dsh-free-search-settings锛夈€?
895
895
  ctx.slots.inject("settings.plugin.item", () =>
896
896
  ctx.slots.register(
897
897
  {
@@ -904,14 +904,14 @@ react_jsx_runtime.jsx("div", {
904
904
  FreeSearchCard
905
905
  )
906
906
  );
907
- // /free-search-engine 弹出式命令:输入 "/" 选中后弹出引擎列表,点选即切换。
908
- // 等效于设置页切换引擎+保存;命令只改 provider 配置,搜索仍走回退链。
907
+ // /free-search-engine 寮瑰嚭寮忓懡浠わ細杈撳叆 "/" 閫変腑鍚庡脊鍑哄紩鎿庡垪琛紝鐐归€夊嵆鍒囨崲銆?
908
+ // 绛夋晥浜庤缃〉鍒囨崲寮曟搸+淇濆瓨锛涘懡浠ゅ彧鏀?provider 閰嶇疆锛屾悳绱粛璧板洖閫€閾俱€?
909
909
  ctx.inject(["commandUi"], (sctx) => {
910
910
  const command = sctx.get("commandUi");
911
911
  sctx.effect(() => {
912
912
  const dispose = command.register({
913
913
  name: "free-search-engine",
914
- description: "切换搜索引擎 / Switch web search engine",
914
+ description: "鍒囨崲鎼滅储寮曟搸 / Switch web search engine",
915
915
  available: () => true,
916
916
  ui: {
917
917
  kind: "popupSelect",
@@ -921,8 +921,8 @@ react_jsx_runtime.jsx("div", {
921
921
  const current = view?.value?.provider ?? "bing";
922
922
  return ENGINES.map((e) => ({
923
923
  id: e.id,
924
- label: `${e.label}${e.badge === "FREE" ? " · 免费" : " · API Key"}`,
925
- detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "当前") : undefined,
924
+ label: `${e.label}${e.badge === "FREE" ? " 路 鍏嶈垂" : " 路 API Key"}`,
925
+ detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "褰撳墠") : undefined,
926
926
  active: e.id === current,
927
927
  }));
928
928
  },
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { SettingsConflictError, installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
1
+ import { SettingsConflictError, SettingsProvider } from "@deepseek-ai/dsh-settings";
2
2
  import { defineTool } from "@deepseek-ai/dsh-tools";
3
3
  import z from "@deepseek-ai/schemastery";
4
4
  import fs from "node:fs";
@@ -15,19 +15,19 @@ const USER_AGENT =
15
15
  "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36";
16
16
  const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
17
17
 
18
- const FREE_SEARCH_NS = settingsNamespace("free-search");
18
+ const FREE_SEARCH_NS = "free-search";
19
19
  const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
20
20
  const FREE_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch"];
21
21
  const ALL_ENGINES = ["ddg", "ddg-lite", "bing", "searxng", "anysearch", "exa", "tavily", "keenable", "perplexity", "deepseek-official"];
22
22
 
23
- // 当前插件版本(发布时与 package.json 同步)
24
- const PLUGIN_VERSION = "0.4.17";
25
- // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
23
+ // 褰撳墠鎻掍欢鐗堟湰锛堝彂甯冩椂涓?package.json 鍚屾锛?
24
+ const PLUGIN_VERSION = "0.4.20";
25
+ // 妫€鏌ユ洿鏂扮殑 npm registry 鍏冩暟鎹湴鍧€锛坉sh-free-search 鏄?npmjs 涓婄殑鍏紑鍖咃級
26
26
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
27
27
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
28
28
  const PLUGIN_REPO_URL = "https://github.com/DDDMUC/dsh-free-search";
29
29
 
30
- // 查询 npm registry 的最新版本;失败时返回 null(网络/代理问题不阻塞设置页)
30
+ // 鏌ヨ npm registry 鐨勬渶鏂扮増鏈紱澶辫触鏃惰繑鍥?null锛堢綉缁?浠g悊闂涓嶉樆濉炶缃〉锛?
31
31
  async function fetchLatestVersion(signal) {
32
32
  const controller = new AbortController();
33
33
  const timer = setTimeout(() => controller.abort(), 10000);
@@ -49,7 +49,7 @@ async function fetchLatestVersion(signal) {
49
49
  }
50
50
  }
51
51
 
52
- // 简单的 semver 比较(仅处理 x.y.z 主/次/补丁,忽略预发布标签);a>b 返回 1, a<b 返回 -1, 相等返回 0
52
+ // 绠€鍗曠殑 semver 姣旇緝锛堜粎澶勭悊 x.y.z 涓?娆?琛ヤ竵锛屽拷鐣ラ鍙戝竷鏍囩锛夛紱a>b 杩斿洖 1, a<b 杩斿洖 -1, 鐩哥瓑杩斿洖 0
53
53
  function compareVersions(a, b) {
54
54
  const na = String(a).split(".").map((n) => parseInt(n, 10) || 0);
55
55
  const nb = String(b).split(".").map((n) => parseInt(n, 10) || 0);
@@ -60,8 +60,8 @@ function compareVersions(a, b) {
60
60
  return 0;
61
61
  }
62
62
 
63
- // 检测插件的安装模式:遍历 profiles/*/node_modules/dsh-free-search,
64
- // symlink(link: 本地开发)→ isLink=true;npm 真安装 → isLink=false;找不到 → null
63
+ // 妫€娴嬫彃浠剁殑瀹夎妯″紡锛氶亶鍘?profiles/*/node_modules/dsh-free-search锛?
64
+ // symlink锛坙ink: 鏈湴寮€鍙戯級鈫?isLink=true锛沶pm 鐪熷畨瑁?鈫?isLink=false锛涙壘涓嶅埌 鈫?null
65
65
  function detectInstallMode() {
66
66
  const profilesDir = path.join(process.cwd(), "profiles");
67
67
  let found = null;
@@ -80,7 +80,7 @@ function detectInstallMode() {
80
80
  return found;
81
81
  }
82
82
 
83
- // time_range 支持:固定档 day/week/month/year,或自定义(相对 12h/3d/2mo/1y、绝对 YYYY-MM-DD)
83
+ // time_range 鏀寔锛氬浐瀹氭。 day/week/month/year锛屾垨鑷畾涔夛紙鐩稿 12h/3d/2mo/1y銆佺粷瀵?YYYY-MM-DD锛?
84
84
  const TIME_RANGES = ["day", "week", "month", "year"];
85
85
  const DAYS_BY_RANGE = { day: 1, week: 7, month: 30, year: 365 };
86
86
  const KEENABLE_REL = { day: "1d", week: "7d", month: "1mo", year: "1y" };
@@ -90,12 +90,12 @@ function isoDaysAgo(days) {
90
90
  return new Date(Date.now() - days * 86_400_000).toISOString().replace(/\.\d{3}Z$/, ".000Z");
91
91
  }
92
92
 
93
- // 把用户/agent 给的 timeRange 解析成统一对象:{ days } 相对天数,或 { after } 绝对日期。
94
- // 输入支持:day/week/month/year、12h/3d/2mo/1y、2026-07-01,或已解析的 {days}/{after} 对象。
95
- // 无效返回 undefined。
93
+ // 鎶婄敤鎴?agent 缁欑殑 timeRange 瑙f瀽鎴愮粺涓€瀵硅薄锛歿 days } 鐩稿澶╂暟锛屾垨 { after } 缁濆鏃ユ湡銆?
94
+ // 杈撳叆鏀寔锛歞ay/week/month/year銆?2h/3d/2mo/1y銆?026-07-01锛屾垨宸茶В鏋愮殑 {days}/{after} 瀵硅薄銆?
95
+ // 鏃犳晥杩斿洖 undefined銆?
96
96
  function parseTimeRange(input) {
97
97
  if (input === undefined || input === null) return undefined;
98
- // 已解析对象:直接透传
98
+ // 宸茶В鏋愬璞★細鐩存帴閫忎紶
99
99
  if (typeof input === "object") {
100
100
  if (typeof input.after === "string" && /^\d{4}-\d{2}-\d{2}$/.test(input.after)) return { after: input.after };
101
101
  if (typeof input.days === "number" && Number.isFinite(input.days) && input.days > 0) return { days: input.days };
@@ -116,7 +116,7 @@ function parseTimeRange(input) {
116
116
  return undefined;
117
117
  }
118
118
 
119
- // 把自定义天数映射到只支持固定档的引擎(Tavily / SearXNG / DDG)的最近似档位
119
+ // 鎶婅嚜瀹氫箟澶╂暟鏄犲皠鍒板彧鏀寔鍥哄畾妗g殑寮曟搸锛圱avily / SearXNG / DDG锛夌殑鏈€杩戜技妗d綅
120
120
  function approximateTimeRange(days) {
121
121
  if (days <= 2) return "day";
122
122
  if (days <= 14) return "week";
@@ -136,18 +136,18 @@ function decodeEntities(text) {
136
136
  .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(Number(n)));
137
137
  }
138
138
 
139
- //#region 结果缓存(防限流/省额度,LRU 50 条,TTL 可配置 0-5 分钟)
139
+ //#region 缁撴灉缂撳瓨锛堥槻闄愭祦/鐪侀搴︼紝LRU 50 鏉★紝TTL 鍙厤缃?0-5 鍒嗛挓锛?
140
140
  const CACHE_MAX_ENTRIES = 50;
141
- // fallback 条目(实际引擎 ≠ 首选引擎时)TTL = 配置 TTL 的 1/5(默认 5 分钟 → 60s):
142
- // 首选引擎恢复后最多 1 分钟即可拿到新结果,避免回退结果被完整 TTL 钉死;首选成功条目仍用完整 TTL。
141
+ // fallback 鏉$洰锛堝疄闄呭紩鎿?鈮?棣栭€夊紩鎿庢椂锛塗TL = 閰嶇疆 TTL 鐨?1/5锛堥粯璁?5 鍒嗛挓 鈫?60s锛夛細
142
+ // 棣栭€夊紩鎿庢仮澶嶅悗鏈€澶?1 鍒嗛挓鍗冲彲鎷垮埌鏂扮粨鏋滐紝閬垮厤鍥為€€缁撴灉琚畬鏁?TTL 閽夋锛涢閫夋垚鍔熸潯鐩粛鐢ㄥ畬鏁?TTL銆?
143
143
 
144
144
  function buildCacheKey(query, maxResults, timeRangeLabel, preferred) {
145
145
  return [query ?? "", maxResults ?? 5, timeRangeLabel ?? "", preferred].join("\u0000");
146
146
  }
147
147
  //#endregion
148
148
 
149
- // 统一的 snippet 清洗:剔除登录/付费墙/订阅等噪音短语,折叠空白,限制长度。
150
- // 只在回退链出口统一应用,各引擎内部不做,避免重复处理。
149
+ // 缁熶竴鐨?snippet 娓呮礂锛氬墧闄ょ櫥褰?浠樿垂澧?璁㈤槄绛夊櫔闊崇煭璇紝鎶樺彔绌虹櫧锛岄檺鍒堕暱搴︺€?
150
+ // 鍙湪鍥為€€閾惧嚭鍙g粺涓€搴旂敤锛屽悇寮曟搸鍐呴儴涓嶅仛锛岄伩鍏嶉噸澶嶅鐞嗐€?
151
151
  const SNIPPET_NOISE =
152
152
  /\b(sign up|sign in|log in|login|subscribe( to| for)?|member[- ]?only|become a member|create (a )?free account|read more|continue reading|story continues|get started|install (the )?app|view on|medium membership|join \w+ for free|get updates from this writer|stories in your inbox|remember me for|unlock this|free to read|become a patron)\b/gi;
153
153
 
@@ -193,7 +193,7 @@ function uniqueSources(sources, limit) {
193
193
  }
194
194
 
195
195
  async function fetchHtml(url, signal) {
196
- // 单次请求超时 12s,避免挂起被当成 Connection error
196
+ // 鍗曟璇锋眰瓒呮椂 12s锛岄伩鍏嶆寕璧疯褰撴垚 Connection error
197
197
  let response;
198
198
  try {
199
199
  const controller = new AbortController();
@@ -215,14 +215,14 @@ async function fetchHtml(url, signal) {
215
215
  throw new Error(`HTTP ${response.status} from ${url.split("?")[0]}`);
216
216
  }
217
217
  const html = await response.text();
218
- // DuckDuckGo 反爬验证页检测(HTTP 202 或验证关键字)
218
+ // DuckDuckGo 鍙嶇埇楠岃瘉椤垫娴嬶紙HTTP 202 鎴栭獙璇佸叧閿瓧锛?
219
219
  if (response.status === 202 || /anomaly|captcha|unusual traffic|robot check/i.test(html.slice(0, 4000))) {
220
220
  throw new Error("DuckDuckGo is rate-limited right now (anti-bot challenge, usually temporary) - Bing works");
221
221
  }
222
222
  return html;
223
223
  }
224
224
 
225
- // 带重试的抓取:网络错误/空结果时重试,间隔 1.5s,最多 3 次
225
+ // 甯﹂噸璇曠殑鎶撳彇锛氱綉缁滈敊璇?绌虹粨鏋滄椂閲嶈瘯锛岄棿闅?1.5s锛屾渶澶?3 娆?
226
226
  async function fetchHtmlWithRetry(url, signal) {
227
227
  let lastError;
228
228
  for (let attempt = 1; attempt <= 3; attempt++) {
@@ -241,10 +241,10 @@ async function fetchHtmlWithRetry(url, signal) {
241
241
  async function searchDdgHtml(query, maxResults, options, signal) {
242
242
  const params = new URLSearchParams({ q: query });
243
243
  if (options?.region) params.set("kl", options.region);
244
- // DDG 安全搜索:off(adlt=-1) / moderate(adlt=0) / strict(adlt=1)
244
+ // DDG 瀹夊叏鎼滅储锛歰ff(adlt=-1) / moderate(adlt=0) / strict(adlt=1)
245
245
  const adlt = options?.safeSearch ?? "off";
246
246
  params.set("adlt", adlt === "strict" ? "1" : adlt === "moderate" ? "0" : "-1");
247
- // DDG 时间过滤:df=d/w/m/y(只支持固定档,自定义取近似档)
247
+ // DDG 鏃堕棿杩囨护锛歞f=d/w/m/y锛堝彧鏀寔鍥哄畾妗o紝鑷畾涔夊彇杩戜技妗o級
248
248
  if (options?.timeRange) {
249
249
  const df = { day: "d", week: "w", month: "m", year: "y" }[approximateTimeRange(options.timeRange.days ?? 7)];
250
250
  if (df) params.set("df", df);
@@ -273,7 +273,7 @@ async function searchDdgLite(query, maxResults, options, signal) {
273
273
  const params = new URLSearchParams({ q: query });
274
274
  const adlt = options?.safeSearch ?? "off";
275
275
  params.set("adlt", adlt === "strict" ? "1" : adlt === "moderate" ? "0" : "-1");
276
- // DDG Lite 同样支持 df 时间过滤
276
+ // DDG Lite 鍚屾牱鏀寔 df 鏃堕棿杩囨护
277
277
  if (options?.timeRange) {
278
278
  const df = { day: "d", week: "w", month: "m", year: "y" }[approximateTimeRange(options.timeRange.days ?? 7)];
279
279
  if (df) params.set("df", df);
@@ -336,12 +336,12 @@ async function searchSearxng(query, maxResults, options, signal) {
336
336
  const instances = options?.searxngInstances?.length
337
337
  ? options.searxngInstances
338
338
  : SEARXNG_INSTANCES;
339
- // 聚合所有实例的失败原因,避免只显示最后一个实例的错误
339
+ // 鑱氬悎鎵€鏈夊疄渚嬬殑澶辫触鍘熷洜锛岄伩鍏嶅彧鏄剧ず鏈€鍚庝竴涓疄渚嬬殑閿欒
340
340
  const errors = [];
341
341
  for (const base of instances) {
342
342
  try {
343
343
  const params = new URLSearchParams({ q: query, format: "json" });
344
- // SearXNG 原生支持 time_range 过滤(只支持固定档,自定义取近似档)
344
+ // SearXNG 鍘熺敓鏀寔 time_range 杩囨护锛堝彧鏀寔鍥哄畾妗o紝鑷畾涔夊彇杩戜技妗o級
345
345
  if (options?.timeRange) {
346
346
  const tr = SEARXNG_TIME[approximateTimeRange(options.timeRange.days ?? 7)];
347
347
  if (tr) params.set("time_range", tr);
@@ -381,9 +381,9 @@ async function searchSearxng(query, maxResults, options, signal) {
381
381
  errors.push(`${base}: ${message}`);
382
382
  }
383
383
  }
384
- // 空实例列表兜底:避免 "all SearXNG instances failed: " 尾巴悬空
384
+ // 绌哄疄渚嬪垪琛ㄥ厹搴曪細閬垮厤 "all SearXNG instances failed: " 灏惧反鎮┖
385
385
  const detail = errors.length > 0 ? errors.join(", ") : "no instances configured";
386
- // Note 会引用这个错误消息,截断避免 6 实例全挂时刷屏
386
+ // Note 浼氬紩鐢ㄨ繖涓敊璇秷鎭紝鎴柇閬垮厤 6 瀹炰緥鍏ㄦ寕鏃跺埛灞?
387
387
  throw new Error(`all SearXNG instances failed: ${detail.slice(0, 300)}`);
388
388
  }
389
389
  //#endregion
@@ -392,7 +392,7 @@ async function searchSearxng(query, maxResults, options, signal) {
392
392
  const ANYSEARCH_URL = "https://api.anysearch.com/v1/search";
393
393
  const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
394
394
 
395
- // AnySearch: 免费匿名额度(无 key),结构化 JSON 结果
395
+ // AnySearch: 鍏嶈垂鍖垮悕棰濆害锛堟棤 key锛夛紝缁撴瀯鍖?JSON 缁撴灉
396
396
  async function searchAnysearch(query, maxResults, signal) {
397
397
  const controller = new AbortController();
398
398
  const timer = setTimeout(() => controller.abort(), 12000);
@@ -429,7 +429,7 @@ async function searchAnysearch(query, maxResults, signal) {
429
429
  };
430
430
  }
431
431
 
432
- // Exa MCP: 匿名公开 MCP(无 key),web_search_exa 工具
432
+ // Exa MCP: 鍖垮悕鍏紑 MCP锛堟棤 key锛夛紝web_search_exa 宸ュ叿
433
433
  async function searchExaMCP(query, maxResults, signal) {
434
434
  const controller = new AbortController();
435
435
  const timer = setTimeout(() => controller.abort(), 20000);
@@ -457,7 +457,7 @@ async function searchExaMCP(query, maxResults, signal) {
457
457
  }
458
458
  if (!response.ok) throw new Error(`Exa MCP error (HTTP ${response.status})`);
459
459
  const text = await response.text();
460
- // 解析 SSE 格式:event: message\ndata: {...}
460
+ // 瑙f瀽 SSE 鏍煎紡锛歟vent: message\ndata: {...}
461
461
  const lines = text.split("\n");
462
462
  let json = null;
463
463
  for (const line of lines) {
@@ -477,7 +477,7 @@ async function searchExaMCP(query, maxResults, signal) {
477
477
  .filter((b) => b.type === "text")
478
478
  .map((b) => b.text)
479
479
  .join("\n");
480
- // 解析 "Title: X\nURL: Y\nPublished: Z\nHighlights:\n..."
480
+ // 瑙f瀽 "Title: X\nURL: Y\nPublished: Z\nHighlights:\n..."
481
481
  const blocks = textBlocks.split(/\n(?=Title:)/);
482
482
  for (const block of blocks) {
483
483
  const title = block.match(/^Title: (.+)$/m)?.[1];
@@ -489,7 +489,7 @@ async function searchExaMCP(query, maxResults, signal) {
489
489
  url,
490
490
  ...(title ? { title } : {}),
491
491
  ...(highlights ? { snippet: highlights.slice(0, 300) } : {}),
492
- // 只保留日期形态(ISO 或 YYYY-MM-DD),过滤 "N/A" 等占位符
492
+ // 鍙繚鐣欐棩鏈熷舰鎬侊紙ISO 鎴?YYYY-MM-DD锛夛紝杩囨护 "N/A" 绛夊崰浣嶇
493
493
  ...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
494
494
  });
495
495
  }
@@ -523,7 +523,7 @@ async function searchGithub(query, maxResults, signal) {
523
523
  sources: (data.items ?? []).map((item) => ({
524
524
  url: item.html_url,
525
525
  title: item.full_name ?? item.name,
526
- snippet: `${item.description ?? ""}${item.stargazers_count ? ` ⭐${item.stargazers_count}` : ""}`.trim(),
526
+ snippet: `${item.description ?? ""}${item.stargazers_count ? ` 猸?{item.stargazers_count}` : ""}`.trim(),
527
527
  })),
528
528
  truncated: false,
529
529
  };
@@ -617,11 +617,11 @@ async function searchHackerNews(query, maxResults, signal) {
617
617
  sources: (data.hits ?? [])
618
618
  .filter((h) => h.title || h.story_title)
619
619
  .map((h) => ({
620
- // 有外链用外链,纯讨论帖用 HN 讨论页
620
+ // 鏈夊閾剧敤澶栭摼锛岀函璁ㄨ甯栫敤 HN 璁ㄨ椤?
621
621
  url: h.url ?? `https://news.ycombinator.com/item?id=${h.objectID}`,
622
622
  title: h.title ?? h.story_title,
623
623
  ...((h.points !== undefined && h.points !== null) || (h.num_comments !== undefined && h.num_comments !== null)
624
- ? { snippet: `HN discussion · ${h.points ?? 0} points · ${h.num_comments ?? 0} comments` }
624
+ ? { snippet: `HN discussion 路 ${h.points ?? 0} points 路 ${h.num_comments ?? 0} comments` }
625
625
  : {}),
626
626
  })),
627
627
  truncated: false,
@@ -644,7 +644,7 @@ async function searchStackOverflow(query, maxResults, signal) {
644
644
  url: it.link,
645
645
  title: it.title,
646
646
  ...(it.score !== undefined || it.answer_count !== undefined
647
- ? { snippet: `${it.is_answered ? "✓ answered" : "unanswered"} · score ${it.score ?? 0} · ${it.answer_count ?? 0} answers` }
647
+ ? { snippet: `${it.is_answered ? "鉁?answered" : "unanswered"} 路 score ${it.score ?? 0} 路 ${it.answer_count ?? 0} answers` }
648
648
  : {}),
649
649
  })),
650
650
  truncated: false,
@@ -666,7 +666,7 @@ async function searchWikipedia(query, maxResults, signal, lang) {
666
666
  sources: (data.query?.search ?? []).map((s) => ({
667
667
  url: `https://${host}/wiki/${encodeURIComponent(String(s.title).replace(/ /g, "_"))}`,
668
668
  title: s.title,
669
- // snippet 含 <span class="searchmatch"> 高亮标签,剥掉
669
+ // snippet 鍚?<span class="searchmatch"> 楂樹寒鏍囩锛屽墺鎺?
670
670
  ...(s.snippet ? { snippet: stripTags(s.snippet).slice(0, 200) } : {}),
671
671
  })),
672
672
  truncated: false,
@@ -691,7 +691,7 @@ async function searchNpm(query, maxResults, signal) {
691
691
  url: p.links?.npm ?? `https://www.npmjs.com/package/${p.name}`,
692
692
  title: p.name,
693
693
  ...((p.description || p.version)
694
- ? { snippet: `v${p.version ?? "?"}${p.description ? ` — ${String(p.description).slice(0, 160)}` : ""}` }
694
+ ? { snippet: `v${p.version ?? "?"}${p.description ? ` 鈥?${String(p.description).slice(0, 160)}` : ""}` }
695
695
  : {}),
696
696
  })),
697
697
  truncated: false,
@@ -731,7 +731,7 @@ async function searchExa(query, maxResults, apiKey, timeRange, signal) {
731
731
  contents: { highlights: { highlightsPerUrl: 1 } },
732
732
  ...(maxResults !== undefined ? { numResults: maxResults } : {}),
733
733
  };
734
- // Exa 时间过滤:startPublishedDate(ISO 日期;支持任意天数和绝对日期)
734
+ // Exa 鏃堕棿杩囨护锛歴tartPublishedDate锛圛SO 鏃ユ湡锛涙敮鎸佷换鎰忓ぉ鏁板拰缁濆鏃ユ湡锛?
735
735
  if (timeRange) {
736
736
  if (timeRange.after) body.startPublishedDate = timeRange.after;
737
737
  else if (timeRange.days !== undefined) body.startPublishedDate = isoDaysAgo(timeRange.days);
@@ -771,7 +771,7 @@ async function searchExa(query, maxResults, apiKey, timeRange, signal) {
771
771
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
772
772
  }
773
773
 
774
- // Tavily: 无 key 走 keyless(免费匿名额度),有 key 走账号档(Bearer)
774
+ // Tavily: 鏃?key 璧?keyless锛堝厤璐瑰尶鍚嶉搴︼級锛屾湁 key 璧拌处鍙锋。锛圔earer锛?
775
775
  async function searchTavily(query, maxResults, apiKey, timeRange, signal) {
776
776
  const controller = new AbortController();
777
777
  const timer = setTimeout(() => controller.abort(), 15000);
@@ -784,7 +784,7 @@ async function searchTavily(query, maxResults, apiKey, timeRange, signal) {
784
784
  max_results: Math.min(maxResults ?? 5, 20),
785
785
  search_depth: "basic",
786
786
  };
787
- // Tavily 时间过滤:time_range 只支持固定档,自定义天数取最近似档位
787
+ // Tavily 鏃堕棿杩囨护锛歵ime_range 鍙敮鎸佸浐瀹氭。锛岃嚜瀹氫箟澶╂暟鍙栨渶杩戜技妗d綅
788
788
  if (timeRange) {
789
789
  const tr = approximateTimeRange(timeRange.days ?? 7);
790
790
  if (tr) body.time_range = tr;
@@ -825,7 +825,7 @@ async function searchTavily(query, maxResults, apiKey, timeRange, signal) {
825
825
  return { sources: uniqueSources(sources, maxResults ?? 10), truncated: false };
826
826
  }
827
827
 
828
- // 把任意天数转成 Keenable 的相对时间格式(12h / Nd / Nmo / Ny)
828
+ // 鎶婁换鎰忓ぉ鏁拌浆鎴?Keenable 鐨勭浉瀵规椂闂存牸寮忥紙12h / Nd / Nmo / Ny锛?
829
829
  function formatKeenableRelative(days) {
830
830
  if (days <= 0.5) return "12h";
831
831
  if (days < 1) return `${Math.round(days * 24)}h`;
@@ -834,7 +834,7 @@ function formatKeenableRelative(days) {
834
834
  return `${Math.round(days / 365)}y`;
835
835
  }
836
836
 
837
- // Keenable: 有 key 走 REST API(X-API-Key),无 key 走 keyless MCP(免费匿名额度)
837
+ // Keenable: 鏈?key 璧?REST API锛圶-API-Key锛夛紝鏃?key 璧?keyless MCP锛堝厤璐瑰尶鍚嶉搴︼級
838
838
  function extractKeenableSources(text, maxResults) {
839
839
  const sources = [];
840
840
  const blocks = String(text).split(/\n(?=Title:)/);
@@ -848,7 +848,7 @@ function extractKeenableSources(text, maxResults) {
848
848
  url,
849
849
  ...(title ? { title } : {}),
850
850
  ...(snippets ? { snippet: snippets.slice(0, 300) } : {}),
851
- // 与 Exa MCP 一致:只保留日期形态,过滤 "N/A" 等占位符
851
+ // 涓?Exa MCP 涓€鑷达細鍙繚鐣欐棩鏈熷舰鎬侊紝杩囨护 "N/A" 绛夊崰浣嶇
852
852
  ...(published && /^\d{4}-\d{2}-\d{2}/.test(published) ? { publishedAt: published } : {}),
853
853
  });
854
854
  }
@@ -863,7 +863,7 @@ async function searchKeenableREST(query, maxResults, apiKey, timeRange, signal)
863
863
  let response;
864
864
  try {
865
865
  const body = { query, mode: "realtime" };
866
- // Keenable 时间过滤:published_after(相对 12h/7d/1mo/1y 或绝对 YYYY-MM-DD)
866
+ // Keenable 鏃堕棿杩囨护锛歱ublished_after锛堢浉瀵?12h/7d/1mo/1y 鎴栫粷瀵?YYYY-MM-DD锛?
867
867
  if (timeRange) {
868
868
  if (timeRange.after) body.published_after = timeRange.after;
869
869
  else if (timeRange.days !== undefined) body.published_after = formatKeenableRelative(timeRange.days);
@@ -908,7 +908,7 @@ async function searchKeenableMCP(query, maxResults, timeRange, signal) {
908
908
  let response;
909
909
  try {
910
910
  const arguments_ = { query };
911
- // Keenable MCP 支持 published_after(相对或绝对日期)
911
+ // Keenable MCP 鏀寔 published_after锛堢浉瀵规垨缁濆鏃ユ湡锛?
912
912
  if (timeRange) {
913
913
  if (timeRange.after) arguments_.published_after = timeRange.after;
914
914
  else if (timeRange.days !== undefined) arguments_.published_after = formatKeenableRelative(timeRange.days);
@@ -935,7 +935,7 @@ async function searchKeenableMCP(query, maxResults, timeRange, signal) {
935
935
  const data = await response.json();
936
936
  if (data.error) throw new Error(`Keenable MCP error: ${data.error?.message ?? "unknown"}`);
937
937
  const content = data.result?.content ?? [];
938
- // isError=true 时 content 里是错误文本
938
+ // isError=true 鏃?content 閲屾槸閿欒鏂囨湰
939
939
  const text = content.filter((b) => b.type === "text").map((b) => b.text).join("\n");
940
940
  if (data.result?.isError) throw new Error(`Keenable MCP error: ${text.slice(0, 200)}`);
941
941
  return { sources: extractKeenableSources(text, maxResults ?? 10), truncated: false };
@@ -948,7 +948,7 @@ async function searchKeenable(query, maxResults, apiKey, timeRange, signal) {
948
948
 
949
949
  async function searchPerplexity(query, maxResults, apiKey, signal) {
950
950
  if (!apiKey) throw new Error("Perplexity search requires PERPLEXITY_API_KEY");
951
- // 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
951
+ // 鍐呯疆 20s 瓒呮椂锛堜笌澶栭儴 signal 缁勫悎锛夛細璋冪敤鏂逛笉浼?signal 鏃朵篃涓嶄細姘镐箙鍗′綇
952
952
  const response = await fetch("https://api.perplexity.ai/chat/completions", {
953
953
  method: "POST",
954
954
  redirect: "error",
@@ -984,7 +984,7 @@ async function searchPerplexity(query, maxResults, apiKey, signal) {
984
984
 
985
985
  async function searchDeepSeekOfficial(query, maxResults, apiKey, signal) {
986
986
  if (!apiKey) throw new Error("DeepSeek search requires DEEPSEEK_API_KEY");
987
- // 内置 20s 超时(与外部 signal 组合):调用方不传 signal 时也不会永久卡住
987
+ // 鍐呯疆 20s 瓒呮椂锛堜笌澶栭儴 signal 缁勫悎锛夛細璋冪敤鏂逛笉浼?signal 鏃朵篃涓嶄細姘镐箙鍗′綇
988
988
  const response = await fetch("https://api.deepseek.com/anthropic/v1/messages", {
989
989
  method: "POST",
990
990
  redirect: "error",
@@ -1131,13 +1131,13 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1131
1131
  hasUpdate: cmp > 0,
1132
1132
  updateUrl: PLUGIN_NPM_URL,
1133
1133
  repoUrl: PLUGIN_REPO_URL,
1134
- // 可一键升级:npm 真安装时 true;本地 link 开发模式 false(升级请 git pull 源码)
1134
+ // 鍙竴閿崌绾э細npm 鐪熷畨瑁呮椂 true锛涙湰鍦?link 寮€鍙戞ā寮?false锛堝崌绾ц git pull 婧愮爜锛?
1135
1135
  installable: mode !== null && !mode.isLink,
1136
1136
  installMode: mode?.isLink ? "link" : mode !== null ? "registry" : "unknown",
1137
1137
  },
1138
1138
  };
1139
1139
  },
1140
- // 一键升级:仅在 npm 真安装模式下执行 pnpm 升级;link 模式拒绝(避免破坏本地开发链路)
1140
+ // 涓€閿崌绾э細浠呭湪 npm 鐪熷畨瑁呮ā寮忎笅鎵ц pnpm 鍗囩骇锛沴ink 妯″紡鎷掔粷锛堥伩鍏嶇牬鍧忔湰鍦板紑鍙戦摼璺級
1141
1141
  async updatePlugin() {
1142
1142
  const mode = detectInstallMode();
1143
1143
  if (mode === null) {
@@ -1177,7 +1177,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1177
1177
  }
1178
1178
  const maxResults = Math.min(Math.max(Number(request.maxResults) || 5, 1), 10);
1179
1179
  const timeRange = parseTimeRange(request.timeRange);
1180
- // 指定 engine:直测该引擎本身(不走回退链),报告它自己的可用性
1180
+ // 鎸囧畾 engine锛氱洿娴嬭寮曟搸鏈韩锛堜笉璧板洖閫€閾撅級锛屾姤鍛婂畠鑷繁鐨勫彲鐢ㄦ€?
1181
1181
  if (typeof request.engine === "string" && request.engine.length > 0) {
1182
1182
  if (typeof testEngine !== "function") {
1183
1183
  return { ok: false, code: "search-unavailable", message: "engine test is not wired" };
@@ -1207,11 +1207,11 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1207
1207
  return {
1208
1208
  ok: true,
1209
1209
  value: {
1210
- // 实际使用的引擎:provider.search 在成功时返回 provider 字段
1210
+ // 瀹為檯浣跨敤鐨勫紩鎿庯細provider.search 鍦ㄦ垚鍔熸椂杩斿洖 provider 瀛楁
1211
1211
  provider: result.provider ?? request.engine ?? request.provider ?? "bing",
1212
1212
  sources: result.sources ?? [],
1213
1213
  content: result.content ?? "",
1214
- // 缓存命中标记:provider.search 成功路径标记 _cache(hit=命中缓存,miss=真实搜索)
1214
+ // 缂撳瓨鍛戒腑鏍囪锛歱rovider.search 鎴愬姛璺緞鏍囪 _cache锛坔it=鍛戒腑缂撳瓨锛宮iss=鐪熷疄鎼滅储锛?
1215
1215
  cache: result._cache === "hit" ? "hit" : "miss",
1216
1216
  },
1217
1217
  };
@@ -1243,7 +1243,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1243
1243
  }
1244
1244
  const expectedRevision = typeof body.expectedRevision === "number" ? body.expectedRevision : undefined;
1245
1245
  try {
1246
- await settings.mutate(settingsNamespace(ns), body.ops, expectedRevision);
1246
+ await settings.mutate(ns, body.ops, expectedRevision);
1247
1247
  } catch (error) {
1248
1248
  if (error instanceof SettingsConflictError) {
1249
1249
  return { ok: false, code: "settings-conflict", message: error.message };
@@ -1257,7 +1257,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1257
1257
  }
1258
1258
  return { ok: true, value: toView(descriptor) };
1259
1259
  },
1260
- // 凭据中心:查询各引擎 key 的配置状态(value 不返回,只返回是否已配置)
1260
+ // 鍑嵁涓績锛氭煡璇㈠悇寮曟搸 key 鐨勯厤缃姸鎬侊紙value 涓嶈繑鍥烇紝鍙繑鍥炴槸鍚﹀凡閰嶇疆锛?
1261
1261
  async credentialsStatus() {
1262
1262
  const credentials = getCredentials();
1263
1263
  if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
@@ -1272,7 +1272,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1272
1272
  }
1273
1273
  return { ok: true, value: { configured, available: true } };
1274
1274
  },
1275
- // 凭据中心:写入一个引擎 key(ref 白名单限定)
1275
+ // 鍑嵁涓績锛氬啓鍏ヤ竴涓紩鎿?key锛坮ef 鐧藉悕鍗曢檺瀹氾級
1276
1276
  async credentialsSet(request) {
1277
1277
  const credentials = getCredentials();
1278
1278
  if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
@@ -1289,7 +1289,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1289
1289
  return { ok: false, code: "credentials-write-failed", message: error instanceof Error ? error.message : String(error) };
1290
1290
  }
1291
1291
  },
1292
- // 凭据中心:删除一个引擎 key
1292
+ // 鍑嵁涓績锛氬垹闄や竴涓紩鎿?key
1293
1293
  async credentialsUnset(request) {
1294
1294
  const credentials = getCredentials();
1295
1295
  if (!credentials) return { ok: false, code: "credentials-unavailable", message: "credentials service is not available" };
@@ -1409,7 +1409,7 @@ function makeBridgeRoutes(settings, search, testEngine, getCredentials) {
1409
1409
  const name = "web-search-free";
1410
1410
  const inject = ["web"];
1411
1411
 
1412
- // 引擎 key 与凭据中心 ref 的映射(白名单:只允许这些 ref 被 UI 读写凭据中心)
1412
+ // 寮曟搸 key 涓庡嚟鎹腑蹇?ref 鐨勬槧灏勶紙鐧藉悕鍗曪細鍙厑璁歌繖浜?ref 琚?UI 璇诲啓鍑嵁涓績锛?
1413
1413
  const KEY_REF_MAP = {
1414
1414
  exaApiKey: "EXA_API_KEY",
1415
1415
  tavilyApiKey: "TAVILY_API_KEY",
@@ -1420,9 +1420,9 @@ const KEY_REF_MAP = {
1420
1420
 
1421
1421
  const Config = z.object({
1422
1422
  provider: z.string().default("bing"),
1423
- cache: z.boolean().default(true), // 单 query 结果缓存开关(防限流/省额度)
1424
- cacheTtl: z.number().default(5), // 缓存时长(分钟),0-5 可配置(使用处再 clamp)
1425
- keyStorage: z.string().default("credentials"), // key 存储位置:credentials(凭据中心)| settings(设置页)
1423
+ cache: z.boolean().default(true), // 鍗?query 缁撴灉缂撳瓨寮€鍏筹紙闃查檺娴?鐪侀搴︼級
1424
+ cacheTtl: z.number().default(5), // 缂撳瓨鏃堕暱锛堝垎閽燂級锛?-5 鍙厤缃紙浣跨敤澶勫啀 clamp锛?
1425
+ keyStorage: z.string().default("credentials"), // key 瀛樺偍浣嶇疆锛歝redentials锛堝嚟鎹腑蹇冿級| settings锛堣缃〉锛?
1426
1426
  lang: z.string().default("zh"),
1427
1427
  region: z.string(),
1428
1428
  bingMarket: z.string().default("zh-CN"),
@@ -1439,16 +1439,16 @@ const Config = z.object({
1439
1439
  function apply(ctx, config) {
1440
1440
  let current = () => config ?? {};
1441
1441
  const logger = ctx.logger;
1442
- // credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
1442
+ // credentials 鏈嶅姟鍙兘鏅氫簬鏈彃浠舵寕杞斤紙璺?bundle 椤哄簭锛夛紝杩愯鏈熷姩鎬佽幏鍙栬€屼笉鏄?apply 鏃剁紦瀛?
1443
1443
  const getCredentials = () => ctx.get("credentials");
1444
1444
 
1445
- // 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
1445
+ // 绯荤粺鎻愮ず璇嶅姩鎬佸埛鏂帮細璁剧疆鍙樻洿鏃堕噸鏂扮敓鎴愶紝閬垮厤鏄剧ず鏃у紩鎿?
1446
1446
  let refreshPrompt = null;
1447
1447
 
1448
- // 单 query 结果缓存(provider.search 内闭包持有):LRU 50 条 / TTL 可配置
1448
+ // 鍗?query 缁撴灉缂撳瓨锛坧rovider.search 鍐呴棴鍖呮寔鏈夛級锛歀RU 50 鏉?/ TTL 鍙厤缃?
1449
1449
  const searchCache = new Map(); // key -> { value, expiresAt }
1450
1450
 
1451
- // key 优先级:credentials(.credentials.yaml 凭据中心,官方推荐)> settings 的 free-search.<x>ApiKey(遗留兼容)> 环境变量
1451
+ // key 浼樺厛绾э細credentials锛?credentials.yaml 鍑嵁涓績锛屽畼鏂规帹鑽愶級> settings 鐨?free-search.<x>ApiKey锛堥仐鐣欏吋瀹癸級> 鐜鍙橀噺
1452
1452
  const resolveApiKey = async (envName, settingsKey) => {
1453
1453
  const credentials = getCredentials();
1454
1454
  if (credentials) {
@@ -1462,32 +1462,32 @@ function apply(ctx, config) {
1462
1462
  return process.env[envName] ?? "";
1463
1463
  };
1464
1464
 
1465
- // 总控 provider:按 settings 的 provider 字段路由到任意引擎。
1466
- // 任何引擎失败(缺 key / 401 / 限流 / 网络)都会自动轮流尝试下一个引擎,
1467
- // 直到成功或全部失败。并在结果里附带回退提示,避免 agent 搜索直接失败。
1465
+ // 鎬绘帶 provider锛氭寜 settings 鐨?provider 瀛楁璺敱鍒颁换鎰忓紩鎿庛€?
1466
+ // 浠讳綍寮曟搸澶辫触锛堢己 key / 401 / 闄愭祦 / 缃戠粶锛夐兘浼氳嚜鍔ㄨ疆娴佸皾璇曚笅涓€涓紩鎿庯紝
1467
+ // 鐩村埌鎴愬姛鎴栧叏閮ㄥけ璐ャ€傚苟鍦ㄧ粨鏋滈噷闄勫甫鍥為€€鎻愮ず锛岄伩鍏?agent 鎼滅储鐩存帴澶辫触銆?
1468
1468
  const provider = {
1469
1469
  id: "ddg",
1470
1470
  available() {
1471
1471
  return true;
1472
1472
  },
1473
- // 单 query 结果缓存:key=query+maxResults+timeRangeLabel+preferred,Map 天然 LRU
1473
+ // 鍗?query 缁撴灉缂撳瓨锛歬ey=query+maxResults+timeRangeLabel+preferred锛孧ap 澶╃劧 LRU
1474
1474
  async search(request, signal) {
1475
- // 公共咽喉校验:web_search / advanced_search / raw-search 三条路径都经过这里
1475
+ // 鍏叡鍜藉枆鏍¢獙锛歸eb_search / advanced_search / raw-search 涓夋潯璺緞閮界粡杩囪繖閲?
1476
1476
  if (request === null || typeof request !== "object" || typeof request.query !== "string" || request.query.trim().length === 0) {
1477
1477
  throw new Error("query is required");
1478
1478
  }
1479
1479
  const cfg = current();
1480
- // 首选引擎:free_search 工具显式指定(request.engine)优先于设置(cfg.provider)
1480
+ // 棣栭€夊紩鎿庯細free_search 宸ュ叿鏄惧紡鎸囧畾锛坮equest.engine锛変紭鍏堜簬璁剧疆锛坈fg.provider锛?
1481
1481
  const preferred =
1482
1482
  typeof request.engine === "string" && ALL_ENGINES.includes(request.engine)
1483
1483
  ? request.engine
1484
1484
  : cfg.provider ?? "bing";
1485
- // time_range 过滤(仅 advanced_search 工具透传;标准 web_search 无此参数)
1486
- // 保留原始字符串用于 Note 展示;raw-search 桥可能已把 timeRange 解析成对象
1485
+ // time_range 杩囨护锛堜粎 advanced_search 宸ュ叿閫忎紶锛涙爣鍑?web_search 鏃犳鍙傛暟锛?
1486
+ // 淇濈暀鍘熷瀛楃涓茬敤浜?Note 灞曠ず锛況aw-search 妗ュ彲鑳藉凡鎶?timeRange 瑙f瀽鎴愬璞?
1487
1487
  const timeRange = parseTimeRange(request.timeRange);
1488
1488
  const timeRangeLabel = typeof request.timeRange === "string" ? request.timeRange : String(timeRange?.days ?? timeRange?.after ?? "");
1489
1489
 
1490
- // 缓存 TTL(分钟,0-5 可配置);cache=false 或 ttl<=0 时完全禁用
1490
+ // 缂撳瓨 TTL锛堝垎閽燂紝0-5 鍙厤缃級锛沜ache=false 鎴?ttl<=0 鏃跺畬鍏ㄧ鐢?
1491
1491
  const cacheTtlMs = (Math.min(Math.max(Number(cfg.cacheTtl) ?? 5, 0), 5)) * 60 * 1000;
1492
1492
  const cacheEnabled = cfg.cache !== false && cacheTtlMs > 0;
1493
1493
  const cacheKey = cacheEnabled
@@ -1499,31 +1499,31 @@ function apply(ctx, config) {
1499
1499
  if (signal?.aborted) throw new Error("search aborted");
1500
1500
  searchCache.delete(cacheKey);
1501
1501
  searchCache.set(cacheKey, hit);
1502
- // 浅拷贝 + 私有标记:sources 数组也复制一层,彻底隔离缓存对象(调用方 push/改元素不影响缓存)
1502
+ // 娴呮嫹璐?+ 绉佹湁鏍囪锛歴ources 鏁扮粍涔熷鍒朵竴灞傦紝褰诲簳闅旂缂撳瓨瀵硅薄锛堣皟鐢ㄦ柟 push/鏀瑰厓绱犱笉褰卞搷缂撳瓨锛?
1503
1503
  return { ...hit.value, sources: hit.value.sources?.slice(), _cache: "hit" };
1504
1504
  }
1505
1505
  if (hit) searchCache.delete(cacheKey);
1506
1506
  }
1507
1507
 
1508
- // 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
1508
+ // 缁熶竴寮曟搸閾撅細棣栭€変紭鍏堬紝鐒跺悗鍏朵粬浠樿垂寮曟搸锛堟湁 key 鐨勪紭鍏堝皾璇曪級锛屾渶鍚庡厤璐瑰紩鎿?
1509
1509
  const paidEngines = ["exa", "tavily", "keenable", "perplexity", "deepseek-official"];
1510
1510
  const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
1511
- // 支持 time_range 过滤的引擎:tavily / exa / keenable / searxng / ddg / ddg-lite
1511
+ // 鏀寔 time_range 杩囨护鐨勫紩鎿庯細tavily / exa / keenable / searxng / ddg / ddg-lite
1512
1512
  const timeEngines = ["tavily", "exa", "keenable", "searxng", "ddg", "ddg-lite"];
1513
1513
  let chain;
1514
- // 首选引擎被跳过的原因(用于生成准确的 Note,避免误导 agent/用户):
1515
- // - "time-filter":带 timeRange 且首选引擎不支持时间过滤(根本没尝试)
1516
- // - "failed":首选引擎确实被尝试但失败(缺 key / 401 / 限流 / 0 结果 / 网络)
1517
- // - null:首选引擎成功或无回退
1514
+ // 棣栭€夊紩鎿庤璺宠繃鐨勫師鍥狅紙鐢ㄤ簬鐢熸垚鍑嗙‘鐨?Note锛岄伩鍏嶈瀵?agent/鐢ㄦ埛锛夛細
1515
+ // - "time-filter"锛氬甫 timeRange 涓旈閫夊紩鎿庝笉鏀寔鏃堕棿杩囨护锛堟牴鏈病灏濊瘯锛?
1516
+ // - "failed"锛氶閫夊紩鎿庣‘瀹炶灏濊瘯浣嗗け璐ワ紙缂?key / 401 / 闄愭祦 / 0 缁撴灉 / 缃戠粶锛?
1517
+ // - null锛氶閫夊紩鎿庢垚鍔熸垨鏃犲洖閫€
1518
1518
  let preferredSkippedReason = null;
1519
1519
  if (timeRange) {
1520
- // 有时间过滤需求时,把支持过滤的引擎排前面(首选引擎若支持仍优先)
1520
+ // 鏈夋椂闂磋繃婊ら渶姹傛椂锛屾妸鏀寔杩囨护鐨勫紩鎿庢帓鍓嶉潰锛堥閫夊紩鎿庤嫢鏀寔浠嶄紭鍏堬級
1521
1521
  const preferredFirst = [preferred].filter((e) => timeEngines.includes(e));
1522
1522
  const otherTime = timeEngines.filter((e) => e !== preferred);
1523
1523
  const noTime = [...paidEngines, ...freeEngines].filter((e) => !timeEngines.includes(e) && e !== preferred);
1524
1524
  chain = [...preferredFirst, ...otherTime, ...noTime];
1525
1525
  if (!timeEngines.includes(preferred)) {
1526
- // 首选引擎不支持时间过滤 → 它不在链里,不会被尝试(这不等于失败)
1526
+ // 棣栭€夊紩鎿庝笉鏀寔鏃堕棿杩囨护 鈫?瀹冧笉鍦ㄩ摼閲岋紝涓嶄細琚皾璇曪紙杩欎笉绛変簬澶辫触锛?
1527
1527
  preferredSkippedReason = "time-filter";
1528
1528
  }
1529
1529
  } else {
@@ -1534,9 +1534,9 @@ function apply(ctx, config) {
1534
1534
 
1535
1535
  let lastError = null;
1536
1536
  let usedEngine = null;
1537
- // 首选引擎若被尝试后失败,记录失败详情(用于 Note)
1537
+ // 棣栭€夊紩鎿庤嫢琚皾璇曞悗澶辫触锛岃褰曞け璐ヨ鎯咃紙鐢ㄤ簬 Note锛?
1538
1538
  let preferredFailure = null;
1539
- // 总超时预算:串行回退时限制整条引擎链的总时长,防止各引擎超时累加达分钟级
1539
+ // 鎬昏秴鏃堕绠楋細涓茶鍥為€€鏃堕檺鍒舵暣鏉″紩鎿庨摼鐨勬€绘椂闀匡紝闃叉鍚勫紩鎿庤秴鏃剁疮鍔犺揪鍒嗛挓绾?
1540
1540
  const BUDGET_MS = 30000;
1541
1541
  const deadline = Date.now() + BUDGET_MS;
1542
1542
  for (const engine of chain) {
@@ -1544,7 +1544,7 @@ function apply(ctx, config) {
1544
1544
  if (remaining <= 0) {
1545
1545
  throw new Error(`search timed out after ${BUDGET_MS / 1000}s`);
1546
1546
  }
1547
- // 组合外部取消 signal + 剩余预算超时:官方 web_search 取消、引擎超时、总预算都能触发
1547
+ // 缁勫悎澶栭儴鍙栨秷 signal + 鍓╀綑棰勭畻瓒呮椂锛氬畼鏂?web_search 鍙栨秷銆佸紩鎿庤秴鏃躲€佹€婚绠楅兘鑳借Е鍙?
1548
1548
  const effSignal = AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(remaining)]);
1549
1549
  try {
1550
1550
  let result;
@@ -1559,7 +1559,7 @@ function apply(ctx, config) {
1559
1559
  } else if (engine === "anysearch") {
1560
1560
  result = await searchAnysearch(request.query, request.maxResults, effSignal);
1561
1561
  } else if (engine === "exa") {
1562
- // exa:有 key 走 REST,无 key 走 keyless MCP(免费)
1562
+ // exa锛氭湁 key 璧?REST锛屾棤 key 璧?keyless MCP锛堝厤璐癸級
1563
1563
  const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
1564
1564
  if (key) {
1565
1565
  result = await searchExa(request.query, request.maxResults, key, timeRange, effSignal);
@@ -1567,11 +1567,11 @@ function apply(ctx, config) {
1567
1567
  result = await searchExaMCP(request.query, request.maxResults, effSignal);
1568
1568
  }
1569
1569
  } else if (engine === "tavily") {
1570
- // tavily:有 key 走账号档,无 key 走 keyless(免费匿名额度)
1570
+ // tavily锛氭湁 key 璧拌处鍙锋。锛屾棤 key 璧?keyless锛堝厤璐瑰尶鍚嶉搴︼級
1571
1571
  const key = await resolveApiKey("TAVILY_API_KEY", "tavilyApiKey");
1572
1572
  result = await searchTavily(request.query, request.maxResults, key, timeRange, effSignal);
1573
1573
  } else if (engine === "keenable") {
1574
- // keenable:有 key 走 REST,无 key 走 keyless MCP(免费)
1574
+ // keenable锛氭湁 key 璧?REST锛屾棤 key 璧?keyless MCP锛堝厤璐癸級
1575
1575
  const key = await resolveApiKey("KEENABLE_API_KEY", "keenableApiKey");
1576
1576
  result = await searchKeenable(request.query, request.maxResults, key, timeRange, effSignal);
1577
1577
  } else if (engine === "perplexity") {
@@ -1580,7 +1580,7 @@ function apply(ctx, config) {
1580
1580
  lastError = new Error("Perplexity requires PERPLEXITY_API_KEY");
1581
1581
  if (engine === preferred) preferredFailure = "PERPLEXITY_API_KEY is not configured";
1582
1582
  logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
1583
- continue; // 无 key 跳过
1583
+ continue; // 鏃?key 璺宠繃
1584
1584
  }
1585
1585
  result = await searchPerplexity(request.query, request.maxResults, key, effSignal);
1586
1586
  } else if (engine === "deepseek-official") {
@@ -1589,7 +1589,7 @@ function apply(ctx, config) {
1589
1589
  lastError = new Error("DeepSeek requires DEEPSEEK_API_KEY");
1590
1590
  if (engine === preferred) preferredFailure = "DEEPSEEK_API_KEY is not configured";
1591
1591
  logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
1592
- continue; // 无 key 跳过
1592
+ continue; // 鏃?key 璺宠繃
1593
1593
  }
1594
1594
  result = await searchDeepSeekOfficial(request.query, request.maxResults, key, effSignal);
1595
1595
  } else {
@@ -1598,11 +1598,11 @@ function apply(ctx, config) {
1598
1598
 
1599
1599
  if (result.sources.length > 0) {
1600
1600
  usedEngine = engine;
1601
- // 统一清洗 snippet:去登录/付费墙/订阅噪音,折叠空白(有值的才处理,保持 lossless JSON)
1601
+ // 缁熶竴娓呮礂 snippet锛氬幓鐧诲綍/浠樿垂澧?璁㈤槄鍣煶锛屾姌鍙犵┖鐧斤紙鏈夊€肩殑鎵嶅鐞嗭紝淇濇寔 lossless JSON锛?
1602
1602
  result.sources = result.sources.map((s) =>
1603
1603
  s.snippet ? { ...s, snippet: cleanSnippet(s.snippet) } : s
1604
1604
  );
1605
- // 用了非首选引擎时,在结果里附上准确提示(区分"不支持时间过滤被跳过"与"真实失败")
1605
+ // 鐢ㄤ簡闈為閫夊紩鎿庢椂锛屽湪缁撴灉閲岄檮涓婂噯纭彁绀猴紙鍖哄垎"涓嶆敮鎸佹椂闂磋繃婊よ璺宠繃"涓?鐪熷疄澶辫触"锛?
1606
1606
  if (engine !== preferred) {
1607
1607
  if (preferredSkippedReason === "time-filter") {
1608
1608
  result.content = `Note: ${preferred} does not support time filtering (timeRange=${timeRangeLabel}), using ${engine}.`;
@@ -1612,10 +1612,10 @@ function apply(ctx, config) {
1612
1612
  result.content = `Note: ${preferred} unavailable or failed, using ${engine}.`;
1613
1613
  }
1614
1614
  }
1615
- // 写入缓存(只缓存成功结果,失败走 throw 天然不缓存)
1615
+ // 鍐欏叆缂撳瓨锛堝彧缂撳瓨鎴愬姛缁撴灉锛屽け璐ヨ蛋 throw 澶╃劧涓嶇紦瀛橈級
1616
1616
  const cached = { ...result, provider: engine, engine: engine };
1617
1617
  if (cacheKey !== null) {
1618
- // fallback 条目(实际引擎≠首选)用配置 TTL 的 1/5,首选成功保持完整 TTL
1618
+ // fallback 鏉$洰锛堝疄闄呭紩鎿庘墵棣栭€夛級鐢ㄩ厤缃?TTL 鐨?1/5锛岄閫夋垚鍔熶繚鎸佸畬鏁?TTL
1619
1619
  const entryTtlMs = engine !== preferred ? Math.max(cacheTtlMs / 5, 1000) : cacheTtlMs;
1620
1620
  searchCache.set(cacheKey, {
1621
1621
  value: cached,
@@ -1642,14 +1642,16 @@ function apply(ctx, config) {
1642
1642
  },
1643
1643
  };
1644
1644
 
1645
- installSettingsSection(ctx, FREE_SEARCH_NS, Config, config ?? {}, {
1646
- setSource: (source) => {
1647
- current = source;
1648
- },
1649
- onChange: () => {
1650
- // settings 变更时刷新系统提示词(显示最新引擎)
1651
- if (typeof refreshPrompt === "function") refreshPrompt();
1652
- },
1645
+ ctx.inject(["settings"], (sctx) => {
1646
+ sctx.settings.installSection(ctx, FREE_SEARCH_NS, Config, config ?? {}, {
1647
+ setSource: (source) => {
1648
+ current = source;
1649
+ },
1650
+ onChange: () => {
1651
+ // settings 鍙樻洿鏃跺埛鏂扮郴缁熸彁绀鸿瘝锛堟樉绀烘渶鏂板紩鎿庯級
1652
+ if (typeof refreshPrompt === "function") refreshPrompt();
1653
+ },
1654
+ });
1653
1655
  });
1654
1656
 
1655
1657
  ctx.inject(["webServer", "settings"], (sctx) => {
@@ -1668,16 +1670,16 @@ function apply(ctx, config) {
1668
1670
 
1669
1671
  ctx.web.registerSearchProvider(provider);
1670
1672
 
1671
- // 运行时兜底:DSH 0.1.1+ 中 profile patch 的 config 会整体覆盖 bundle patch 的 config,
1672
- // 用户的 `- id: web` patch(如只设 fetchProvider)会静默抹掉 searchProvider,导致回退 DeepSeek 官方搜索。
1673
- // 这里在 provider 注册后检查:未指向任何 provider(undefined)时自动接管为本插件;
1674
- // 用户显式配置了其他 provider 则不动。
1673
+ // 杩愯鏃跺厹搴曪細DSH 0.1.1+ 涓?profile patch 鐨?config 浼氭暣浣撹鐩?bundle patch 鐨?config锛?
1674
+ // 鐢ㄦ埛鐨?`- id: web` patch锛堝鍙 fetchProvider锛変細闈欓粯鎶规帀 searchProvider锛屽鑷村洖閫€ DeepSeek 瀹樻柟鎼滅储銆?
1675
+ // 杩欓噷鍦?provider 娉ㄥ唽鍚庢鏌ワ細鏈寚鍚戜换浣?provider锛坲ndefined锛夋椂鑷姩鎺ョ涓烘湰鎻掍欢锛?
1676
+ // 鐢ㄦ埛鏄惧紡閰嶇疆浜嗗叾浠?provider 鍒欎笉鍔ㄣ€?
1675
1677
  if (!ctx.web.searchProviderId) {
1676
1678
  ctx.web.searchProviderId = provider.id;
1677
1679
  logger.info(`free-search: web.searchProvider was unset (patch override or missing config), taking over as "${provider.id}"`);
1678
1680
  }
1679
1681
 
1680
- // 测试工具:让 agent 逐个测试所有搜索引擎,报告可用性
1682
+ // 娴嬭瘯宸ュ叿锛氳 agent 閫愪釜娴嬭瘯鎵€鏈夋悳绱㈠紩鎿庯紝鎶ュ憡鍙敤鎬?
1681
1683
  const runEngineTest = async (engine, query, timeRange) => {
1682
1684
  const cfg = current();
1683
1685
  const q = query || "DeepSeek Harness";
@@ -1723,9 +1725,9 @@ function apply(ctx, config) {
1723
1725
  };
1724
1726
  try {
1725
1727
  const result = await attempt();
1726
- // 付费引擎无 key:直接透传失败结果
1728
+ // 浠樿垂寮曟搸鏃?key锛氱洿鎺ラ€忎紶澶辫触缁撴灉
1727
1729
  if (result.ok === false) return result;
1728
- // 免费引擎偶发反爬/空结果时重试一次
1730
+ // 鍏嶈垂寮曟搸鍋跺彂鍙嶇埇/绌虹粨鏋滄椂閲嶈瘯涓€娆?
1729
1731
  if (result.sources && result.sources.length === 0) {
1730
1732
  await new Promise((resolve) => setTimeout(resolve, 1500));
1731
1733
  return await attempt();
@@ -1813,7 +1815,7 @@ function apply(ctx, config) {
1813
1815
  return { results };
1814
1816
  },
1815
1817
  finalizeContent(exec, result) {
1816
- // 把 render 输出包装成合法的 text block(content 必须是 block 数组)
1818
+ // 鎶?render 杈撳嚭鍖呰鎴愬悎娉曠殑 text block锛坈ontent 蹇呴』鏄?block 鏁扮粍锛?
1817
1819
  const text = result.content;
1818
1820
  if (typeof text === "string" && text.length > 0) {
1819
1821
  return [{ type: "text", text }];
@@ -1828,7 +1830,7 @@ function apply(ctx, config) {
1828
1830
  }, "free-search: test engines tool");
1829
1831
  });
1830
1832
 
1831
- // 平台搜索工具:GitHub / V2EX / Bilibili / Reddit(公开 API,零依赖)
1833
+ // 骞冲彴鎼滅储宸ュ叿锛欸itHub / V2EX / Bilibili / Reddit锛堝叕寮€ API锛岄浂渚濊禆锛?
1832
1834
  ctx.inject(["tools"], (sctx) => {
1833
1835
  sctx.effect(() => {
1834
1836
  const dispose = sctx.tools.register(
@@ -1880,7 +1882,7 @@ function apply(ctx, config) {
1880
1882
  if (!PLATFORMS[platform]) {
1881
1883
  throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
1882
1884
  }
1883
- // 平台开关:settings 里禁用某平台时,工具明确告知
1885
+ // 骞冲彴寮€鍏筹細settings 閲岀鐢ㄦ煇骞冲彴鏃讹紝宸ュ叿鏄庣‘鍛婄煡
1884
1886
  const enabled = current().platforms ?? ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"];
1885
1887
  if (!enabled.includes(platform)) {
1886
1888
  throw new Error(
@@ -1889,7 +1891,7 @@ function apply(ctx, config) {
1889
1891
  }
1890
1892
  const limit = Math.min(args.maxResults ?? 5, 10);
1891
1893
  const result = await searchPlatform(platform, args.query, limit, undefined, current().lang);
1892
- // lossless JSON 不允许 undefined 字段:剔除缺失字段
1894
+ // lossless JSON 涓嶅厑璁?undefined 瀛楁锛氬墧闄ょ己澶卞瓧娈?
1893
1895
  const sources = (result.sources ?? []).map((s) => {
1894
1896
  const source = {};
1895
1897
  if (s.url !== undefined && s.url !== null && s.url !== "") source.url = s.url;
@@ -1912,8 +1914,8 @@ function apply(ctx, config) {
1912
1914
  }, "free-search: platform search tool");
1913
1915
  });
1914
1916
 
1915
- // 高级搜索工具:支持时间过滤(time_range)和指定引擎(engine)。
1916
- // 走与 web_search 相同的统一回退链,但允许 agent 显式请求"最近 N 天"的结果。
1917
+ // 楂樼骇鎼滅储宸ュ叿锛氭敮鎸佹椂闂磋繃婊わ紙time_range锛夊拰鎸囧畾寮曟搸锛坋ngine锛夈€?
1918
+ // 璧颁笌 web_search 鐩稿悓鐨勭粺涓€鍥為€€閾撅紝浣嗗厑璁?agent 鏄惧紡璇锋眰"鏈€杩?N 澶?鐨勭粨鏋溿€?
1917
1919
  ctx.inject(["tools"], (sctx) => {
1918
1920
  sctx.effect(() => {
1919
1921
  const dispose = sctx.tools.register(
@@ -1973,10 +1975,10 @@ function apply(ctx, config) {
1973
1975
  maxResults: Math.min(args.maxResults ?? 5, 10),
1974
1976
  };
1975
1977
  if (parseTimeRange(args.timeRange) !== undefined) request.timeRange = args.timeRange;
1976
- // engine 指定时:仅当该引擎可用才优先(仍走回退链,失败自动换引擎)
1978
+ // engine 鎸囧畾鏃讹細浠呭綋璇ュ紩鎿庡彲鐢ㄦ墠浼樺厛锛堜粛璧板洖閫€閾撅紝澶辫触鑷姩鎹㈠紩鎿庯級
1977
1979
  if (args.engine && ALL_ENGINES.includes(args.engine)) request.engine = args.engine;
1978
1980
  const result = await provider.search(request);
1979
- // lossless JSON 不允许 undefined 字段:按存在的值构造对象,缺字段直接省略
1981
+ // lossless JSON 涓嶅厑璁?undefined 瀛楁锛氭寜瀛樺湪鐨勫€兼瀯閫犲璞★紝缂哄瓧娈电洿鎺ョ渷鐣?
1980
1982
  return {
1981
1983
  provider: result.provider ?? result._provider ?? "bing",
1982
1984
  content: typeof result.content === "string" ? result.content : "",
@@ -2005,7 +2007,7 @@ function apply(ctx, config) {
2005
2007
  }, "free-search: advanced search tool");
2006
2008
  });
2007
2009
 
2008
- // 让 agent 知道可用搜索引擎(动态生成,随 key/设置变化)
2010
+ // 璁?agent 鐭ラ亾鍙敤鎼滅储寮曟搸锛堝姩鎬佺敓鎴愶紝闅?key/璁剧疆鍙樺寲锛?
2009
2011
  ctx.inject(["systemPrompt"], (sctx) => {
2010
2012
  let disposeSection = null;
2011
2013
  refreshPrompt = () => {
@@ -2043,7 +2045,7 @@ function apply(ctx, config) {
2043
2045
  "",
2044
2046
  "For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos, Reddit posts, Hacker News discussions, Stack Overflow questions, Wikipedia articles, npm packages), use the platform_search tool with platform: github|v2ex|bilibili|reddit|hn|stackoverflow|wikipedia|npm.",
2045
2047
  "",
2046
- "The user can switch the search engine themselves by typing /free-search-engine in the chat — it opens a picker to choose an engine, just like the settings page. This changes the preferred engine; search still falls back to other engines automatically if it fails. You should not switch engines on your own; let the user decide.",
2048
+ "The user can switch the search engine themselves by typing /free-search-engine in the chat 鈥?it opens a picker to choose an engine, just like the settings page. This changes the preferred engine; search still falls back to other engines automatically if it fails. You should not switch engines on your own; let the user decide.",
2047
2049
  ].join("\n"),
2048
2050
  });
2049
2051
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "dsh-free-search",
3
- "version": "0.4.18",
3
+ "version": "0.4.20",
4
4
  "description": "Free web search for DeepSeek Harness: 10 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable keyless) + time filtering + platform search + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",