dsh-free-search 0.9.13 → 0.9.14
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 +22 -20
- package/lib/client.js +1 -1
- package/lib/index.js +22 -37
- package/lib/weather.js +214 -183
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
32
32
|
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、AnySearch、Exa、Tavily、Keenable、Firecrawl、Parallel、Perplexity、SerpBase、Serply、DeepSeek 官方、DeepSeek 原生搜索 API、You.com、百度千帆、Kimi、阿里云百炼、火山联网搜索(豆包搜索)、知乎全网/站内(`zhihu_global`/`zhihu_site`,经知乎开放平台 MCP)、OpenAI / Gemini / Claude 模型内置搜索
|
|
33
33
|
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")+ 中英文切换;入口在左侧「插件」页的组件行配置(`plugins.row.config`,DSH 0.1.7-rc.1+)。远程访问(VPS / 反向代理 / 自定义域名)下也能用:桥接接口沿用宿主的 Host 围栏,`--trusted-host` 与 `--public-url` 声明的地址会放行(#84)
|
|
34
34
|
- **右侧浮动目录** —— 卡片右侧的页面留白里浮出「引擎 / 回退 / 语言池 / 搜索行为 / 抓取 / 性能」六个快捷跳转,滚动自动高亮当前项、点击平滑跳转;不占用正文宽度(正文布局与 0.9.6 一致),右侧留白不足时自动隐藏
|
|
35
|
-
- **多 key 轮换** —— 任何 key 字段(或对应环境变量)都可以用逗号 / 分号 / 换行分隔多把 key;遇到 401/402/429(鉴权失败、额度耗尽)先轮换同一引擎的下一把,再走引擎回退
|
|
35
|
+
- **多 key 轮换** —— 任何 key 字段(或对应环境变量)都可以用逗号 / 分号 / 换行分隔多把 key;遇到 401/402/429(鉴权失败、额度耗尽)先轮换同一引擎的下一把,再走引擎回退
|
|
36
36
|
- **弹出式切换命令** —— 聊天框输入 `/free-search-engine`,弹出引擎选择窗口,点选即切换(等效设置页 + 保存)
|
|
37
37
|
- **引擎测试** —— `free_search_test` 工具让 agent 一键测试所有引擎;设置页也有"测试引擎"按钮(直测当前引擎,不走回退链,付费引擎无 key 会明确报错)
|
|
38
38
|
- **全局引擎开关与回退优先级** —— 设置页可勾选/取消引擎(取消后全局生效:普通搜索 / Auto / `advanced_search` / `multi_search` / 引擎测试都不再用它),并可用 ↑↓ 调整全局回退顺序(一键恢复默认)
|
|
@@ -305,9 +305,10 @@ DSH 0.1.7-rc.1 起,配置跟随 profile 的插件条目保存:设置页与 `
|
|
|
305
305
|
# profiles/<profile>/cordis.patch.yml 中该条目的 config:
|
|
306
306
|
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / deepseek-tools / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
307
307
|
fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # 允许触发回退的失败类别;[] = 任何失败都立即中止(默认全部)
|
|
308
|
-
lang: zh # 搜索语言池/本地化(zh / en / ru …,不再被设置页强行改写)
|
|
309
|
-
# lang 决定:Bing 的 mkt/Accept-Language、Wikipedia 子域(ja → ja.wikipedia.org)、地理编码语言;
|
|
310
|
-
# 我们自己渲染的文案只有中文/英文两套,其它语言(ja/ru/de…)一律走英文,避免回答前先塞一段中文(#92
|
|
308
|
+
lang: zh # 搜索语言池/本地化(zh / en / ru …,不再被设置页强行改写)
|
|
309
|
+
# lang 决定:Bing 的 mkt/Accept-Language、Wikipedia 子域(ja → ja.wikipedia.org)、地理编码语言;
|
|
310
|
+
# 我们自己渲染的文案只有中文/英文两套,其它语言(ja/ru/de…)一律走英文,避免回答前先塞一段中文(#92);
|
|
311
|
+
# weather 例外,另有日文(ja 得到日文天气,ru/de… 仍走英文)。
|
|
311
312
|
# web_fetch(fs-quality-fetch)的 Accept-Language 同样跟随 lang,不再固定中文(#92)。
|
|
312
313
|
uiLang: zh # 设置页界面语言(zh / en);留空则跟随 lang
|
|
313
314
|
bingMarket: zh-CN # Bing 市场;留空 = 自动(不发送 mkt)
|
|
@@ -510,12 +511,12 @@ Search engine test:
|
|
|
510
511
|
| `assert-killtree-win32.mjs` | Windows 子进程树终止(#74):不再依赖 `taskkill` | `node tools/assert-killtree-win32.mjs`(8) |
|
|
511
512
|
| `assert-killtree-portable.mjs` | 跨平台 `killTree` 便携版契约 | `node tools/assert-killtree-portable.mjs`(5) |
|
|
512
513
|
| `assert-parallel-mcp.mjs` | Parallel MCP 引擎:25 s 超时/调用方取消覆盖**响应体读取**、共享 SSE 解析器(通知/多行帧)、6 类错误路径、REST 精确时间过滤 | `node tools/assert-parallel-mcp.mjs`(15) |
|
|
513
|
-
| `assert-output-language.mjs` | 语言分支(#92):`searchLanguage` 支持 zh/en/ja/ru/de/es/fr/ko 其余回退 en、`outputLanguage` 只输出 zh/en、天气文案 ja
|
|
514
|
-
| `assert-uncertainty.mjs` | 输出契约:0/1 源为 low、2 源与同站为 medium、3 个不同域为 high、时间过滤无日期、失败备注、聚合站提示 | `node tools/assert-uncertainty.mjs`(8) |
|
|
515
|
-
| `assert-multikey.mjs` | 多 key 字段:单 key 不变、逗号/分号/换行分隔、空白项丢弃、空输入为空数组 | `node tools/assert-multikey.mjs`(3) |
|
|
516
|
-
| `assert-firecrawl-keyless.mjs` | Firecrawl keyless 出口判定:403 + VPN/proxy 文案才算拒绝、普通 403 与 401/429/500 不算、冷却初始未触发 | `node tools/assert-firecrawl-keyless.mjs`(5) |
|
|
517
|
-
| `assert-fetch-charset.mjs` | 抓取编码(#88):`charset=gbk` / `gb2312` 按声明解码、无声明时嗅探 `<meta charset>` 与 http-equiv 形式、默认 UTF-8、未知 label 兜底 UTF-8 不抛错 | `node tools/assert-fetch-charset.mjs`(6) |
|
|
518
|
-
| `assert-fetch-abort.mjs` | 抓取中断(#87):压缩响应中途断连必须 reject(不再永久 pending)、明文断连同样 reject、abort 能打断停滞的 gzip 流、complete 响应与重定向仍正常 | `node tools/assert-fetch-abort.mjs`(6) |
|
|
514
|
+
| `assert-output-language.mjs` | 语言分支(#92):`searchLanguage` 支持 zh/en/ja/ru/de/es/fr/ko 其余回退 en、`outputLanguage` 只输出 zh/en、天气文案 zh/en/ja(其它回退英文,含 WMO 描述与正文)、置信度说明随语言、失败备注随语言 | `node tools/assert-output-language.mjs`(10) |
|
|
515
|
+
| `assert-uncertainty.mjs` | 输出契约:0/1 源为 low、2 源与同站为 medium、3 个不同域为 high、时间过滤无日期、失败备注、聚合站提示 | `node tools/assert-uncertainty.mjs`(8) |
|
|
516
|
+
| `assert-multikey.mjs` | 多 key 字段:单 key 不变、逗号/分号/换行分隔、空白项丢弃、空输入为空数组 | `node tools/assert-multikey.mjs`(3) |
|
|
517
|
+
| `assert-firecrawl-keyless.mjs` | Firecrawl keyless 出口判定:403 + VPN/proxy 文案才算拒绝、普通 403 与 401/429/500 不算、冷却初始未触发 | `node tools/assert-firecrawl-keyless.mjs`(5) |
|
|
518
|
+
| `assert-fetch-charset.mjs` | 抓取编码(#88):`charset=gbk` / `gb2312` 按声明解码、无声明时嗅探 `<meta charset>` 与 http-equiv 形式、默认 UTF-8、未知 label 兜底 UTF-8 不抛错 | `node tools/assert-fetch-charset.mjs`(6) |
|
|
519
|
+
| `assert-fetch-abort.mjs` | 抓取中断(#87):压缩响应中途断连必须 reject(不再永久 pending)、明文断连同样 reject、abort 能打断停滞的 gzip 流、complete 响应与重定向仍正常 | `node tools/assert-fetch-abort.mjs`(6) |
|
|
519
520
|
| `assert-bridge-access.mjs` | 桥接 Host 围栏(#84):loopback 放行、remote peer 打 loopback Host 拒绝、`--trusted-host`(无端口匹配任意端口 / 带端口精确匹配)、`--public-url`、绑定地址、跨站与异源拒绝 | `node tools/assert-bridge-access.mjs`(11) |
|
|
520
521
|
| `assert-provider-takeover.mjs` | provider 接管:旧属性 API 与 rc.2 的 `config.*` ref 两套兼容、`settings.mutate(web)` 单批写入、显式指定的 provider 不被抢占、`fetchProviderTakeover: false` 时保留显式 `http`、describe 噪声不误报、嵌套 config 变化与 web 重建分别成行、关闭 DSH 搜索开关后重新接管且不抢 `deepseek-search-api`、apply 重入/重复注册不再中断、重复日志去重 | `node tools/assert-provider-takeover.mjs`(16) |
|
|
521
522
|
|
|
@@ -569,7 +570,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
569
570
|
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Tavily, Keenable, Firecrawl, Parallel, Perplexity, SerpBase, Serply, DeepSeek Official, You.com, Baidu Qianfan, Kimi, Aliyun Bailian, Volcano Doubao, Zhihu web-wide/site search (`zhihu_global`/`zhihu_site`, via the Zhihu open-platform MCP), and the model-based OpenAI / Gemini / Claude searches
|
|
570
571
|
- **Web Settings UI** — Engine switching, API key configuration (keys masked as "configured" in the UI), and a Chinese/English toggle; the entry is the component-row config (`plugins.row.config`) on the sidebar Plugins page (DSH 0.1.7-rc.1+). Remote access (VPS, reverse proxy, custom domain) works too: the bridge reuses the host's own Host fence, so addresses declared with `--trusted-host` or `--public-url` are accepted (#84)
|
|
571
572
|
- **Right-hand section rail** — A floating list (Engines / Fallback / Language pools / Search behaviour / Fetch / Performance) sits in the page margin to the right of the card: it highlights the section you are in while scrolling and jumps smoothly on click. It is rendered in a portal and never narrows the card, so the body layout is unchanged; it hides itself when the margin is too tight.
|
|
572
|
-
- **Multi-key rotation** — Any key field (or its env var) accepts several keys separated by commas, semicolons or newlines; on 401/402/429 (bad key, quota exhausted) the same engine retries with the next key before the engine-level fallback runs
|
|
573
|
+
- **Multi-key rotation** — Any key field (or its env var) accepts several keys separated by commas, semicolons or newlines; on 401/402/429 (bad key, quota exhausted) the same engine retries with the next key before the engine-level fallback runs
|
|
573
574
|
- **Popup Switch Command** — Type `/free-search-engine` in the chat: a picker opens with all engines; click one to switch (equivalent to the settings page + save)
|
|
574
575
|
- **Engine Testing** — `free_search_test` for the agent to check all engines in one call; the settings UI also has a "Test engine" button that tests the selected engine directly (no fallback chain; paid engines without a key report an explicit error)
|
|
575
576
|
- **Global engine enable/disable & fallback priority** — uncheck engines you never want (applies everywhere: web_search fallback, Auto routing, advanced_search, multi_search, engine tests) and reorder the global fallback chain with ↑↓ (one-click reset)
|
|
@@ -843,9 +844,10 @@ The old `free-search:` section of `~/.dsh/settings.yaml` is **not** imported by
|
|
|
843
844
|
# config of that entry in profiles/<profile>/cordis.patch.yml:
|
|
844
845
|
provider: bing # ddg / ddg-lite / bing / searxng / anysearch / exa / tavily / keenable / firecrawl / parallel / perplexity / serpbase / serply / deepseek-official / deepseek-tools / you / baidu / kimi / aliyun / doubao / zhihu_global / zhihu_site / openai / gemini / claude / auto / multi
|
|
845
846
|
fallbackOn: [quota, auth, bot-wall, transient, invalid-response, unknown] # failure classes allowed to trigger fallback; [] = abort on any failure (default: all)
|
|
846
|
-
lang: zh # search language pools / localization (zh / en / ru …; no longer rewritten by the settings page)
|
|
847
|
-
# lang drives Bing's mkt/Accept-Language, the Wikipedia subdomain (ja → ja.wikipedia.org) and the geocoding language;
|
|
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)
|
|
847
|
+
lang: zh # search language pools / localization (zh / en / ru …; no longer rewritten by the settings page)
|
|
848
|
+
# lang drives Bing's mkt/Accept-Language, the Wikipedia subdomain (ja → ja.wikipedia.org) and the geocoding language;
|
|
849
|
+
# 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);
|
|
850
|
+
# weather also has Japanese (ja gets Japanese weather text, ru/de… still get English).
|
|
849
851
|
# web_fetch (fs-quality-fetch) sends the same Accept-Language instead of a fixed Chinese one (#92).
|
|
850
852
|
uiLang: zh # settings UI language (zh / en); empty = follow lang
|
|
851
853
|
bingMarket: zh-CN # Bing market; empty = automatic (no mkt sent)
|
|
@@ -1048,12 +1050,12 @@ Restart `dsh web` after switching to apply changes.
|
|
|
1048
1050
|
| `assert-killtree-win32.mjs` | Windows process-tree termination (#74): no longer relies on `taskkill` | `node tools/assert-killtree-win32.mjs` (8) |
|
|
1049
1051
|
| `assert-killtree-portable.mjs` | portable `killTree` contract | `node tools/assert-killtree-portable.mjs` (5) |
|
|
1050
1052
|
| `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) |
|
|
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
|
|
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) |
|
|
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) |
|
|
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) |
|
|
1055
|
-
| `assert-fetch-charset.mjs` | fetch encoding (#88): `charset=gbk` / `gb2312` decoded as declared, `<meta charset>` and the http-equiv form sniffed when the header omits it, UTF-8 by default, and an unknown label falls back to UTF-8 instead of throwing | `node tools/assert-fetch-charset.mjs` (6) |
|
|
1056
|
-
| `assert-fetch-abort.mjs` | fetch interruption (#87): a compressed response cut mid-body rejects instead of hanging forever, identity truncation rejects too, abort breaks a stalled gzip stream, and complete responses plus redirects still work | `node tools/assert-fetch-abort.mjs` (6) |
|
|
1053
|
+
| `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 has zh/en/ja (WMO descriptions and body; others fall back to English), and both confidence and failure notes follow the language | `node tools/assert-output-language.mjs` (10) |
|
|
1054
|
+
| `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) |
|
|
1055
|
+
| `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) |
|
|
1056
|
+
| `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) |
|
|
1057
|
+
| `assert-fetch-charset.mjs` | fetch encoding (#88): `charset=gbk` / `gb2312` decoded as declared, `<meta charset>` and the http-equiv form sniffed when the header omits it, UTF-8 by default, and an unknown label falls back to UTF-8 instead of throwing | `node tools/assert-fetch-charset.mjs` (6) |
|
|
1058
|
+
| `assert-fetch-abort.mjs` | fetch interruption (#87): a compressed response cut mid-body rejects instead of hanging forever, identity truncation rejects too, abort breaks a stalled gzip stream, and complete responses plus redirects still work | `node tools/assert-fetch-abort.mjs` (6) |
|
|
1057
1059
|
| `assert-bridge-access.mjs` | bridge Host fence (#84): loopback allowed, a remote peer behind a loopback Host refused, `--trusted-host` (port-less matches any port, explicit port matches exactly), `--public-url`, the bind address, cross-site and foreign-Origin refused | `node tools/assert-bridge-access.mjs` (11) |
|
|
1058
1060
|
| `assert-provider-takeover.mjs` | provider takeover: both the legacy property API and the rc.2 `config.*` refs, a single batched `settings.mutate(web)`, explicit foreign providers left untouched, an explicit `http` kept when `fetchProviderTakeover` is off, describe()-triggered events ignored when nothing changed, nested config changes and web rebuilds reported separately, re-takeover after DSH's search switch is turned off without fighting an explicit `deepseek-search-api`, apply re-entry / duplicate registrations tolerated, and repeated logs deduplicated | `node tools/assert-provider-takeover.mjs` (16) |
|
|
1059
1061
|
|
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.
|
|
429
|
+
const PLUGIN_VERSION = "0.9.14";
|
|
430
430
|
const PLUGIN_REPO_URL = "https://github.com/DDDMUC/dsh-free-search";
|
|
431
431
|
// 右侧浮动目录(#85):条目顺序即页面顺序,anchor 是页面里对应 field 的 id。
|
|
432
432
|
const NAV_ITEMS = [
|
package/lib/index.js
CHANGED
|
@@ -9,7 +9,7 @@ import { mcpCall, mcpContentText, parseMcpResponseText } from "./mcp_adapter.js"
|
|
|
9
9
|
import { registerResearchTool } from "./research.js";
|
|
10
10
|
import { registerDomainSearchTool } from "./vertical.js";
|
|
11
11
|
import { createFetchProvider } from "./fetch_provider.js";
|
|
12
|
-
import { fetchWeather, sanitizeWeatherResult, WEATHER_OUTPUT_SCHEMA } from "./weather.js";
|
|
12
|
+
import { fetchWeather, formatWeather, sanitizeWeatherResult, WEATHER_OUTPUT_SCHEMA } from "./weather.js";
|
|
13
13
|
import { setActiveBlockedDomains, getActiveBlockedDomains, isDomainBlocked, applyBlockedFilterToResult } from "./domain_filter.js";
|
|
14
14
|
import { stripTrackingParams, normalizeUrlForDedupe } from "./url_norm.js";
|
|
15
15
|
import { selectEngines, planTopUp, resolveMinSources, describeTarget, credentialRequirement, partitionByCredentials, rankMergedResults, domainOf } from "./multi_policy.js";
|
|
@@ -399,7 +399,7 @@ function routeEngines(query, { timeRange, disabledEngines = [], fallbackOrder =
|
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
// 当前插件版本(发布时与 package.json 同步;跟随上游 v0.7.5,不在 PR 里 bump)
|
|
402
|
-
const PLUGIN_VERSION = "0.9.
|
|
402
|
+
const PLUGIN_VERSION = "0.9.14";
|
|
403
403
|
// 检查更新的 npm registry 元数据地址(dsh-free-search 是 npmjs 上的公开包)
|
|
404
404
|
const NPM_REGISTRY_URL = "https://registry.npmjs.org/dsh-free-search/latest";
|
|
405
405
|
const PLUGIN_NPM_URL = "https://www.npmjs.com/package/dsh-free-search";
|
|
@@ -3699,23 +3699,23 @@ function apply(ctx, config) {
|
|
|
3699
3699
|
// credentials 服务可能晚于本插件挂载(跨 bundle 顺序),运行期动态获取而不是 apply 时缓存
|
|
3700
3700
|
const getCredentials = () => ctx.get("credentials");
|
|
3701
3701
|
|
|
3702
|
-
// #84:读取宿主的远程访问信任集合(--trusted-host / --public-url),供桥接 Host 围栏使用。
|
|
3703
|
-
// webStartup 只在 dsh web 下提供;拿不到就维持 loopback-only 的老行为。
|
|
3704
|
-
ctx.inject(["webStartup"], (sctx) => {
|
|
3705
|
-
try {
|
|
3706
|
-
const startup = sctx.get("webStartup");
|
|
3707
|
-
if (!startup) return;
|
|
3708
|
-
if (Array.isArray(startup.trustedHosts)) {
|
|
3709
|
-
bridgeTrust.hosts = startup.trustedHosts
|
|
3710
|
-
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
3711
|
-
.map((entry) => entry.trim());
|
|
3712
|
-
}
|
|
3713
|
-
if (typeof startup.publicUrl === "string" && startup.publicUrl) {
|
|
3714
|
-
try {
|
|
3715
|
-
bridgeTrust.publicHost = new URL(startup.publicUrl).host;
|
|
3716
|
-
} catch {}
|
|
3717
|
-
}
|
|
3718
|
-
} catch {}
|
|
3702
|
+
// #84:读取宿主的远程访问信任集合(--trusted-host / --public-url),供桥接 Host 围栏使用。
|
|
3703
|
+
// webStartup 只在 dsh web 下提供;拿不到就维持 loopback-only 的老行为。
|
|
3704
|
+
ctx.inject(["webStartup"], (sctx) => {
|
|
3705
|
+
try {
|
|
3706
|
+
const startup = sctx.get("webStartup");
|
|
3707
|
+
if (!startup) return;
|
|
3708
|
+
if (Array.isArray(startup.trustedHosts)) {
|
|
3709
|
+
bridgeTrust.hosts = startup.trustedHosts
|
|
3710
|
+
.filter((entry) => typeof entry === "string" && entry.trim())
|
|
3711
|
+
.map((entry) => entry.trim());
|
|
3712
|
+
}
|
|
3713
|
+
if (typeof startup.publicUrl === "string" && startup.publicUrl) {
|
|
3714
|
+
try {
|
|
3715
|
+
bridgeTrust.publicHost = new URL(startup.publicUrl).host;
|
|
3716
|
+
} catch {}
|
|
3717
|
+
}
|
|
3718
|
+
} catch {}
|
|
3719
3719
|
});
|
|
3720
3720
|
|
|
3721
3721
|
// 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
|
|
@@ -5216,27 +5216,12 @@ function apply(ctx, config) {
|
|
|
5216
5216
|
output: {
|
|
5217
5217
|
schema: WEATHER_OUTPUT_SCHEMA,
|
|
5218
5218
|
render(args, value) {
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
const lines = [];
|
|
5222
|
-
lines.push(lang === "en" ? `Weather for ${value.location}:` : `${value.location} 天气:`);
|
|
5223
|
-
lines.push(
|
|
5224
|
-
lang === "en"
|
|
5225
|
-
? `Now: ${c.temperature}°C (feels ${c.apparent}°C), ${c.text}, humidity ${c.humidity}%, wind ${c.wind} km/h`
|
|
5226
|
-
: `当前:${c.temperature}°C(体感 ${c.apparent}°C),${c.text},湿度 ${c.humidity}%,风速 ${c.wind} km/h`
|
|
5227
|
-
);
|
|
5228
|
-
for (const d of value.daily ?? []) {
|
|
5229
|
-
lines.push(
|
|
5230
|
-
lang === "en"
|
|
5231
|
-
? `${d.date}: ${d.min}~${d.max}°C, ${d.text}${d.precipitationProbability !== undefined ? `, precip ${d.precipitationProbability}%` : ""}`
|
|
5232
|
-
: `${d.date}:${d.min}~${d.max}°C,${d.text}${d.precipitationProbability !== undefined ? `,降水概率 ${d.precipitationProbability}%` : ""}`
|
|
5233
|
-
);
|
|
5234
|
-
}
|
|
5235
|
-
return [{ type: "text", text: wrapUntrustedBlock(lines.join("\n")) }];
|
|
5219
|
+
// 正文 zh / en / ja 三套(其它语言回退 en),与 fetchWeather 生成的天气描述同一语言
|
|
5220
|
+
return [{ type: "text", text: wrapUntrustedBlock(formatWeather(value, configLanguage(current()))) }];
|
|
5236
5221
|
},
|
|
5237
5222
|
},
|
|
5238
5223
|
async execute(args) {
|
|
5239
|
-
//
|
|
5224
|
+
// 天气正文 zh/en/ja(其它回退 en,见 textLanguage),地理编码用真实搜索语言(ja 就查日文地名)。
|
|
5240
5225
|
const lang = searchLanguage(configLanguage(current()));
|
|
5241
5226
|
const location = String(args.location ?? "").trim();
|
|
5242
5227
|
if (!location) throw new Error("weather: location is required");
|
package/lib/weather.js
CHANGED
|
@@ -1,183 +1,214 @@
|
|
|
1
|
-
// Weather(Open-Meteo:geocoding + forecast,全部免 key)
|
|
2
|
-
// 与 lib/index.js 的 weather 工具配套。这里同时导出 output schema 与 sanitizeWeatherResult(),
|
|
3
|
-
// 使 tools/assert-weather-contract.mjs 能离线校验「返回值 ↔ schema」逐字段对齐——
|
|
4
|
-
// 0.8.4/0.8.5 曾因 schema 漏声明 latitude/longitude/timezone/current.code/daily[].code
|
|
5
|
-
// 导致 additionalProperties:false 校验失败、工具 100% 不可用(issue #73)。
|
|
6
|
-
|
|
7
|
-
const OPEN_METEO_GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
|
|
8
|
-
const OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast";
|
|
9
|
-
|
|
10
|
-
// WMO weather code →
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
const
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
1
|
+
// Weather(Open-Meteo:geocoding + forecast,全部免 key)
|
|
2
|
+
// 与 lib/index.js 的 weather 工具配套。这里同时导出 output schema 与 sanitizeWeatherResult(),
|
|
3
|
+
// 使 tools/assert-weather-contract.mjs 能离线校验「返回值 ↔ schema」逐字段对齐——
|
|
4
|
+
// 0.8.4/0.8.5 曾因 schema 漏声明 latitude/longitude/timezone/current.code/daily[].code
|
|
5
|
+
// 导致 additionalProperties:false 校验失败、工具 100% 不可用(issue #73)。
|
|
6
|
+
|
|
7
|
+
const OPEN_METEO_GEOCODING_URL = "https://geocoding-api.open-meteo.com/v1/search";
|
|
8
|
+
const OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast";
|
|
9
|
+
|
|
10
|
+
// WMO weather code → 描述(中文/英文/日文)。
|
|
11
|
+
// 日文按日本气象厅「国際式の天気記号」的 ww 表(与 Open-Meteo 的 code 同一套编号):
|
|
12
|
+
// https://www.jma.go.jp/jma/kishou/know/kurashi/symbols.html
|
|
13
|
+
// 「X,弱」→ 弱いX,「X,並」「X,並又は強」→ X,「X,強」→ 強いX,「激しい」→ 激しいX。
|
|
14
|
+
// 0–3 在 ww 表里是「云的变化」而非云量,按气象厅预报用语(快晴 / 晴れ / 曇り)与天气预报画面的
|
|
15
|
+
// 天気テロップ「晴時々曇 = PARTLY CLOUDY」对应。96/99 的强弱在 ww 表里指雷电而非冰雹。
|
|
16
|
+
const WMO_CODES = {
|
|
17
|
+
0: ["晴", "Clear sky", "快晴"],
|
|
18
|
+
1: ["大部晴朗", "Mainly clear", "晴れ"],
|
|
19
|
+
2: ["多云", "Partly cloudy", "晴れ時々曇り"],
|
|
20
|
+
3: ["阴", "Overcast", "曇り"],
|
|
21
|
+
45: ["雾", "Fog", "霧"],
|
|
22
|
+
48: ["雾凇", "Depositing rime fog", "霧(霧氷を伴う)"],
|
|
23
|
+
51: ["毛毛雨(弱)", "Light drizzle", "弱い霧雨"],
|
|
24
|
+
53: ["毛毛雨", "Moderate drizzle", "霧雨"],
|
|
25
|
+
55: ["毛毛雨(强)", "Dense drizzle", "強い霧雨"],
|
|
26
|
+
56: ["冻毛毛雨(弱)", "Light freezing drizzle", "弱い着氷性の霧雨"],
|
|
27
|
+
57: ["冻毛毛雨(强)", "Dense freezing drizzle", "着氷性の霧雨"],
|
|
28
|
+
61: ["小雨", "Slight rain", "弱い雨"],
|
|
29
|
+
63: ["中雨", "Moderate rain", "雨"],
|
|
30
|
+
65: ["大雨", "Heavy rain", "強い雨"],
|
|
31
|
+
66: ["冻雨(弱)", "Light freezing rain", "弱い着氷性の雨"],
|
|
32
|
+
67: ["冻雨(强)", "Heavy freezing rain", "着氷性の雨"],
|
|
33
|
+
71: ["小雪", "Slight snow", "弱い雪"],
|
|
34
|
+
73: ["中雪", "Moderate snow", "雪"],
|
|
35
|
+
75: ["大雪", "Heavy snow", "強い雪"],
|
|
36
|
+
77: ["雪粒", "Snow grains", "霧雪"],
|
|
37
|
+
80: ["阵雨(弱)", "Slight rain showers", "弱いにわか雨"],
|
|
38
|
+
81: ["阵雨", "Moderate rain showers", "にわか雨"],
|
|
39
|
+
82: ["阵雨(强)", "Violent rain showers", "激しいにわか雨"],
|
|
40
|
+
85: ["阵雪(弱)", "Slight snow showers", "弱いにわか雪"],
|
|
41
|
+
86: ["阵雪(强)", "Heavy snow showers", "にわか雪"],
|
|
42
|
+
95: ["雷暴", "Thunderstorm", "雷雨"],
|
|
43
|
+
96: ["雷暴伴小冰雹", "Thunderstorm with slight hail", "雷雨(ひょうを伴う)"],
|
|
44
|
+
99: ["雷暴伴大冰雹", "Thunderstorm with heavy hail", "激しい雷雨(ひょうを伴う)"],
|
|
45
|
+
};
|
|
46
|
+
const TEXT_LANGUAGES = ["zh", "en", "ja"];
|
|
47
|
+
const UNKNOWN_TEXT = { zh: "未知", en: "Unknown", ja: "不明" };
|
|
48
|
+
|
|
49
|
+
/** 天气文案有 zh / en / ja 三套;其它语言(ru/de/…)走英文,避免整段中文(#92)。 */
|
|
50
|
+
export function textLanguage(lang) {
|
|
51
|
+
const tag = String(lang ?? "").trim().toLowerCase();
|
|
52
|
+
return TEXT_LANGUAGES.includes(tag) ? tag : "en";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function wmoText(code, lang) {
|
|
56
|
+
const textLang = textLanguage(lang);
|
|
57
|
+
const hit = WMO_CODES[code];
|
|
58
|
+
if (!hit) return UNKNOWN_TEXT[textLang];
|
|
59
|
+
return hit[TEXT_LANGUAGES.indexOf(textLang)] + ` (WMO ${code})`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** weather 工具的正文(render 用)。lang 同 textLanguage(),description 由 fetchWeather 按同一语言生成。 */
|
|
63
|
+
export function formatWeather(value, lang) {
|
|
64
|
+
const textLang = textLanguage(lang);
|
|
65
|
+
const c = value.current ?? {};
|
|
66
|
+
const pp = (d, fmt) => (d.precipitationProbability !== undefined ? fmt(d.precipitationProbability) : "");
|
|
67
|
+
const lines = [];
|
|
68
|
+
if (textLang === "zh") {
|
|
69
|
+
lines.push(`${value.location} 天气:`);
|
|
70
|
+
lines.push(`当前:${c.temperature}°C(体感 ${c.apparent}°C),${c.text},湿度 ${c.humidity}%,风速 ${c.wind} km/h`);
|
|
71
|
+
for (const d of value.daily ?? []) lines.push(`${d.date}:${d.min}~${d.max}°C,${d.text}${pp(d, (p) => `,降水概率 ${p}%`)}`);
|
|
72
|
+
} else if (textLang === "ja") {
|
|
73
|
+
lines.push(`${value.location} の天気:`);
|
|
74
|
+
lines.push(`現在:${c.temperature}°C(体感 ${c.apparent}°C)、${c.text}、湿度 ${c.humidity}%、風速 ${c.wind} km/h`);
|
|
75
|
+
for (const d of value.daily ?? []) lines.push(`${d.date}:${d.min}〜${d.max}°C、${d.text}${pp(d, (p) => `、降水確率 ${p}%`)}`);
|
|
76
|
+
} else {
|
|
77
|
+
lines.push(`Weather for ${value.location}:`);
|
|
78
|
+
lines.push(`Now: ${c.temperature}°C (feels ${c.apparent}°C), ${c.text}, humidity ${c.humidity}%, wind ${c.wind} km/h`);
|
|
79
|
+
for (const d of value.daily ?? []) lines.push(`${d.date}: ${d.min}~${d.max}°C, ${d.text}${pp(d, (p) => `, precip ${p}%`)}`);
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function geocodeLocation(name, signal, language = "en") {
|
|
85
|
+
const params = new URLSearchParams({ name, count: "1", language: String(language ?? "en") || "en", format: "json" });
|
|
86
|
+
const response = await fetch(`${OPEN_METEO_GEOCODING_URL}?${params}`, {
|
|
87
|
+
method: "GET",
|
|
88
|
+
redirect: "error",
|
|
89
|
+
headers: { accept: "application/json", "user-agent": "deepseek-harness/free-search" },
|
|
90
|
+
signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(15000)]),
|
|
91
|
+
});
|
|
92
|
+
if (!response.ok) throw new Error(`geocoding API error (HTTP ${response.status})`);
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
const hit = data.results?.[0];
|
|
95
|
+
if (!hit) return null;
|
|
96
|
+
return {
|
|
97
|
+
name: hit.name,
|
|
98
|
+
admin: hit.admin1 ?? "",
|
|
99
|
+
country: hit.country ?? "",
|
|
100
|
+
latitude: hit.latitude,
|
|
101
|
+
longitude: hit.longitude,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function fetchWeather(location, days, lang, signal) {
|
|
106
|
+
const geo = await geocodeLocation(location, signal, lang);
|
|
107
|
+
if (!geo) throw new Error(`could not geocode location "${location}" — try a nearby larger city`);
|
|
108
|
+
const params = new URLSearchParams({
|
|
109
|
+
latitude: String(geo.latitude),
|
|
110
|
+
longitude: String(geo.longitude),
|
|
111
|
+
current: "temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m",
|
|
112
|
+
daily: "weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max",
|
|
113
|
+
timezone: "auto",
|
|
114
|
+
forecast_days: String(Math.min(Math.max(Number(days) || 3, 1), 7)),
|
|
115
|
+
});
|
|
116
|
+
const response = await fetch(`${OPEN_METEO_FORECAST_URL}?${params}`, {
|
|
117
|
+
method: "GET",
|
|
118
|
+
redirect: "error",
|
|
119
|
+
headers: { accept: "application/json", "user-agent": "deepseek-harness/free-search" },
|
|
120
|
+
signal: AbortSignal.any([...(signal !== undefined ? [signal] : []), AbortSignal.timeout(15000)]),
|
|
121
|
+
});
|
|
122
|
+
if (!response.ok) throw new Error(`forecast API error (HTTP ${response.status})`);
|
|
123
|
+
const data = await response.json();
|
|
124
|
+
const cur = data.current ?? {};
|
|
125
|
+
const daily = data.daily ?? {};
|
|
126
|
+
return {
|
|
127
|
+
location: [geo.name, geo.admin, geo.country].filter(Boolean).join(", "),
|
|
128
|
+
latitude: geo.latitude,
|
|
129
|
+
longitude: geo.longitude,
|
|
130
|
+
current: {
|
|
131
|
+
temperature: cur.temperature_2m,
|
|
132
|
+
apparent: cur.apparent_temperature,
|
|
133
|
+
humidity: cur.relative_humidity_2m,
|
|
134
|
+
wind: cur.wind_speed_10m,
|
|
135
|
+
code: cur.weather_code,
|
|
136
|
+
text: wmoText(cur.weather_code, lang),
|
|
137
|
+
},
|
|
138
|
+
daily: (daily.time ?? []).map((date, i) => ({
|
|
139
|
+
date,
|
|
140
|
+
min: daily.temperature_2m_min?.[i],
|
|
141
|
+
max: daily.temperature_2m_max?.[i],
|
|
142
|
+
code: daily.weather_code?.[i],
|
|
143
|
+
text: wmoText(daily.weather_code?.[i], lang),
|
|
144
|
+
precipitationProbability: daily.precipitation_probability_max?.[i],
|
|
145
|
+
})),
|
|
146
|
+
timezone: data.timezone ?? "",
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// weather 工具的 output schema。与 fetchWeather 返回值逐字段对齐:
|
|
151
|
+
// 任何一边增删字段都必须同步,否则 additionalProperties:false 会让工具调用失败(#73)。
|
|
152
|
+
export const WEATHER_OUTPUT_SCHEMA = {
|
|
153
|
+
type: "object",
|
|
154
|
+
additionalProperties: false,
|
|
155
|
+
properties: {
|
|
156
|
+
location: { type: "string" },
|
|
157
|
+
latitude: { type: "number" },
|
|
158
|
+
longitude: { type: "number" },
|
|
159
|
+
timezone: { type: "string" },
|
|
160
|
+
current: {
|
|
161
|
+
type: "object",
|
|
162
|
+
additionalProperties: false,
|
|
163
|
+
properties: {
|
|
164
|
+
temperature: { type: "number" },
|
|
165
|
+
apparent: { type: "number" },
|
|
166
|
+
humidity: { type: "number" },
|
|
167
|
+
wind: { type: "number" },
|
|
168
|
+
code: { type: "number" },
|
|
169
|
+
text: { type: "string" },
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
daily: {
|
|
173
|
+
type: "array",
|
|
174
|
+
items: {
|
|
175
|
+
type: "object",
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
properties: {
|
|
178
|
+
date: { type: "string" },
|
|
179
|
+
min: { type: "number" },
|
|
180
|
+
max: { type: "number" },
|
|
181
|
+
code: { type: "number" },
|
|
182
|
+
text: { type: "string" },
|
|
183
|
+
precipitationProbability: { type: "number" },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// lossless JSON 不允许 undefined 字段:按"存在且有值"构造对象(与其它工具的做法一致)。
|
|
191
|
+
export function sanitizeWeatherResult(r) {
|
|
192
|
+
return {
|
|
193
|
+
location: r.location,
|
|
194
|
+
...(r.latitude !== undefined ? { latitude: r.latitude } : {}),
|
|
195
|
+
...(r.longitude !== undefined ? { longitude: r.longitude } : {}),
|
|
196
|
+
...(r.timezone ? { timezone: r.timezone } : {}),
|
|
197
|
+
current: {
|
|
198
|
+
...(r.current?.temperature !== undefined ? { temperature: r.current.temperature } : {}),
|
|
199
|
+
...(r.current?.apparent !== undefined ? { apparent: r.current.apparent } : {}),
|
|
200
|
+
...(r.current?.humidity !== undefined ? { humidity: r.current.humidity } : {}),
|
|
201
|
+
...(r.current?.wind !== undefined ? { wind: r.current.wind } : {}),
|
|
202
|
+
...(r.current?.code !== undefined ? { code: r.current.code } : {}),
|
|
203
|
+
...(r.current?.text ? { text: r.current.text } : {}),
|
|
204
|
+
},
|
|
205
|
+
daily: (r.daily ?? []).map((d) => ({
|
|
206
|
+
date: d.date,
|
|
207
|
+
...(d.min !== undefined ? { min: d.min } : {}),
|
|
208
|
+
...(d.max !== undefined ? { max: d.max } : {}),
|
|
209
|
+
...(d.code !== undefined ? { code: d.code } : {}),
|
|
210
|
+
...(d.text ? { text: d.text } : {}),
|
|
211
|
+
...(d.precipitationProbability !== undefined ? { precipitationProbability: d.precipitationProbability } : {}),
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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",
|