dsh-free-search 0.2.0
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/LICENSE +21 -0
- package/README.en.md +139 -0
- package/README.md +140 -0
- package/cordis.patch.yml +17 -0
- package/lib/client.js +348 -0
- package/lib/index.js +815 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-free-search contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# dsh-free-search
|
|
2
|
+
|
|
3
|
+
**Free web search provider for DeepSeek Harness — no API key required, multi-engine, zero cost.**
|
|
4
|
+
|
|
5
|
+
A plugin that adds multi-engine search providers to DeepSeek Harness (dsh), registered into the `ctx.web` seam. The built-in `web_search` tool picks it up automatically. Includes a web settings page for engine switching and API key configuration, plus an engine test tool.
|
|
6
|
+
|
|
7
|
+
[中文](./README.md) | English
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
dsh ships a DeepSeek-official search provider by default, which requires a valid `DEEPSEEK_API_KEY`. If you:
|
|
12
|
+
- don't have (or don't want) a DeepSeek official API key,
|
|
13
|
+
- already use a gateway like opencode-go (which does not expose a `web_search` tool),
|
|
14
|
+
|
|
15
|
+
...then the built-in search always fails and the agent tells you "I can't access the internet."
|
|
16
|
+
|
|
17
|
+
This plugin provides multiple free engines with automatic failover, fully independent of the DeepSeek official key.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Zero cost** — multiple free engines, no key, no registration
|
|
22
|
+
- **Multi-engine**: DuckDuckGo (html/lite), Bing, SearXNG (meta-search, custom instances), Exa, Perplexity, DeepSeek Official
|
|
23
|
+
- **Web settings UI** — engine switching + API key configuration (keys are redacted in the UI, shown as "configured")
|
|
24
|
+
- **Engine test tool** — `free_search_test`, lets the agent verify every engine in one call
|
|
25
|
+
- **Auto-failover** — free engines fall back to the next available one on failure/rate-limit
|
|
26
|
+
- **System prompt injection** — the agent knows the current engine and which need keys
|
|
27
|
+
- **FREE / API KEY badges** — green FREE badge for free engines, orange API KEY badge for paid ones
|
|
28
|
+
- **Clean integration** — implements the official `WebSearchProvider` seam, coexists with official plugins
|
|
29
|
+
- **web_fetch** — agent can fetch page content (official `dsh-web-fetch-http` provider, pure JS, zero extra deps)
|
|
30
|
+
|
|
31
|
+
## Engines
|
|
32
|
+
|
|
33
|
+
| id | Engine | Cost | Notes |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `ddg` | DuckDuckGo HTML | Free | Occasionally rate-limited (anti-bot), recovers automatically |
|
|
36
|
+
| `ddg-lite` | DuckDuckGo Lite | Free | Lightweight variant, same caveat |
|
|
37
|
+
| `bing` | Bing | Free | **Default engine**, most stable, zh-CN optimized |
|
|
38
|
+
| `searxng` | SearXNG meta-search | Free | Multi-instance failover, custom instances supported |
|
|
39
|
+
| `exa` | Exa | Paid | Requires `EXA_API_KEY` |
|
|
40
|
+
| `perplexity` | Perplexity | Paid | Requires `PERPLEXITY_API_KEY` |
|
|
41
|
+
| `deepseek-official` | DeepSeek Official | Paid | Requires `DEEPSEEK_API_KEY` |
|
|
42
|
+
|
|
43
|
+
Free engines auto-fallback to another free engine on failure. Paid engines fail with a clear error when their key is missing or invalid — never a silent switch.
|
|
44
|
+
|
|
45
|
+
- **Default engine is `bing`** (free and most stable), works out of the box after install.
|
|
46
|
+
- **Settings page has official links**: free engines show "访问官网 →", paid engines show "获取 API Key →" (opens in a new tab):
|
|
47
|
+
- Exa: <https://dashboard.exa.ai/api-keys>
|
|
48
|
+
- Perplexity: <https://www.perplexity.ai/settings/api>
|
|
49
|
+
- DeepSeek: <https://platform.deepseek.com/api_keys>
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
git clone https://github.com/DDDMUC/dsh-free-search.git
|
|
55
|
+
dsh plugin --profile web add /path/to/dsh-free-search
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then restart:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
dsh web
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Usage
|
|
65
|
+
|
|
66
|
+
### Web settings (recommended)
|
|
67
|
+
|
|
68
|
+
After install, open **Settings → Plugins → Configurable** tab → **Free Search** card (official settings page, no dsh-web-ui needed):
|
|
69
|
+
|
|
70
|
+
- **Search engine**: dropdown to switch engines, save to apply
|
|
71
|
+
- **API keys**: fill keys for Exa / Perplexity / DeepSeek (password inputs; after save the UI only shows "configured")
|
|
72
|
+
|
|
73
|
+
### Config file
|
|
74
|
+
|
|
75
|
+
Configuration lives in `~/.dsh/settings.yaml`:
|
|
76
|
+
|
|
77
|
+
```yaml
|
|
78
|
+
free-search:
|
|
79
|
+
provider: bing # ddg / ddg-lite / bing / searxng / exa / perplexity / deepseek-official
|
|
80
|
+
bingMarket: zh-CN # Bing market
|
|
81
|
+
region: cn-zh # DuckDuckGo region (optional)
|
|
82
|
+
searxngInstances: # custom SearXNG instances (optional)
|
|
83
|
+
- https://your-instance.example
|
|
84
|
+
exaApiKey: ... # or configure via the settings page
|
|
85
|
+
perplexityApiKey: ...
|
|
86
|
+
deepseekApiKey: ...
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Have the agent test all engines
|
|
90
|
+
|
|
91
|
+
Ask the agent to "test all search engines" — it calls the `free_search_test` tool and reports:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
Search engine test:
|
|
95
|
+
- ddg: FAIL - DuckDuckGo is rate-limited right now (anti-bot challenge, usually temporary) - Bing works
|
|
96
|
+
- bing: OK (2 results, e.g. "DeepSeek Harness developer preview...")
|
|
97
|
+
- exa: FAIL - EXA_API_KEY not configured
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Fetch page content (web_fetch)
|
|
101
|
+
|
|
102
|
+
After search, ask the agent to **read a page** (e.g. "open the first link and summarize it"). The `web_fetch` tool is enabled (official `dsh-web-fetch-http` provider):
|
|
103
|
+
|
|
104
|
+
- Follows redirects, decodes body (HTML to text)
|
|
105
|
+
- Timeout and size limits
|
|
106
|
+
- ⚠️ Note: `web_fetch` has no SSRF protection — the agent could reach internal addresses. Use deliberately.
|
|
107
|
+
|
|
108
|
+
## Local engine switcher (tools/)
|
|
109
|
+
|
|
110
|
+
Prefer a local tool over the web UI? The `tools/` directory ships a zero-dependency switcher:
|
|
111
|
+
|
|
112
|
+
- **`启动搜索引擎切换器.cmd`** (Windows) — double-click to start a local Node server (`http://127.0.0.1:4789`) and open the picker page in your browser
|
|
113
|
+
- **`switch-engine.html`** — the picker UI: shows the current engine, one-click switch
|
|
114
|
+
- **`server.mjs`** — local server that reads/writes `~/.dsh/profiles/web/cordis.patch.yml`
|
|
115
|
+
- **`switch-engine.ps1`** — headless CLI: `powershell -File tools/switch-engine.ps1 -Engine bing`
|
|
116
|
+
|
|
117
|
+
Restart `dsh web` after switching.
|
|
118
|
+
|
|
119
|
+
> The settings card mounts on the official `settings.plugin.item` slot (built into dsh); config reads/writes go through the plugin's own bridge. **No dsh-web-ui dependency — the plugin works standalone.**
|
|
120
|
+
|
|
121
|
+
## Proxy note
|
|
122
|
+
|
|
123
|
+
DuckDuckGo and some engines may be blocked and need a proxy. Node.js `fetch` does not use the system proxy by default — set these environment variables for the dsh process (Node 24+):
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
export NODE_USE_ENV_PROXY=1
|
|
127
|
+
export HTTPS_PROXY=http://127.0.0.1:7897 # your proxy
|
|
128
|
+
export HTTP_PROXY=http://127.0.0.1:7897
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## How it works
|
|
132
|
+
|
|
133
|
+
- `lib/index.js`: host side. Implements `WebSearchProvider` (`id` / `available()` / `search()`), multi-engine routing + auto-failover; registers the `free-search` settings namespace; serves the `/api/dsh-free-search-settings` read/write bridge; registers the `free_search_test` tool; injects the engine list into the system prompt.
|
|
134
|
+
- `lib/client.js`: browser side. React settings card (engine select + key inputs), mounted on the official `settings.plugin.item` slot (Settings → Plugins → Configurable), no dsh-web-ui dependency.
|
|
135
|
+
- `cordis.patch.yml`: plugin loader config.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# dsh-free-search
|
|
2
|
+
|
|
3
|
+
**DeepSeek Harness 免费搜索插件 —— 无需 API key,零成本,多引擎可切换。**
|
|
4
|
+
|
|
5
|
+
一个给 DeepSeek Harness (dsh) 添加多引擎搜索 provider 的插件,注册进 `ctx.web` seam。内置 `web_search` 工具自动选用,支持网页设置页切换引擎、配置 API key、一键测试所有引擎。
|
|
6
|
+
|
|
7
|
+
[English](./README.en.md) | 中文
|
|
8
|
+
|
|
9
|
+
## 为什么需要它
|
|
10
|
+
|
|
11
|
+
dsh 默认的搜索 provider 依赖 DeepSeek 官方 API key(`DEEPSEEK_API_KEY`)。如果你:
|
|
12
|
+
- 没有(或不想用)DeepSeek 官方 key,
|
|
13
|
+
- 用的是 opencode-go 这类网关(其 OpenAI 兼容端点不支持 `web_search` 工具),
|
|
14
|
+
|
|
15
|
+
……那么内置搜索必然失败,agent 会告诉你"无法联网"。
|
|
16
|
+
|
|
17
|
+
这个插件提供多个免费引擎 + 自动回退,彻底摆脱 DeepSeek 官方 key 的依赖。
|
|
18
|
+
|
|
19
|
+
## 特性
|
|
20
|
+
|
|
21
|
+
- **零成本** —— 多个免费引擎,无需 key、无需注册
|
|
22
|
+
- **多引擎可选**:DuckDuckGo(html/lite)、Bing、SearXNG(元搜索,支持自定义实例)、Exa、Perplexity、DeepSeek 官方
|
|
23
|
+
- **网页设置页** —— 引擎切换 + API key 配置(UI 中 key 脱敏显示"已配置")
|
|
24
|
+
- **引擎测试工具** —— `free_search_test`,让 agent 一键测试所有引擎可用性
|
|
25
|
+
- **自动回退** —— 免费引擎失败/限流时自动切换到下一个可用引擎
|
|
26
|
+
- **系统提示词注入** —— agent 知道当前用哪个引擎、哪些需要 key
|
|
27
|
+
- **免费标注** —— 设置页中免费引擎带绿色 `FREE` 徽章,付费引擎带橙色 `API KEY` 徽章
|
|
28
|
+
- **网页抓取(web_fetch)** —— 让 agent 抓取网页内容(官方 `dsh-web-fetch-http` provider,纯 JS,零额外依赖)
|
|
29
|
+
- **干净集成** —— 实现官方 `WebSearchProvider` seam 接口,与官方插件共存
|
|
30
|
+
|
|
31
|
+
## 引擎列表
|
|
32
|
+
|
|
33
|
+
| id | 引擎 | 费用 | 说明 |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| `ddg` | DuckDuckGo HTML | 免费 | 偶发限流(反爬),解封自动恢复 |
|
|
36
|
+
| `ddg-lite` | DuckDuckGo Lite | 免费 | 轻量版,同上 |
|
|
37
|
+
| `bing` | Bing | 免费 | **默认引擎**,最稳定,中文优化(zh-CN) |
|
|
38
|
+
| `searxng` | SearXNG 元搜索 | 免费 | 多实例自动切换,支持自定义实例 |
|
|
39
|
+
| `exa` | Exa | 付费 | 需 `EXA_API_KEY` |
|
|
40
|
+
| `perplexity` | Perplexity | 付费 | 需 `PERPLEXITY_API_KEY` |
|
|
41
|
+
| `deepseek-official` | DeepSeek 官方 | 付费 | 需 `DEEPSEEK_API_KEY` |
|
|
42
|
+
|
|
43
|
+
- **默认引擎为 `bing`**(免费且最稳定),安装后开箱即用。
|
|
44
|
+
- 免费引擎失败会自动回退到其他免费引擎;付费引擎缺 key 或 key 无效时报清晰错误,不会静默切换。
|
|
45
|
+
- **设置页有官网链接**:免费引擎显示"访问官网 →",付费引擎显示"获取 API Key →"(新标签页打开):
|
|
46
|
+
- Exa:<https://dashboard.exa.ai/api-keys>
|
|
47
|
+
- Perplexity:<https://www.perplexity.ai/settings/api>
|
|
48
|
+
- DeepSeek:<https://platform.deepseek.com/api_keys>
|
|
49
|
+
|
|
50
|
+
## 安装
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/DDDMUC/dsh-free-search.git
|
|
54
|
+
dsh plugin --profile web add /path/to/dsh-free-search
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
然后重启:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
dsh web
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 使用
|
|
64
|
+
|
|
65
|
+
### 网页设置(推荐)
|
|
66
|
+
|
|
67
|
+
安装后,打开 **设置 → 插件 → 可配置** 标签页 → **Free Search** 卡片(官方设置页,无需 dsh-web-ui):
|
|
68
|
+
|
|
69
|
+
- **Search engine**:下拉框切换引擎,保存即生效
|
|
70
|
+
- **API keys**:为 Exa / Perplexity / DeepSeek 填写 key(密码框,保存后只显示"已配置")
|
|
71
|
+
|
|
72
|
+
### 命令行 / 配置文件
|
|
73
|
+
|
|
74
|
+
配置存在 `~/.dsh/settings.yaml`:
|
|
75
|
+
|
|
76
|
+
```yaml
|
|
77
|
+
free-search:
|
|
78
|
+
provider: bing # ddg / ddg-lite / bing / searxng / exa / perplexity / deepseek-official
|
|
79
|
+
bingMarket: zh-CN # Bing 市场
|
|
80
|
+
region: cn-zh # DuckDuckGo 区域(可选)
|
|
81
|
+
searxngInstances: # 自定义 SearXNG 实例(可选)
|
|
82
|
+
- https://your-instance.example
|
|
83
|
+
exaApiKey: ... # 或通过设置页填写
|
|
84
|
+
perplexityApiKey: ...
|
|
85
|
+
deepseekApiKey: ...
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 让 agent 测试所有引擎
|
|
89
|
+
|
|
90
|
+
对 agent 说"测试一下所有搜索引擎",它会调用 `free_search_test` 工具,逐个测试并报告:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
Search engine test:
|
|
94
|
+
- ddg: FAIL - DuckDuckGo is rate-limited right now (anti-bot challenge, usually temporary) - Bing works
|
|
95
|
+
- bing: OK (2 results, e.g. "DeepSeek Harness developer preview...")
|
|
96
|
+
- exa: FAIL - EXA_API_KEY not configured
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 抓取网页内容(web_fetch)
|
|
100
|
+
|
|
101
|
+
搜索到 URL 后,可以让 agent **读取网页全文**(如"打开第一个链接看看内容")。`web_fetch` 工具已启用(官方 `dsh-web-fetch-http` provider):
|
|
102
|
+
|
|
103
|
+
- 自动跟随重定向、解码正文(HTML 转文本)
|
|
104
|
+
- 支持超时和大小限制
|
|
105
|
+
- ⚠️ 注意:`web_fetch` 无 SSRF 防护,agent 理论上可访问内网地址——按需使用
|
|
106
|
+
|
|
107
|
+
## 本地引擎切换工具(tools/)
|
|
108
|
+
|
|
109
|
+
不想用网页设置页?`tools/` 目录附带了一个本地切换小工具(零依赖):
|
|
110
|
+
|
|
111
|
+
- **`启动搜索引擎切换器.cmd`**(Windows)——双击启动本地 Node 服务(`http://127.0.0.1:4789`)并自动打开浏览器选择页面
|
|
112
|
+
- **`switch-engine.html`** —— 选择页面:显示当前引擎,点选新引擎,一键写入配置
|
|
113
|
+
- **`server.mjs`** —— 本地服务,负责读写 `~/.dsh/profiles/web/cordis.patch.yml`
|
|
114
|
+
- **`switch-engine.ps1`** —— 无界面命令行版:`powershell -File tools/switch-engine.ps1 -Engine bing`
|
|
115
|
+
|
|
116
|
+
切换后重启 `dsh web` 生效。
|
|
117
|
+
|
|
118
|
+
> 配置卡片挂在官方设置页的 `settings.plugin.item` 插槽(dsh 自带),配置读写走插件自建 bridge,**不依赖 dsh-web-ui**,插件可独立使用。
|
|
119
|
+
|
|
120
|
+
## 代理说明(国内用户)
|
|
121
|
+
|
|
122
|
+
DuckDuckGo 等引擎可能需要代理才能访问,而 Node.js 的 `fetch` 默认不走系统代理。需要给 dsh 进程设置(Node 24+):
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
export NODE_USE_ENV_PROXY=1
|
|
126
|
+
export HTTPS_PROXY=http://127.0.0.1:7897 # 你的代理地址
|
|
127
|
+
export HTTP_PROXY=http://127.0.0.1:7897
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Windows 用户:桌面快捷方式已内置此配置(`set NODE_USE_ENV_PROXY=1&& set HTTPS_PROXY=...`)。
|
|
131
|
+
|
|
132
|
+
## 工作原理
|
|
133
|
+
|
|
134
|
+
- `lib/index.js`:host 端。实现 `WebSearchProvider`(`id` / `available()` / `search()`),多引擎路由 + 自动回退;注册 `free-search` settings namespace;提供 `/api/dsh-free-search-settings` 读写桥;注册 `free_search_test` 工具;注入引擎清单到系统提示词。
|
|
135
|
+
- `lib/client.js`:浏览器端。React 配置卡片(引擎选择 + key 输入),挂载到官方设置页的 `settings.plugin.item` 插槽(设置 → 插件 → 可配置),不依赖 dsh-web-ui。
|
|
136
|
+
- `cordis.patch.yml`:插件 loader 配置。
|
|
137
|
+
|
|
138
|
+
## License
|
|
139
|
+
|
|
140
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# dsh-free-search — free web search providers (no API key needed)
|
|
2
|
+
# Registers the free-search plugin into ctx.web + a settings bridge
|
|
3
|
+
# (/api/dsh-free-search-settings) that serves the "free-search" namespace.
|
|
4
|
+
# The engine (ddg / ddg-lite / bing) is chosen from the web Settings page
|
|
5
|
+
# (Settings > Plugins > Web UI plugins > Free Search) or settings.yaml:
|
|
6
|
+
# free-search:
|
|
7
|
+
# provider: bing
|
|
8
|
+
# region: cn-zh # DuckDuckGo region
|
|
9
|
+
# bingMarket: zh-CN # Bing market
|
|
10
|
+
# Failed/empty engines automatically fall back to the next free engine.
|
|
11
|
+
# web.searchProvider must point at this plugin's provider id (ddg).
|
|
12
|
+
- insert:
|
|
13
|
+
- id: web-search-free
|
|
14
|
+
name: dsh-free-search
|
|
15
|
+
config:
|
|
16
|
+
provider: bing
|
|
17
|
+
bingMarket: zh-CN
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "dsh-free-search",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
|
|
10
|
+
//#region css
|
|
11
|
+
const css = [
|
|
12
|
+
".dshfs-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;min-width:0;list-style:none;transition:border-color .16s,background .16s;overflow:hidden;margin-bottom:8px}",
|
|
13
|
+
".dshfs-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
|
|
14
|
+
".dshfs-header{width:100%;color:inherit;cursor:pointer;text-align:left;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;display:flex}",
|
|
15
|
+
".dshfs-header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
|
|
16
|
+
".dshfs-headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex;overflow:hidden}",
|
|
17
|
+
".dshfs-name{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}",
|
|
18
|
+
".dshfs-description{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}",
|
|
19
|
+
".dshfs-pending{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;flex:none;font-size:12px}",
|
|
20
|
+
".dshfs-chevron{color:var(--dsw-alias-label-tertiary);flex:none;font-size:13px;transition:transform .12s}",
|
|
21
|
+
".dshfs-chevronOpen{transform:rotate(180deg)}",
|
|
22
|
+
".dshfs-body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}",
|
|
23
|
+
".dshfs-footer{justify-content:flex-end;align-items:center;gap:8px;display:flex}",
|
|
24
|
+
".dshfs-failed{color:var(--dsw-alias-state-error-primary);margin:0 auto 0 0;font-size:12px}",
|
|
25
|
+
".dshfs-field{flex-direction:column;gap:4px;min-width:0;display:flex}",
|
|
26
|
+
".dshfs-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}",
|
|
27
|
+
".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%}",
|
|
28
|
+
".dshfs-select:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
|
|
29
|
+
".dshfs-input{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%}",
|
|
30
|
+
".dshfs-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
|
|
31
|
+
".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
32
|
+
".dshfs-input:disabled{opacity:.6;cursor:default}",
|
|
33
|
+
".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
|
|
34
|
+
".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
|
|
35
|
+
".dshfs-link:hover{text-decoration:underline}",
|
|
36
|
+
".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
|
|
37
|
+
".dshfs-save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}",
|
|
38
|
+
".dshfs-save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}",
|
|
39
|
+
".dshfs-save:disabled{opacity:.5;cursor:default}",
|
|
40
|
+
".dshfs-badge{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-state-business-primary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
|
|
41
|
+
".dshfs-badgeFree{background:rgba(80,200,120,.15);color:#7ddb9c;border:1px solid rgba(80,200,120,.3);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
|
|
42
|
+
".dshfs-badgeKey{background:rgba(240,170,80,.15);color:#f0b060;border:1px solid rgba(240,170,80,.3);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
|
|
43
|
+
].join("");
|
|
44
|
+
const tagId = "dsh-free-search/card.css";
|
|
45
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
46
|
+
const tag = document.createElement("style");
|
|
47
|
+
tag.dataset.plugin = "dsh-free-search";
|
|
48
|
+
tag.dataset.pluginCss = tagId;
|
|
49
|
+
tag.textContent = css;
|
|
50
|
+
document.head.appendChild(tag);
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
|
|
54
|
+
const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
|
|
55
|
+
const NS = "free-search";
|
|
56
|
+
const ENGINES = [
|
|
57
|
+
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
58
|
+
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
59
|
+
{ id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
|
|
60
|
+
{ id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
|
|
61
|
+
{ id: "exa", label: "Exa", badge: "API KEY", link: "https://dashboard.exa.ai/api-keys" },
|
|
62
|
+
{ id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
|
|
63
|
+
{ id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
async function bridgeDescribe() {
|
|
67
|
+
const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "content-type": "application/json" },
|
|
70
|
+
body: "{}",
|
|
71
|
+
});
|
|
72
|
+
return response.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function bridgeMutate(payload) {
|
|
76
|
+
const response = await fetch(`${BRIDGE_PREFIX}/mutate`, {
|
|
77
|
+
method: "POST",
|
|
78
|
+
headers: { "content-type": "application/json" },
|
|
79
|
+
body: JSON.stringify(payload),
|
|
80
|
+
});
|
|
81
|
+
return response.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function FreeSearchCard(props) {
|
|
85
|
+
const [open, setOpen] = react.useState(false);
|
|
86
|
+
const [state, setState] = react.useState({ status: "loading" });
|
|
87
|
+
const [provider, setProvider] = react.useState("bing");
|
|
88
|
+
const [exaKey, setExaKey] = react.useState("");
|
|
89
|
+
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
90
|
+
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
91
|
+
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
92
|
+
const [dirty, setDirty] = react.useState(false);
|
|
93
|
+
const [saving, setSaving] = react.useState(false);
|
|
94
|
+
const [failed, setFailed] = react.useState(false);
|
|
95
|
+
|
|
96
|
+
const load = react.useCallback(async () => {
|
|
97
|
+
try {
|
|
98
|
+
const result = await bridgeDescribe();
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
const view = result.value.namespaces.find((n) => n.ns === NS);
|
|
101
|
+
if (view) {
|
|
102
|
+
const v = view.value ?? {};
|
|
103
|
+
setProvider(v.provider ?? "ddg");
|
|
104
|
+
setExaKey(v.exaApiKey ?? "");
|
|
105
|
+
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
106
|
+
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
107
|
+
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
108
|
+
const configured = {};
|
|
109
|
+
for (const secret of view.secrets ?? []) {
|
|
110
|
+
if (secret.set) {
|
|
111
|
+
const path = secret.path.join(".");
|
|
112
|
+
if (path === "exaApiKey") configured.exa = true;
|
|
113
|
+
if (path === "perplexityApiKey") configured.perplexity = true;
|
|
114
|
+
if (path === "deepseekApiKey") configured.deepseek = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
setKeysConfigured(configured);
|
|
118
|
+
setState({ status: "ready", writable: result.value.writable });
|
|
119
|
+
} else {
|
|
120
|
+
setState({ status: "unavailable" });
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
setState({ status: "unavailable" });
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
setState({ status: "unavailable" });
|
|
127
|
+
}
|
|
128
|
+
}, []);
|
|
129
|
+
|
|
130
|
+
react.useEffect(() => {
|
|
131
|
+
load();
|
|
132
|
+
}, [load]);
|
|
133
|
+
|
|
134
|
+
const select = (value) => {
|
|
135
|
+
setProvider(value);
|
|
136
|
+
setDirty(true);
|
|
137
|
+
setFailed(false);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const save = async () => {
|
|
141
|
+
setSaving(true);
|
|
142
|
+
setFailed(false);
|
|
143
|
+
try {
|
|
144
|
+
const ops = [{ op: "set", path: ["provider"], value: provider }];
|
|
145
|
+
if (exaKey.trim()) ops.push({ op: "set", path: ["exaApiKey"], value: exaKey.trim() });
|
|
146
|
+
if (perplexityKey.trim()) ops.push({ op: "set", path: ["perplexityApiKey"], value: perplexityKey.trim() });
|
|
147
|
+
if (deepseekKey.trim()) ops.push({ op: "set", path: ["deepseekApiKey"], value: deepseekKey.trim() });
|
|
148
|
+
const result = await bridgeMutate({ ns: NS, ops });
|
|
149
|
+
if (result.ok) {
|
|
150
|
+
setDirty(false);
|
|
151
|
+
setProvider(result.value.value.provider ?? provider);
|
|
152
|
+
setFailed(false);
|
|
153
|
+
load();
|
|
154
|
+
} else {
|
|
155
|
+
setFailed(true);
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
setFailed(true);
|
|
159
|
+
} finally {
|
|
160
|
+
setSaving(false);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const discard = () => {
|
|
165
|
+
load();
|
|
166
|
+
setDirty(false);
|
|
167
|
+
setFailed(false);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (state.status === "loading") return null;
|
|
171
|
+
const ready = state.status === "ready";
|
|
172
|
+
const title = "Free Search";
|
|
173
|
+
const description = "Free web search — no API key needed (DuckDuckGo / Bing)";
|
|
174
|
+
const currentEngine = ENGINES.find((e) => e.id === provider) ?? ENGINES[0];
|
|
175
|
+
const badgeClass =
|
|
176
|
+
currentEngine.badge === "FREE" ? "dshfs-badge dshfs-badgeFree" : "dshfs-badge dshfs-badgeKey";
|
|
177
|
+
|
|
178
|
+
return react_jsx_runtime.jsx("li", {
|
|
179
|
+
className: open ? "dshfs-card dshfs-cardOpen" : "dshfs-card",
|
|
180
|
+
children: [
|
|
181
|
+
react_jsx_runtime.jsx("button", {
|
|
182
|
+
type: "button",
|
|
183
|
+
className: "dshfs-header",
|
|
184
|
+
"aria-expanded": open,
|
|
185
|
+
onClick: () => setOpen(!open),
|
|
186
|
+
children: [
|
|
187
|
+
react_jsx_runtime.jsx("span", {
|
|
188
|
+
className: "dshfs-headText",
|
|
189
|
+
children: [
|
|
190
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-name", children: title }),
|
|
191
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-description", children: description }),
|
|
192
|
+
],
|
|
193
|
+
}),
|
|
194
|
+
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
195
|
+
dirty ? react_jsx_runtime.jsx("span", { className: "dshfs-pending", children: "unsaved" }) : null,
|
|
196
|
+
react_jsx_runtime.jsx("span", {
|
|
197
|
+
className: open ? "dshfs-chevron dshfs-chevronOpen" : "dshfs-chevron",
|
|
198
|
+
children: "▾",
|
|
199
|
+
}),
|
|
200
|
+
],
|
|
201
|
+
}),
|
|
202
|
+
open
|
|
203
|
+
? react_jsx_runtime.jsx("div", {
|
|
204
|
+
className: "dshfs-body",
|
|
205
|
+
children: [
|
|
206
|
+
react_jsx_runtime.jsx("div", {
|
|
207
|
+
className: "dshfs-field",
|
|
208
|
+
children: [
|
|
209
|
+
react_jsx_runtime.jsx("div", {
|
|
210
|
+
className: "dshfs-label",
|
|
211
|
+
children: [
|
|
212
|
+
"Search engine",
|
|
213
|
+
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
214
|
+
],
|
|
215
|
+
}),
|
|
216
|
+
react_jsx_runtime.jsx("select", {
|
|
217
|
+
className: "dshfs-select",
|
|
218
|
+
value: provider,
|
|
219
|
+
disabled: !ready || saving,
|
|
220
|
+
onChange: (e) => select(e.target.value),
|
|
221
|
+
children: ENGINES.map((engine) =>
|
|
222
|
+
react_jsx_runtime.jsx("option", { value: engine.id, children: `${engine.label} (${engine.badge})` }, engine.id)
|
|
223
|
+
),
|
|
224
|
+
}),
|
|
225
|
+
currentEngine.link
|
|
226
|
+
? react_jsx_runtime.jsx("a", {
|
|
227
|
+
className: "dshfs-link",
|
|
228
|
+
href: currentEngine.link,
|
|
229
|
+
target: "_blank",
|
|
230
|
+
rel: "noopener noreferrer",
|
|
231
|
+
children:
|
|
232
|
+
currentEngine.badge === "FREE"
|
|
233
|
+
? "访问官网 →"
|
|
234
|
+
: "获取 API Key →",
|
|
235
|
+
})
|
|
236
|
+
: null,
|
|
237
|
+
react_jsx_runtime.jsx("p", {
|
|
238
|
+
className: "dshfs-hint",
|
|
239
|
+
children:
|
|
240
|
+
"Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
|
|
241
|
+
}),
|
|
242
|
+
],
|
|
243
|
+
}),
|
|
244
|
+
react_jsx_runtime.jsx("div", {
|
|
245
|
+
className: "dshfs-field",
|
|
246
|
+
children: [
|
|
247
|
+
react_jsx_runtime.jsx("div", {
|
|
248
|
+
className: "dshfs-label",
|
|
249
|
+
children: "API keys (optional)",
|
|
250
|
+
}),
|
|
251
|
+
react_jsx_runtime.jsx("input", {
|
|
252
|
+
className: "dshfs-input",
|
|
253
|
+
type: "password",
|
|
254
|
+
placeholder: keysConfigured.exa ? "Exa API key (configured)" : "Exa API key (sk-...)",
|
|
255
|
+
value: exaKey,
|
|
256
|
+
disabled: !ready || saving,
|
|
257
|
+
onChange: (e) => {
|
|
258
|
+
setExaKey(e.target.value);
|
|
259
|
+
setDirty(true);
|
|
260
|
+
setFailed(false);
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
react_jsx_runtime.jsx("input", {
|
|
264
|
+
className: "dshfs-input",
|
|
265
|
+
type: "password",
|
|
266
|
+
placeholder: keysConfigured.perplexity ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
267
|
+
value: perplexityKey,
|
|
268
|
+
disabled: !ready || saving,
|
|
269
|
+
onChange: (e) => {
|
|
270
|
+
setPerplexityKey(e.target.value);
|
|
271
|
+
setDirty(true);
|
|
272
|
+
setFailed(false);
|
|
273
|
+
},
|
|
274
|
+
}),
|
|
275
|
+
react_jsx_runtime.jsx("input", {
|
|
276
|
+
className: "dshfs-input",
|
|
277
|
+
type: "password",
|
|
278
|
+
placeholder: keysConfigured.deepseek ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
279
|
+
value: deepseekKey,
|
|
280
|
+
disabled: !ready || saving,
|
|
281
|
+
onChange: (e) => {
|
|
282
|
+
setDeepseekKey(e.target.value);
|
|
283
|
+
setDirty(true);
|
|
284
|
+
setFailed(false);
|
|
285
|
+
},
|
|
286
|
+
}),
|
|
287
|
+
react_jsx_runtime.jsx("p", {
|
|
288
|
+
className: "dshfs-hint",
|
|
289
|
+
children: "Keys are stored in settings.yaml (redacted in the UI). Enter a key for an engine to use it.",
|
|
290
|
+
}),
|
|
291
|
+
],
|
|
292
|
+
}),
|
|
293
|
+
!ready
|
|
294
|
+
? react_jsx_runtime.jsx("p", {
|
|
295
|
+
className: "dshfs-hint",
|
|
296
|
+
children: "Settings unavailable — the free-search bridge is not exposed.",
|
|
297
|
+
})
|
|
298
|
+
: null,
|
|
299
|
+
react_jsx_runtime.jsx("div", {
|
|
300
|
+
className: "dshfs-footer",
|
|
301
|
+
children: [
|
|
302
|
+
failed ? react_jsx_runtime.jsx("p", { className: "dshfs-failed", children: "save failed" }) : null,
|
|
303
|
+
react_jsx_runtime.jsx("button", {
|
|
304
|
+
className: "dshfs-btn",
|
|
305
|
+
type: "button",
|
|
306
|
+
onClick: discard,
|
|
307
|
+
disabled: saving || !dirty,
|
|
308
|
+
children: "Discard",
|
|
309
|
+
}),
|
|
310
|
+
react_jsx_runtime.jsx("button", {
|
|
311
|
+
className: "dshfs-btn dshfs-save",
|
|
312
|
+
type: "button",
|
|
313
|
+
onClick: save,
|
|
314
|
+
disabled: saving || !dirty || !ready,
|
|
315
|
+
children: saving ? "Saving…" : "Save",
|
|
316
|
+
}),
|
|
317
|
+
],
|
|
318
|
+
}),
|
|
319
|
+
],
|
|
320
|
+
})
|
|
321
|
+
: null,
|
|
322
|
+
],
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const inject = ["slots"];
|
|
327
|
+
|
|
328
|
+
function apply(ctx) {
|
|
329
|
+
// 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
|
|
330
|
+
// 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
|
|
331
|
+
ctx.slots.inject("settings.plugin.item", () =>
|
|
332
|
+
ctx.slots.register(
|
|
333
|
+
{
|
|
334
|
+
name: "settings.plugin.item",
|
|
335
|
+
id: "dsh-free-search",
|
|
336
|
+
order: 120,
|
|
337
|
+
inject: () => ({}),
|
|
338
|
+
},
|
|
339
|
+
FreeSearchCard
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
exports.apply = apply;
|
|
345
|
+
exports.inject = inject;
|
|
346
|
+
return module.exports;
|
|
347
|
+
},
|
|
348
|
+
});
|