dsh-free-search 0.9.12 → 0.9.13

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/README.md CHANGED
@@ -307,7 +307,8 @@ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa
307
307
  fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # 允许触发回退的失败类别;[] = 任何失败都立即中止(默认全部)
308
308
  lang: zh # 搜索语言池/本地化(zh / en / ru …,不再被设置页强行改写)
309
309
  # lang 决定:Bing 的 mkt/Accept-Language、Wikipedia 子域(ja → ja.wikipedia.org)、地理编码语言;
310
- # 我们自己渲染的文案只有中文/英文两套,其它语言(ja/ru/de…)一律走英文,避免回答前先塞一段中文(#92)。
310
+ # 我们自己渲染的文案只有中文/英文两套,其它语言(ja/ru/de…)一律走英文,避免回答前先塞一段中文(#92)。
311
+ # web_fetch(fs-quality-fetch)的 Accept-Language 同样跟随 lang,不再固定中文(#92)。
311
312
  uiLang: zh # 设置页界面语言(zh / en);留空则跟随 lang
312
313
  bingMarket: zh-CN # Bing 市场;留空 = 自动(不发送 mkt)
313
314
  fetchProviderTakeover: true # false = 保留你显式 pin 的 fetchProvider: http
@@ -489,7 +490,7 @@ Search engine test:
489
490
 
490
491
  #### 离线自检断言(tools/)
491
492
 
492
- 仓库自带 **24 个**零依赖断言脚本(合计 **555 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
493
+ 仓库自带 **24 个**零依赖断言脚本(合计 **558 项**),**不需要 API key、不联网、不需要测试框架**,Node ≥ 20 即可跑:
493
494
 
494
495
  | 脚本 | 覆盖 | 运行(项数) |
495
496
  |---|---|---|
@@ -509,7 +510,7 @@ Search engine test:
509
510
  | `assert-killtree-win32.mjs` | Windows 子进程树终止(#74):不再依赖 `taskkill` | `node tools/assert-killtree-win32.mjs`(8) |
510
511
  | `assert-killtree-portable.mjs` | 跨平台 `killTree` 便携版契约 | `node tools/assert-killtree-portable.mjs`(5) |
511
512
  | `assert-parallel-mcp.mjs` | Parallel MCP 引擎:25 s 超时/调用方取消覆盖**响应体读取**、共享 SSE 解析器(通知/多行帧)、6 类错误路径、REST 精确时间过滤 | `node tools/assert-parallel-mcp.mjs`(15) |
512
- | `assert-output-language.mjs` | 语言分支(#92):`searchLanguage` 支持 zh/en/ja/ru/de/es/fr/ko 其余回退 en、`outputLanguage` 只输出 zh/en、天气文案 ja→英文、置信度说明随语言、失败备注随语言 | `node tools/assert-output-language.mjs`(5) |
513
+ | `assert-output-language.mjs` | 语言分支(#92):`searchLanguage` 支持 zh/en/ja/ru/de/es/fr/ko 其余回退 en、`outputLanguage` 只输出 zh/en、天气文案 ja→英文、置信度说明随语言、失败备注随语言 | `node tools/assert-output-language.mjs`(8) |
513
514
  | `assert-uncertainty.mjs` | 输出契约:0/1 源为 low、2 源与同站为 medium、3 个不同域为 high、时间过滤无日期、失败备注、聚合站提示 | `node tools/assert-uncertainty.mjs`(8) |
514
515
  | `assert-multikey.mjs` | 多 key 字段:单 key 不变、逗号/分号/换行分隔、空白项丢弃、空输入为空数组 | `node tools/assert-multikey.mjs`(3) |
515
516
  | `assert-firecrawl-keyless.mjs` | Firecrawl keyless 出口判定:403 + VPN/proxy 文案才算拒绝、普通 403 与 401/429/500 不算、冷却初始未触发 | `node tools/assert-firecrawl-keyless.mjs`(5) |
@@ -844,7 +845,8 @@ provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa
844
845
  fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # failure classes allowed to trigger fallback; [] = abort on any failure (default: all)
845
846
  lang: zh # search language pools / localization (zh / en / ru …; no longer rewritten by the settings page)
846
847
  # lang drives Bing's mkt/Accept-Language, the Wikipedia subdomain (ja → ja.wikipedia.org) and the geocoding language;
847
- # text this plugin renders itself only exists in Chinese and English, so other languages (ja/ru/de…) get English rather than a wall of Chinese (#92).
848
+ # text this plugin renders itself only exists in Chinese and English, so other languages (ja/ru/de…) get English rather than a wall of Chinese (#92).
849
+ # web_fetch (fs-quality-fetch) sends the same Accept-Language instead of a fixed Chinese one (#92).
848
850
  uiLang: zh # settings UI language (zh / en); empty = follow lang
849
851
  bingMarket: zh-CN # Bing market; empty = automatic (no mkt sent)
850
852
  fetchProviderTakeover: true # false = keep the fetchProvider: http you pinned yourself
@@ -1026,7 +1028,7 @@ Restart `dsh web` after switching to apply changes.
1026
1028
 
1027
1029
  #### Offline self-check assertions (`tools/`)
1028
1030
 
1029
- **Twenty-four** zero-dependency assertion scripts ship with the repo (**555 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
1031
+ **Twenty-four** zero-dependency assertion scripts ship with the repo (**558 assertions** total): **no API keys, no network, no test framework**, Node ≥ 20:
1030
1032
 
1031
1033
  | Script | Covers | Run (count) |
1032
1034
  |---|---|---|
@@ -1046,7 +1048,7 @@ Restart `dsh web` after switching to apply changes.
1046
1048
  | `assert-killtree-win32.mjs` | Windows process-tree termination (#74): no longer relies on `taskkill` | `node tools/assert-killtree-win32.mjs` (8) |
1047
1049
  | `assert-killtree-portable.mjs` | portable `killTree` contract | `node tools/assert-killtree-portable.mjs` (5) |
1048
1050
  | `assert-parallel-mcp.mjs` | Parallel MCP engine: the 25 s timeout / caller abort covers the **body read**, shared SSE parser (notifications, multiline frames), six error paths, REST precise time filtering | `node tools/assert-parallel-mcp.mjs` (15) |
1049
- | `assert-output-language.mjs` | language branches (#92): `searchLanguage` passes zh/en/ja/ru/de/es/fr/ko through and falls back to en, `outputLanguage` only returns zh/en, weather text is English for ja, and both confidence and failure notes follow the language | `node tools/assert-output-language.mjs` (5) |
1051
+ | `assert-output-language.mjs` | language branches (#92): `searchLanguage` passes zh/en/ja/ru/de/es/fr/ko through and falls back to en, `outputLanguage` only returns zh/en, weather text is English for ja, and both confidence and failure notes follow the language | `node tools/assert-output-language.mjs` (8) |
1050
1052
  | `assert-uncertainty.mjs` | output contract: 0/1 source is low, two sources or a single site is medium, three distinct domains is high, time filter without dates, failure notes, aggregator caveat | `node tools/assert-uncertainty.mjs` (8) |
1051
1053
  | `assert-multikey.mjs` | multi-key fields: a single key is unchanged, comma/semicolon/newline separate keys, blank entries are dropped, empty input yields none | `node tools/assert-multikey.mjs` (3) |
1052
1054
  | `assert-firecrawl-keyless.mjs` | Firecrawl keyless egress refusal: only a 403 with the VPN/proxy wording counts, plain 403 and 401/429/500 do not, and the cooldown starts inactive | `node tools/assert-firecrawl-keyless.mjs` (5) |
package/lib/client.js CHANGED
@@ -426,7 +426,7 @@ window.__ModuleLoader__.load({
426
426
  };
427
427
  const tt = (lang) => I18N[String(lang ?? "").trim().toLowerCase() === "zh" ? "zh" : "en"];
428
428
  // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致;跟随上游 v0.7.5)
429
- const PLUGIN_VERSION = "0.9.12";
429
+ const PLUGIN_VERSION = "0.9.13";
430
430
  const PLUGIN_REPO_URL = "https://github.com/DDDMUC/dsh-free-search";
431
431
  // 右侧浮动目录(#85):条目顺序即页面顺序,anchor 是页面里对应 field 的 id。
432
432
  const NAV_ITEMS = [
@@ -274,11 +274,11 @@ export function httpsGetFollow(url, { headers = {}, timeoutMs = 20000, signal }
274
274
  }
275
275
 
276
276
  // 完整浏览器请求头(Chrome 128 形态):部分反爬站(如百度系)对精简头直接 403,完整头可通过。
277
+ // Accept-Language 不在这里:它跟随 lang(#92),由调用方逐次传入,否则日语环境抓到的仍是中文版页面。
277
278
  // 注意:声明了 Accept-Encoding 就必须解压(httpsGetFollow 已支持 gzip/br/deflate)。
278
279
  const BROWSERISH = {
279
280
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36",
280
281
  Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
281
- "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
282
282
  "Accept-Encoding": "gzip, deflate, br",
283
283
  "sec-ch-ua": '"Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"',
284
284
  "sec-ch-ua-mobile": "?0",
@@ -290,7 +290,11 @@ const BROWSERISH = {
290
290
  "Upgrade-Insecure-Requests": "1",
291
291
  };
292
292
 
293
- export function createFetchProvider({ resolveApiKey, logger }) {
293
+ /** 请求头:Accept-Language 跟随调用方(#92),其余固定为浏览器形态。 */
294
+ export function browserishHeaders(acceptLanguage = "en-US,en;q=0.9") {
295
+ return { ...BROWSERISH, "Accept-Language": String(acceptLanguage ?? "").trim() || "en-US,en;q=0.9" };
296
+ }
297
+ export function createFetchProvider({ resolveApiKey, logger, acceptLanguage = () => "en-US,en;q=0.9" }) {
294
298
  return {
295
299
  id: "fs-quality-fetch",
296
300
  /** 廉价本地可用性检查(seam 契约要求,禁止网络调用)。 */
@@ -302,7 +306,7 @@ export function createFetchProvider({ resolveApiKey, logger }) {
302
306
 
303
307
  // T1:直连(文本类内容;二进制交给后层——exa 取正文,PDF 等仍由 firecrawl 处理)
304
308
  try {
305
- const r = await httpsGetFollow(url, { headers: BROWSERISH, signal });
309
+ const r = await httpsGetFollow(url, { headers: browserishHeaders(acceptLanguage()), signal });
306
310
  if (r.status < 400 && TEXTUAL.test(r.contentType) && r.text.length >= T1_MIN_CHARS) {
307
311
  return { url: r.url, statusCode: r.status, body: { kind: "html", content: r.text }, truncated: false };
308
312
  }
package/lib/index.js CHANGED
@@ -164,6 +164,13 @@ const USER_AGENT =
164
164
  const ACCEPT_LANG = "zh-CN,zh;q=0.9,en;q=0.8";
165
165
  // 当前请求生效的 Accept-Language:由 lang(或显式 bingMarket)决定,搜索前设置(#92)。
166
166
  let activeAcceptLanguage = ACCEPT_LANG;
167
+ /** 由配置推导 Accept-Language:显式 bingMarket 优先,否则跟随 lang(未知回退 en)。 */
168
+ export function acceptLanguageFor(cfg) {
169
+ const market = cfg?.bingMarket;
170
+ if (market && MARKET_TO_LANG[market]) return MARKET_TO_LANG[market];
171
+ return LANG_PROFILES[searchLanguage(configLanguage(cfg))].acceptLang;
172
+ }
173
+
167
174
  export function setActiveAcceptLanguage(value) {
168
175
  if (typeof value === "string" && value.trim() !== "") activeAcceptLanguage = value.trim();
169
176
  }
@@ -180,6 +187,18 @@ export function searchLanguage(lang) {
180
187
  return Object.prototype.hasOwnProperty.call(LANG_PROFILES, tag) ? tag : "en";
181
188
  }
182
189
 
190
+ /**
191
+ * 配置里生效的语言标签:未设置时按 schema 默认 zh(#92)。
192
+ * searchLanguage() 只负责「未知值回退 en」,两者不要混用。
193
+ */
194
+ export function configLanguage(cfg) {
195
+ const value = cfg?.lang;
196
+ if (value === undefined || value === null) return "zh";
197
+ const tag = String(value).trim().toLowerCase();
198
+ return tag === "" ? "zh" : tag;
199
+ }
200
+
201
+
183
202
  export function outputLanguage(lang) {
184
203
  return searchLanguage(lang) === "zh" ? "zh" : "en";
185
204
  }
@@ -380,7 +399,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
380
399
  }
381
400
 
382
401
  // 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
383
- const PLUGIN_VERSION = "0.9.12";
402
+ const PLUGIN_VERSION = "0.9.13";
384
403
  // 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
385
404
  const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
386
405
  const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
@@ -742,13 +761,13 @@ function extractBingUrl(href) {
742
761
  async function searchBing(query, maxResults, options, signal) {
743
762
  // mkt:仅显式 bingMarket 才发。默认不发——mkt=zh-CN 在部分地区/线路会返回空 SERP 容器
744
763
  // (#71:海外 IP + mkt=zh-CN → b_algo=0),Bing 会按 Accept-Language 自行本地化。
745
- const profile = LANG_PROFILES[options?.lang] ?? LANG_PROFILES.zh;
764
+ const profile = LANG_PROFILES[searchLanguage(configLanguage(options))];
746
765
  const params = new URLSearchParams({ q: query });
747
766
  if (options?.bingMarket) params.set("mkt", options.bingMarket);
748
767
  // Accept-Language:显式 bingMarket 时按 market 反推(避免中文 header 污染),否则用 lang 档案
749
768
  const acceptLang = options?.bingMarket
750
- ? (MARKET_TO_LANG[options.bingMarket] ?? ACCEPT_LANG)
751
- : (profile.acceptLang ?? ACCEPT_LANG);
769
+ ? (MARKET_TO_LANG[options.bingMarket] ?? activeAcceptLanguage)
770
+ : (profile.acceptLang ?? activeAcceptLanguage);
752
771
  const adlt = options?.safeSearch ?? "off";
753
772
  if (adlt === "off") params.set("adlt", "off");
754
773
  else if (adlt === "moderate") params.set("adlt", "moderate");
@@ -1383,7 +1402,7 @@ function sliceBalancedObject(text, start) {
1383
1402
  // json 字段 murl(真实播放地址) / vt(标题) / du(时长) / purl(详情页)(字段语义以实测为准)。
1384
1403
  async function searchBingVideos(query, maxResults, options, signal) {
1385
1404
  const params = new URLSearchParams({ q: query, count: String(Math.max(Number(maxResults) || 10, 1)), first: "1", mmasync: "1" });
1386
- const market = options?.bingMarket ?? (LANG_PROFILES[options?.lang] ?? LANG_PROFILES.zh).market;
1405
+ const market = options?.bingMarket ?? LANG_PROFILES[searchLanguage(configLanguage(options))].market;
1387
1406
  if (market) params.set("mkt", market);
1388
1407
  const html = await fetchVideoHtml(`${BING_VIDEOS_ASYNC_URL}?${params}`, signal);
1389
1408
  if (/captcha|unusual traffic|robot check/i.test(html.slice(0, 5000))) {
@@ -3822,10 +3841,8 @@ function apply(ctx, config) {
3822
3841
  // #72:本次请求生效的域名黑名单(模块级活动值;各结果咽喉点过滤读取它)
3823
3842
  setActiveBlockedDomains(cfg.blockedDomains);
3824
3843
  // #92:Accept-Language 跟随 lang(显式 bingMarket 优先),不再固定中文
3825
- setActiveAcceptLanguage(
3826
- (cfg.bingMarket && MARKET_TO_LANG[cfg.bingMarket]) || LANG_PROFILES[searchLanguage(cfg.lang)].acceptLang
3827
- );
3828
- setActiveOutputLanguage(outputLanguage(cfg.lang));
3844
+ setActiveAcceptLanguage(acceptLanguageFor(cfg));
3845
+ setActiveOutputLanguage(outputLanguage(configLanguage(cfg)));
3829
3846
  const disabledEngines = normalizeDisabledEngines(cfg.disabledEngines);
3830
3847
  const fallbackOnSet = new Set(normalizeFallbackOn(cfg.fallbackOn));
3831
3848
  const disabledSet = new Set(disabledEngines);
@@ -4371,7 +4388,12 @@ function apply(ctx, config) {
4371
4388
  // web_fetch 质量链接管:注册 fs-quality-fetch(直连 → exa contents → firecrawl → crawl4ai),
4372
4389
  // 未设置 fetchProviderId、或仍是出厂默认 http 时接管;已被显式指向其他 provider 时不抢占。
4373
4390
  // 与 web_search 的接管规则同构:运行时兜底,不依赖 bundle patch 是否落地。
4374
- const fetchProvider = createFetchProvider({ resolveApiKey, logger });
4391
+ const fetchProvider = createFetchProvider({
4392
+ resolveApiKey,
4393
+ logger,
4394
+ // #92:web_fetch 不经过 searchOnce,所以这里按当前配置现算,而不是复用上次搜索留下的值。
4395
+ acceptLanguage: () => acceptLanguageFor(current()),
4396
+ });
4375
4397
  registerOnce(() => ctx.web.registerFetchProvider(fetchProvider), fetchProvider, "fetch provider");
4376
4398
 
4377
4399
  // 接管必须先等 settings 服务就绪:rc.2 起 provider 选择是 config 的 volatile ref,
@@ -5049,7 +5071,7 @@ function apply(ctx, config) {
5049
5071
  );
5050
5072
  }
5051
5073
  const limit = Math.min(args.maxResults ?? 5, 10);
5052
- let result = await searchPlatform(platform, args.query, limit, undefined, current().lang);
5074
+ let result = await searchPlatform(platform, args.query, limit, undefined, configLanguage(current()));
5053
5075
  if (platform === "vimeo" && (result.sources ?? []).length === 0) {
5054
5076
  // Vimeo 搜索页有时不返回结果:退回网页搜索(site: 过滤)
5055
5077
  try {
@@ -5194,7 +5216,7 @@ function apply(ctx, config) {
5194
5216
  output: {
5195
5217
  schema: WEATHER_OUTPUT_SCHEMA,
5196
5218
  render(args, value) {
5197
- const lang = outputLanguage(current().lang);
5219
+ const lang = outputLanguage(configLanguage(current()));
5198
5220
  const c = value.current ?? {};
5199
5221
  const lines = [];
5200
5222
  lines.push(lang === "en" ? `Weather for ${value.location}:` : `${value.location} 天气:`);
@@ -5215,7 +5237,7 @@ function apply(ctx, config) {
5215
5237
  },
5216
5238
  async execute(args) {
5217
5239
  // 天气正文只提供 zh/en(回退 en),地理编码用真实搜索语言(ja 就查日文地名)。
5218
- const lang = searchLanguage(current().lang);
5240
+ const lang = searchLanguage(configLanguage(current()));
5219
5241
  const location = String(args.location ?? "").trim();
5220
5242
  if (!location) throw new Error("weather: location is required");
5221
5243
  return sanitizeWeatherResult(await fetchWeather(location, args.days, lang, undefined));
@@ -5324,7 +5346,7 @@ function apply(ctx, config) {
5324
5346
  sources: result.sources ?? [],
5325
5347
  timeRange: Boolean(request.timeRange),
5326
5348
  note: typeof result.content === "string" ? result.content : "",
5327
- lang: current().lang,
5349
+ lang: configLanguage(current()),
5328
5350
  }),
5329
5351
  },
5330
5352
  };
@@ -5424,7 +5446,7 @@ function apply(ctx, config) {
5424
5446
  uncertainty: assessUncertainty({
5425
5447
  sources: toolResult.sources ?? [],
5426
5448
  note: typeof toolResult.content === "string" ? toolResult.content : "",
5427
- lang: current().lang,
5449
+ lang: configLanguage(current()),
5428
5450
  }),
5429
5451
  },
5430
5452
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-free-search",
3
- "version": "0.9.12",
3
+ "version": "0.9.13",
4
4
  "description": "Free web search for DeepSeek Harness: 27 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa/Tavily/Keenable/Firecrawl/Parallel keyless; Zhihu global/site via MCP and Perplexity/SerpBase/DeepSeek (official + native tools/search)/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 + domain blocklist (blockedDomains) + web_fetch, with web settings UI.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",