dsh-free-search 0.4.4 → 0.4.6
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.en.md +5 -4
- package/README.md +6 -5
- package/lib/client.js +45 -0
- package/lib/index.js +123 -55
- package/package.json +5 -5
package/README.en.md
CHANGED
|
@@ -28,7 +28,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
28
28
|
- **Multi-Engine Support** — DuckDuckGo (HTML / Lite), Bing, AnySearch AI, SearXNG (meta-search with custom instances), Exa, Perplexity, and DeepSeek Official
|
|
29
29
|
- **Web Settings UI** — Engine switching and API key configuration (API keys are masked in the UI and displayed as "configured")
|
|
30
30
|
- **Engine Testing Tool** — `free_search_test`, allowing the agent to test the availability of all engines in a single call
|
|
31
|
-
- **
|
|
31
|
+
- **Unified Engine Fallback** — Any engine failure (paid or free, missing key, 401, rate limit, network error) automatically tries the next engine: other paid engines with configured keys first, then free engines. Search never fails outright.
|
|
32
32
|
- **System Prompt Injection** — The agent is aware of the currently active engine and which engines require API keys
|
|
33
33
|
- **Visual Badges** — Free engines feature a green `FREE` badge, while paid engines show an orange `API KEY` badge in the settings UI
|
|
34
34
|
- **Webpage Fetching (`web_fetch`)** — Allows the agent to read full webpage contents (official `dsh-web-fetch-http` provider, pure JS, zero extra dependencies)
|
|
@@ -49,7 +49,7 @@ This plugin provides multiple free search engines with automatic fallback, compl
|
|
|
49
49
|
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
50
50
|
|
|
51
51
|
- **Default engine is `bing`** (free and most stable), ready to use out of the box after installation.
|
|
52
|
-
-
|
|
52
|
+
- **Auto-failover**: any engine failure (rate-limited free engine, or missing/invalid paid key, network error) automatically tries the next engine — other paid engines with configured keys first, then free engines (Bing/AnySearch etc.) — with a note attached to the results. Search never fails outright because of engine issues.
|
|
53
53
|
- **Official Links in Settings**: Free engines display "Visit Website →", while paid engines display "Get API Key →" (opens in a new tab):
|
|
54
54
|
- Exa: <https://dashboard.exa.ai/api-keys>
|
|
55
55
|
- Perplexity: <https://www.perplexity.ai/settings/api>
|
|
@@ -86,7 +86,8 @@ After installation, navigate to **Settings → Plugins → Configurable** tab
|
|
|
86
86
|
|
|
87
87
|
- **Search engine**: Select an engine from the dropdown; changes take effect immediately upon saving.
|
|
88
88
|
- **API keys**: Enter keys for Exa / Perplexity / DeepSeek (password fields; displayed as "configured" once saved).
|
|
89
|
-
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
89
|
+
- **Use Bing default**: stage a switch back to the stable free Bing engine; `Discard` only cancels unsaved edits
|
|
90
|
+
- **Platform search**: check GitHub / V2EX / Bilibili to enable them for the `platform_search` tool (disabled platforms are skipped).
|
|
90
91
|
|
|
91
92
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
92
93
|
<tr style="border: none;">
|
|
@@ -181,7 +182,7 @@ Windows users: The desktop shortcut already includes this configuration (`set NO
|
|
|
181
182
|
|
|
182
183
|
## How It Works
|
|
183
184
|
|
|
184
|
-
- `lib/index.js`: Host side. Implements `WebSearchProvider` (`id` / `available()` / `search()`),
|
|
185
|
+
- `lib/index.js`: Host side. Implements `WebSearchProvider` (`id` / `available()` / `search()`), unified engine routing + auto-fallback (paid engines first, free as fallback); registers the `free-search` settings namespace; provides the `/api/dsh-free-search-settings` read/write bridge + `raw-search` debug endpoint; registers the `free_search_test` and `platform_search` tools; dynamically injects the engine list into system prompts (auto-refreshes on settings change).
|
|
185
186
|
- `lib/client.js`: Browser side. React configuration card (engine select + key inputs), mounted into the official `settings.plugin.item` slot (Settings → Plugins → Configurable), without requiring `dsh-web-ui`.
|
|
186
187
|
- `cordis.patch.yml`: Plugin loader configuration.
|
|
187
188
|
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
28
28
|
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、Exa、Perplexity、DeepSeek 官方
|
|
29
29
|
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")
|
|
30
30
|
- **引擎测试工具** —— `free_search_test`,让 agent 一键测试所有引擎可用性
|
|
31
|
-
-
|
|
31
|
+
- **统一引擎回退** —— 任何引擎失败(付费/免费,缺 key/401/限流/网络)自动轮流尝试下一个引擎:先试其他已配 key 的付费引擎,再试免费引擎,搜索永不直接失败
|
|
32
32
|
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
|
|
33
33
|
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
34
34
|
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
@@ -49,7 +49,7 @@ dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`
|
|
|
49
49
|
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
50
50
|
|
|
51
51
|
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
|
|
52
|
-
-
|
|
52
|
+
- **自动回退**:任何引擎失败(免费限流/反爬,付费缺 key/无效/网络错误)都会自动轮流尝试下一个引擎——先试其他已配 key 的付费引擎,再试免费引擎(Bing/AnySearch 等),并在结果中附带回退提示——搜索不会因引擎问题直接失败。
|
|
53
53
|
- **设置页有官网链接**:免费引擎显示"访问官网 →",付费引擎显示"获取 API Key →"(新标签页打开):
|
|
54
54
|
- Exa:<https://dashboard.exa.ai/api-keys>
|
|
55
55
|
- Perplexity:<https://www.perplexity.ai/settings/api>
|
|
@@ -85,8 +85,9 @@ dsh web
|
|
|
85
85
|
安装后,打开 **设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片(官方设置页):
|
|
86
86
|
|
|
87
87
|
- **Search engine**:下拉框切换引擎,保存即生效
|
|
88
|
-
- **API keys**:为 Exa / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
89
|
-
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
88
|
+
- **API keys**:为 Exa / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
89
|
+
- **Use Bing default**:把当前搜索引擎切回稳定的免费 Bing;`Discard` 只撤销尚未保存的编辑
|
|
90
|
+
- **Platform search**:勾选启用 GitHub / V2EX / Bilibili 平台搜索(`platform_search` 工具按此过滤)
|
|
90
91
|
|
|
91
92
|
<table align="center" style="border: none; border-collapse: collapse;">
|
|
92
93
|
<tr style="border: none;">
|
|
@@ -182,7 +183,7 @@ Windows 用户:桌面快捷方式已内置此配置(`set NODE_USE_ENV_PROXY=
|
|
|
182
183
|
|
|
183
184
|
## 工作原理
|
|
184
185
|
|
|
185
|
-
- `lib/index.js`:host 端。实现 `WebSearchProvider`(`id` / `available()` / `search()
|
|
186
|
+
- `lib/index.js`:host 端。实现 `WebSearchProvider`(`id` / `available()` / `search()`),统一引擎路由 + 自动回退(付费引擎优先,免费兜底);注册 `free-search` settings namespace;提供 `/api/dsh-free-search-settings` 读写桥 + `raw-search` 调试接口;注册 `free_search_test` 和 `platform_search` 工具;动态注入引擎清单到系统提示词(设置变更时自动刷新)。
|
|
186
187
|
- `lib/client.js`:浏览器端。React 配置卡片(引擎选择 + key 输入),挂载到官方设置页的 `settings.plugin.item` 插槽(设置 → 插件 → 可配置),不依赖 dsh-web-ui。
|
|
187
188
|
- `cordis.patch.yml`:插件 loader 配置。
|
|
188
189
|
|
package/lib/client.js
CHANGED
|
@@ -31,6 +31,9 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
32
32
|
".dshfs-input:disabled{opacity:.6;cursor:default}",
|
|
33
33
|
".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
|
|
34
|
+
".dshfs-platforms{display:flex;gap:10px;flex-wrap:wrap}",
|
|
35
|
+
".dshfs-platform{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
|
|
36
|
+
".dshfs-platform input{accent-color:var(--dsw-alias-state-business-primary)}",
|
|
34
37
|
".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
|
|
35
38
|
".dshfs-link:hover{text-decoration:underline}",
|
|
36
39
|
".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
|
|
@@ -89,6 +92,7 @@ window.__ModuleLoader__.load({
|
|
|
89
92
|
const [exaKey, setExaKey] = react.useState("");
|
|
90
93
|
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
91
94
|
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
95
|
+
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili"]);
|
|
92
96
|
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
93
97
|
const [dirty, setDirty] = react.useState(false);
|
|
94
98
|
const [saving, setSaving] = react.useState(false);
|
|
@@ -105,6 +109,7 @@ window.__ModuleLoader__.load({
|
|
|
105
109
|
setExaKey(v.exaApiKey ?? "");
|
|
106
110
|
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
107
111
|
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
112
|
+
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili"]);
|
|
108
113
|
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
109
114
|
const configured = {};
|
|
110
115
|
for (const secret of view.secrets ?? []) {
|
|
@@ -146,6 +151,7 @@ window.__ModuleLoader__.load({
|
|
|
146
151
|
if (exaKey.trim()) ops.push({ op: "set", path: ["exaApiKey"], value: exaKey.trim() });
|
|
147
152
|
if (perplexityKey.trim()) ops.push({ op: "set", path: ["perplexityApiKey"], value: perplexityKey.trim() });
|
|
148
153
|
if (deepseekKey.trim()) ops.push({ op: "set", path: ["deepseekApiKey"], value: deepseekKey.trim() });
|
|
154
|
+
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
149
155
|
const result = await bridgeMutate({ ns: NS, ops });
|
|
150
156
|
if (result.ok) {
|
|
151
157
|
setDirty(false);
|
|
@@ -291,6 +297,44 @@ window.__ModuleLoader__.load({
|
|
|
291
297
|
}),
|
|
292
298
|
],
|
|
293
299
|
}),
|
|
300
|
+
react_jsx_runtime.jsx("div", {
|
|
301
|
+
className: "dshfs-field",
|
|
302
|
+
children: [
|
|
303
|
+
react_jsx_runtime.jsx("div", {
|
|
304
|
+
className: "dshfs-label",
|
|
305
|
+
children: "Platform search (platform_search tool)",
|
|
306
|
+
}),
|
|
307
|
+
react_jsx_runtime.jsx("div", {
|
|
308
|
+
className: "dshfs-platforms",
|
|
309
|
+
children: [
|
|
310
|
+
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"],
|
|
311
|
+
].map(([id, label]) =>
|
|
312
|
+
react_jsx_runtime.jsx("label", {
|
|
313
|
+
className: "dshfs-platform",
|
|
314
|
+
children: [
|
|
315
|
+
react_jsx_runtime.jsx("input", {
|
|
316
|
+
type: "checkbox",
|
|
317
|
+
checked: platforms.includes(id),
|
|
318
|
+
disabled: !ready || saving,
|
|
319
|
+
onChange: (e) => {
|
|
320
|
+
setPlatforms((prev) =>
|
|
321
|
+
e.target.checked ? [...prev, id] : prev.filter((p) => p !== id)
|
|
322
|
+
);
|
|
323
|
+
setDirty(true);
|
|
324
|
+
setFailed(false);
|
|
325
|
+
},
|
|
326
|
+
}),
|
|
327
|
+
label,
|
|
328
|
+
],
|
|
329
|
+
}, id)
|
|
330
|
+
),
|
|
331
|
+
}),
|
|
332
|
+
react_jsx_runtime.jsx("p", {
|
|
333
|
+
className: "dshfs-hint",
|
|
334
|
+
children: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
335
|
+
}),
|
|
336
|
+
],
|
|
337
|
+
}),
|
|
294
338
|
!ready
|
|
295
339
|
? react_jsx_runtime.jsx("p", {
|
|
296
340
|
className: "dshfs-hint",
|
|
@@ -344,6 +388,7 @@ window.__ModuleLoader__.load({
|
|
|
344
388
|
ctx.slots.register(
|
|
345
389
|
{
|
|
346
390
|
name: "settings.plugin.item",
|
|
391
|
+
key: "free-search",
|
|
347
392
|
id: "dsh-free-search",
|
|
348
393
|
order: 120,
|
|
349
394
|
inject: () => ({}),
|
package/lib/index.js
CHANGED
|
@@ -648,7 +648,7 @@ function toView(descriptor) {
|
|
|
648
648
|
};
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
-
function makeBridgeRoutes(settings) {
|
|
651
|
+
function makeBridgeRoutes(settings, search) {
|
|
652
652
|
const allowlisted = () =>
|
|
653
653
|
settings
|
|
654
654
|
.describe({ redactSecrets: true })
|
|
@@ -656,6 +656,28 @@ function makeBridgeRoutes(settings) {
|
|
|
656
656
|
.map((descriptor) => String(descriptor.ns));
|
|
657
657
|
|
|
658
658
|
const handlers = {
|
|
659
|
+
async rawSearch(request) {
|
|
660
|
+
if (request === null || typeof request !== "object" || typeof request.query !== "string" || request.query.length === 0) {
|
|
661
|
+
return { ok: false, code: "search-rejected", message: "malformed bridge search request (query is required)" };
|
|
662
|
+
}
|
|
663
|
+
if (typeof search !== "function") {
|
|
664
|
+
return { ok: false, code: "search-unavailable", message: "search provider is not wired" };
|
|
665
|
+
}
|
|
666
|
+
const maxResults = Math.min(Math.max(Number(request.maxResults) || 5, 1), 10);
|
|
667
|
+
try {
|
|
668
|
+
const result = await search(request.query, maxResults);
|
|
669
|
+
return {
|
|
670
|
+
ok: true,
|
|
671
|
+
value: {
|
|
672
|
+
provider: (settings.describe({ redactSecrets: true }).find((c) => String(c.ns) === FREE_SEARCH_NS)?.schema ?? {}).provider ?? undefined,
|
|
673
|
+
sources: result.sources ?? [],
|
|
674
|
+
content: result.content ?? "",
|
|
675
|
+
},
|
|
676
|
+
};
|
|
677
|
+
} catch (error) {
|
|
678
|
+
return { ok: false, code: "search-failed", message: error instanceof Error ? error.message : String(error) };
|
|
679
|
+
}
|
|
680
|
+
},
|
|
659
681
|
async describe() {
|
|
660
682
|
const descriptors = settings.describe({ redactSecrets: true });
|
|
661
683
|
return {
|
|
@@ -730,6 +752,19 @@ function makeBridgeRoutes(settings) {
|
|
|
730
752
|
writeJson(res, 200, await handlers.mutate(body));
|
|
731
753
|
},
|
|
732
754
|
},
|
|
755
|
+
{
|
|
756
|
+
kind: "exact",
|
|
757
|
+
path: `${BRIDGE_PREFIX}/raw-search`,
|
|
758
|
+
handler: async (req, res) => {
|
|
759
|
+
if (!guard(req, res)) return;
|
|
760
|
+
const body = await readJsonBody(req);
|
|
761
|
+
if (body === undefined) {
|
|
762
|
+
writeJson(res, 400, { ok: false, code: "search-rejected", message: "malformed JSON body" });
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
writeJson(res, 200, await handlers.rawSearch(body));
|
|
766
|
+
},
|
|
767
|
+
},
|
|
733
768
|
];
|
|
734
769
|
}
|
|
735
770
|
//#endregion
|
|
@@ -742,6 +777,7 @@ const Config = z.object({
|
|
|
742
777
|
region: z.string(),
|
|
743
778
|
bingMarket: z.string().default("zh-CN"),
|
|
744
779
|
searxngInstances: z.array(z.string()),
|
|
780
|
+
platforms: z.array(z.string()).default(["github", "v2ex", "bilibili"]),
|
|
745
781
|
exaApiKey: z.string().role("secret"),
|
|
746
782
|
perplexityApiKey: z.string().role("secret"),
|
|
747
783
|
deepseekApiKey: z.string().role("secret"),
|
|
@@ -752,6 +788,9 @@ function apply(ctx, config) {
|
|
|
752
788
|
const logger = ctx.logger;
|
|
753
789
|
const credentials = ctx.get("credentials");
|
|
754
790
|
|
|
791
|
+
// 系统提示词动态刷新:设置变更时重新生成,避免显示旧引擎
|
|
792
|
+
let refreshPrompt = null;
|
|
793
|
+
|
|
755
794
|
// key 优先级:settings 的 free-search.<x>ApiKey > 环境变量/credentials
|
|
756
795
|
const resolveApiKey = async (envName, settingsKey) => {
|
|
757
796
|
const cfg = current();
|
|
@@ -766,7 +805,8 @@ function apply(ctx, config) {
|
|
|
766
805
|
};
|
|
767
806
|
|
|
768
807
|
// 总控 provider:按 settings 的 provider 字段路由到任意引擎。
|
|
769
|
-
//
|
|
808
|
+
// 任何引擎失败(缺 key / 401 / 限流 / 网络)都会自动轮流尝试下一个引擎,
|
|
809
|
+
// 直到成功或全部失败。并在结果里附带回退提示,避免 agent 搜索直接失败。
|
|
770
810
|
const provider = {
|
|
771
811
|
id: "ddg",
|
|
772
812
|
available() {
|
|
@@ -776,56 +816,64 @@ function apply(ctx, config) {
|
|
|
776
816
|
const cfg = current();
|
|
777
817
|
const preferred = cfg.provider ?? "bing";
|
|
778
818
|
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (preferred === "perplexity") {
|
|
786
|
-
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
787
|
-
return searchPerplexity(request.query, request.maxResults, key, signal);
|
|
788
|
-
}
|
|
789
|
-
if (preferred === "deepseek-official") {
|
|
790
|
-
const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
|
|
791
|
-
return searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
|
|
792
|
-
}
|
|
819
|
+
// 统一引擎链:首选优先,然后其他付费引擎(有 key 的优先尝试),最后免费引擎
|
|
820
|
+
const paidEngines = ["exa", "perplexity", "deepseek-official"];
|
|
821
|
+
const freeEngines = ["bing", "anysearch", "ddg", "ddg-lite", "searxng"];
|
|
822
|
+
const othersPaid = paidEngines.filter((e) => e !== preferred);
|
|
823
|
+
const othersFree = freeEngines.filter((e) => e !== preferred);
|
|
824
|
+
const chain = [preferred, ...othersPaid, ...othersFree];
|
|
793
825
|
|
|
794
|
-
// 免费引擎:首选 + 自动回退(anysearch / searxng 无 key)
|
|
795
|
-
const chain =
|
|
796
|
-
preferred === "ddg-lite"
|
|
797
|
-
? ["ddg-lite", "ddg", "anysearch", "searxng", "bing"]
|
|
798
|
-
: preferred === "bing"
|
|
799
|
-
? ["bing", "anysearch", "searxng", "ddg", "ddg-lite"]
|
|
800
|
-
: preferred === "searxng"
|
|
801
|
-
? ["searxng", "anysearch", "bing", "ddg", "ddg-lite"]
|
|
802
|
-
: preferred === "anysearch"
|
|
803
|
-
? ["anysearch", "bing", "searxng", "ddg", "ddg-lite"]
|
|
804
|
-
: ["ddg", "anysearch", "searxng", "ddg-lite", "bing"];
|
|
805
826
|
let lastError = null;
|
|
827
|
+
let usedEngine = null;
|
|
806
828
|
for (const engine of chain) {
|
|
807
829
|
try {
|
|
808
830
|
let result;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
result = await
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
831
|
+
if (engine === "ddg") {
|
|
832
|
+
result = await searchDdgHtml(request.query, request.maxResults, cfg, signal);
|
|
833
|
+
} else if (engine === "ddg-lite") {
|
|
834
|
+
result = await searchDdgLite(request.query, request.maxResults, signal);
|
|
835
|
+
} else if (engine === "bing") {
|
|
836
|
+
result = await searchBing(request.query, request.maxResults, cfg, signal);
|
|
837
|
+
} else if (engine === "searxng") {
|
|
838
|
+
result = await searchSearxng(request.query, request.maxResults, cfg, signal);
|
|
839
|
+
} else if (engine === "anysearch") {
|
|
840
|
+
result = await searchAnysearch(request.query, request.maxResults, signal);
|
|
841
|
+
} else if (engine === "exa") {
|
|
842
|
+
// exa:有 key 走 REST,无 key 走 keyless MCP(免费)
|
|
843
|
+
const key = await resolveApiKey("EXA_API_KEY", "exaApiKey");
|
|
844
|
+
if (key) {
|
|
845
|
+
result = await searchExa(request.query, request.maxResults, key, signal);
|
|
846
|
+
} else {
|
|
847
|
+
result = await searchExaMCP(request.query, request.maxResults, signal);
|
|
848
|
+
}
|
|
849
|
+
} else if (engine === "perplexity") {
|
|
850
|
+
const key = await resolveApiKey("PERPLEXITY_API_KEY", "perplexityApiKey");
|
|
851
|
+
if (!key) {
|
|
852
|
+
lastError = new Error("Perplexity requires PERPLEXITY_API_KEY");
|
|
853
|
+
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
854
|
+
continue; // 无 key 跳过
|
|
855
|
+
}
|
|
856
|
+
result = await searchPerplexity(request.query, request.maxResults, key, signal);
|
|
857
|
+
} else if (engine === "deepseek-official") {
|
|
858
|
+
const key = await resolveApiKey("DEEPSEEK_API_KEY", "deepseekApiKey");
|
|
859
|
+
if (!key) {
|
|
860
|
+
lastError = new Error("DeepSeek requires DEEPSEEK_API_KEY");
|
|
861
|
+
logger.warn(`free-search: engine "${engine}" skipped (no key), trying next engine`);
|
|
862
|
+
continue; // 无 key 跳过
|
|
863
|
+
}
|
|
864
|
+
result = await searchDeepSeekOfficial(request.query, request.maxResults, key, signal);
|
|
865
|
+
} else {
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (result.sources.length > 0) {
|
|
870
|
+
usedEngine = engine;
|
|
871
|
+
// 用了非首选引擎时,在结果里附上提示(agent 会看到)
|
|
872
|
+
if (engine !== preferred) {
|
|
873
|
+
result.content = `Note: ${preferred} unavailable or failed, using ${engine}.`;
|
|
874
|
+
}
|
|
875
|
+
return result;
|
|
827
876
|
}
|
|
828
|
-
if (result.sources.length > 0) return result;
|
|
829
877
|
lastError = new Error(`engine "${engine}" returned 0 results`);
|
|
830
878
|
logger.warn(`free-search: ${engine} returned 0 results, trying next engine`);
|
|
831
879
|
} catch (error) {
|
|
@@ -842,12 +890,17 @@ function apply(ctx, config) {
|
|
|
842
890
|
setSource: (source) => {
|
|
843
891
|
current = source;
|
|
844
892
|
},
|
|
845
|
-
onChange: () => {
|
|
893
|
+
onChange: () => {
|
|
894
|
+
// settings 变更时刷新系统提示词(显示最新引擎)
|
|
895
|
+
if (typeof refreshPrompt === "function") refreshPrompt();
|
|
896
|
+
},
|
|
846
897
|
});
|
|
847
898
|
|
|
848
899
|
ctx.inject(["webServer", "settings"], (sctx) => {
|
|
849
900
|
sctx.effect(() => {
|
|
850
|
-
const disposers = makeBridgeRoutes(sctx.settings
|
|
901
|
+
const disposers = makeBridgeRoutes(sctx.settings, (query, maxResults) => provider.search({ query, maxResults }, undefined)).map((route) =>
|
|
902
|
+
sctx.webServer.register(route)
|
|
903
|
+
);
|
|
851
904
|
return () => {
|
|
852
905
|
for (const dispose of disposers) dispose();
|
|
853
906
|
};
|
|
@@ -1044,6 +1097,13 @@ function apply(ctx, config) {
|
|
|
1044
1097
|
if (!PLATFORMS[platform]) {
|
|
1045
1098
|
throw new Error(`unknown platform "${platform}" - use one of: ${Object.keys(PLATFORMS).join(", ")}`);
|
|
1046
1099
|
}
|
|
1100
|
+
// 平台开关:settings 里禁用某平台时,工具明确告知
|
|
1101
|
+
const enabled = current().platforms ?? ["github", "v2ex", "bilibili"];
|
|
1102
|
+
if (!enabled.includes(platform)) {
|
|
1103
|
+
throw new Error(
|
|
1104
|
+
`platform "${platform}" is disabled in Free Search settings - enable it in Settings > Plugins > Free Search to use it`
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1047
1107
|
const limit = Math.min(args.maxResults ?? 5, 10);
|
|
1048
1108
|
const result = await searchPlatform(platform, args.query, limit);
|
|
1049
1109
|
return { platform, sources: result.sources };
|
|
@@ -1061,10 +1121,15 @@ function apply(ctx, config) {
|
|
|
1061
1121
|
}, "free-search: platform search tool");
|
|
1062
1122
|
});
|
|
1063
1123
|
|
|
1064
|
-
// 让 agent 知道可用搜索引擎(动态生成,随 key
|
|
1124
|
+
// 让 agent 知道可用搜索引擎(动态生成,随 key/设置变化)
|
|
1065
1125
|
ctx.inject(["systemPrompt"], (sctx) => {
|
|
1066
|
-
|
|
1067
|
-
|
|
1126
|
+
let disposeSection = null;
|
|
1127
|
+
refreshPrompt = () => {
|
|
1128
|
+
if (disposeSection) {
|
|
1129
|
+
disposeSection();
|
|
1130
|
+
disposeSection = null;
|
|
1131
|
+
}
|
|
1132
|
+
disposeSection = sctx.systemPrompt.section({
|
|
1068
1133
|
name: "free-search:engines",
|
|
1069
1134
|
order: 500,
|
|
1070
1135
|
text: [
|
|
@@ -1083,16 +1148,19 @@ function apply(ctx, config) {
|
|
|
1083
1148
|
"- perplexity - requires PERPLEXITY_API_KEY",
|
|
1084
1149
|
"- deepseek-official - requires DEEPSEEK_API_KEY",
|
|
1085
1150
|
"",
|
|
1086
|
-
"
|
|
1151
|
+
"IMPORTANT: If the configured engine fails (missing key, invalid key, 401, rate limit, or network error), web_search automatically tries other engines in this order: (1) other PAID engines whose API key is configured, (2) then FREE engines (Bing, AnySearch, DuckDuckGo, SearXNG). This applies to ALL engines - paid or free. The results include a note showing which engine was actually used. Never tell the user search is unavailable - it always falls back.",
|
|
1087
1152
|
"",
|
|
1088
1153
|
"Use the free_search_test tool to test which engines actually work right now.",
|
|
1089
1154
|
"",
|
|
1090
1155
|
"For platform-specific searches (GitHub repos, V2EX threads, Bilibili videos), use the platform_search tool with platform: github|v2ex|bilibili.",
|
|
1091
1156
|
].join("\n"),
|
|
1092
|
-
};
|
|
1093
|
-
|
|
1157
|
+
});
|
|
1158
|
+
};
|
|
1159
|
+
sctx.effect(() => {
|
|
1160
|
+
refreshPrompt();
|
|
1094
1161
|
return () => {
|
|
1095
|
-
|
|
1162
|
+
if (disposeSection) disposeSection();
|
|
1163
|
+
disposeSection = null;
|
|
1096
1164
|
};
|
|
1097
1165
|
}, "free-search: engine list prompt section");
|
|
1098
1166
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-free-search",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"description": "Free web search for DeepSeek Harness: 8 engines (Bing/DuckDuckGo/AnySearch/SearXNG/Exa keyless) + platform search + web_fetch, with web settings UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
44
|
-
"@deepseek-ai/dsh-settings": "
|
|
45
|
-
"@deepseek-ai/dsh-tools": "
|
|
44
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.6",
|
|
45
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-settings": "0.1.0-rc.
|
|
49
|
-
"@deepseek-ai/dsh-tools": "0.1.0-rc.
|
|
48
|
+
"@deepseek-ai/dsh-settings": "0.1.0-rc.7",
|
|
49
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.7"
|
|
50
50
|
},
|
|
51
51
|
"dsh": {
|
|
52
52
|
"bundle": {
|