dsh-bing-search 1.1.3
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/CHANGELOG.md +55 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/README_EN.md +93 -0
- package/cordis.patch.yml +32 -0
- package/lib/index.js +326 -0
- package/lib/types/index.d.ts +63 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.1.3] - 2026-09-13
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- 包名、GitHub 仓库、本地目录统一为 `dsh-bing-search`(无作用域、不带 `@cwsong/`)。`dsh-websearch-bing` 被 npm 判定与已占用的 `dsh-web-search-bing` 过于相似。
|
|
7
|
+
|
|
8
|
+
## [1.1.2] - 2026-09-11
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- npm 包名改为 `@cwsong/dsh-websearch-bing`(无作用域名被判定与已占用的 `dsh-web-search-bing` 过于相似)
|
|
12
|
+
|
|
13
|
+
## [1.1.1] - 2026-09-11
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- 包名、GitHub 仓库、loader 行和设置节统一为 `dsh-websearch-bing` / `websearch-bing`(npm 上的 `dsh-web-search-bing` 已被占用)
|
|
17
|
+
|
|
18
|
+
## [1.1.0] - 2026-09-11
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- 适配 DSH `0.1.5-rc.1`:peerDependencies 从 `^0.1.0-rc.6` 升级到 `>=0.1.5-rc.1 <0.2.0-0`(旧的 prerelease 区间不匹配 0.1.5-rc.x,导致安装/启动报错)
|
|
22
|
+
- `cordis.patch.yml` 补回 `fetchProvider: http`:补丁是整段替换 `web` 行配置,只写 `searchProvider` 会丢掉 base 层显式设置的 fetch provider
|
|
23
|
+
- 清理从 duckduckgo 版本遗留的注释/模块名(`web-search-duckduckgo`、`@module dsh-web-search-duckduckgo`)
|
|
24
|
+
- 修复 README_EN.md 的 emoji 乱码
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- 设置节注册改用电官方同款 `ctx.inject(["settings"])` + `settings.installSection(...)` 模式,命名空间改为普通字符串常量
|
|
28
|
+
- provider 改为「单实例 + resolveOptions thunk」模式(对齐官方 `@deepseek-ai/dsh-web-search-deepseek`),每次搜索快照一次配置,不再每次 new 实例
|
|
29
|
+
- `available()` 现在校验 endpoint 是合法 URL 且 maxResults 为正整数
|
|
30
|
+
- provider 错误信息附带端点恢复指引(对齐官方错误风格)
|
|
31
|
+
- `@deepseek-ai/schemastery` 从 peerDependencies 移到 dependencies(对齐官方插件结构)
|
|
32
|
+
- 移除未被代码引用的 `@deepseek-ai/dsh-agent` peer 依赖
|
|
33
|
+
- User-Agent 更新到 Chrome/131
|
|
34
|
+
- HTML 实体解码补充 ` ` ` ` ` ` `–` `—` `…`
|
|
35
|
+
|
|
36
|
+
## [1.0.2] - 2026-08-18
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
- 重命名为 dsh-web-search-bing(从 dsh-web-search-duckduckgo 迁移)
|
|
40
|
+
- 更新 README 和文档
|
|
41
|
+
|
|
42
|
+
## [1.0.1] - 2026-08-18
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
- 从 DuckDuckGo 切换到 Bing(cn.bing.com,国内可达)
|
|
46
|
+
- 更新 HTML 解析逻辑适配 Bing 页面结构
|
|
47
|
+
- 添加 ensearch 参数支持中英文搜索切换
|
|
48
|
+
- 添加 accept-language 头部
|
|
49
|
+
|
|
50
|
+
## [1.0.0] - 2026-08-18
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
- 初始版本:DuckDuckGo 免费搜索 provider
|
|
54
|
+
- 无需 API Key,不消耗 DeepSeek 搜索配额
|
|
55
|
+
- 支持 DSH web 能力接缝注册
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 dsh-web-search-duckduckgo 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.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# dsh-bing-search
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-bing-search)
|
|
6
|
+
|
|
7
|
+
> DeepSeek Harness (DSH) 的免费 Bing 搜索 provider。无需 API Key,不消耗模型搜索配额。
|
|
8
|
+
|
|
9
|
+
<p align="right">
|
|
10
|
+
<a href="README_EN.md">English</a> | <b>中文</b>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
> 本仓库是 [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing) 的维护 fork,已适配 DSH `0.1.5-rc.1`。包名用 `dsh-bing-search`,因为 `dsh-web-search-bing` 已被占用,`dsh-websearch-bing` 又被 npm 判定过于相似。
|
|
14
|
+
|
|
15
|
+
## 特点
|
|
16
|
+
|
|
17
|
+
- **完全免费**:使用 Bing 公共 HTML 搜索页面,无需 API Key
|
|
18
|
+
- **国内可达**:默认使用 cn.bing.com,中国大陆可直接访问
|
|
19
|
+
- **零配额消耗**:不消耗 DeepSeek 或任何 LLM 的搜索配额
|
|
20
|
+
- **即插即用**:安装后自动把 `web_search` 切到 `bing-free`
|
|
21
|
+
- **可配置**:支持切换搜索端点、中英文结果、结果数量
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
npm:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dsh plugin --profile desktop add dsh-bing-search
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GitHub:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile desktop add github:whiteS18/dsh-bing-search
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
网页版 profile 把 `desktop` 换成 `web`。装完重启 DSH。
|
|
38
|
+
|
|
39
|
+
也可以在 profile 目录手动安装:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
cd $DSH_HOME/profiles/desktop # 桌面版通常为 ~/.dsh/profiles/desktop
|
|
43
|
+
pnpm add dsh-bing-search
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
然后把 `"dsh-bing-search"` 加入 `dsh.profile.bundles`(放在 `@deepseek-ai/dsh-web-app` 之后),重启 DSH。
|
|
47
|
+
|
|
48
|
+
**注意:** `node_modules` 里的目录名必须等于 `package.json` 的 `name`。不要用旧名 `dsh-web-search-bing` 去 link 这个目录,否则桌面端会报 `profile package identity is invalid`。
|
|
49
|
+
|
|
50
|
+
## 配置
|
|
51
|
+
|
|
52
|
+
当前版本没有设置页卡片。改 `~/.dsh/settings.yaml`(热加载):
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
bing-search:
|
|
56
|
+
endpoint: https://cn.bing.com/search
|
|
57
|
+
maxResults: 20
|
|
58
|
+
ensearch: 0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
或改 profile 的 `cordis.patch.yml` 后重启:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- id: bing-search
|
|
65
|
+
config:
|
|
66
|
+
endpoint: https://cn.bing.com/search
|
|
67
|
+
maxResults: 15
|
|
68
|
+
ensearch: 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| 参数 | 默认值 | 说明 |
|
|
72
|
+
|------|--------|------|
|
|
73
|
+
| `endpoint` | `https://cn.bing.com/search` | 搜索端点 |
|
|
74
|
+
| `maxResults` | `20` | 每次搜索最大结果数 |
|
|
75
|
+
| `ensearch` | `0` | 0=中文结果,1=英文结果 |
|
|
76
|
+
|
|
77
|
+
设置 → 插件 → 插件配置 里的 **Web Search** 卡片属于官方 `web-search-deepseek`,不是本插件。
|
|
78
|
+
|
|
79
|
+
## 如何确认走的是 Bing
|
|
80
|
+
|
|
81
|
+
把 endpoint 临时改成无效地址后再搜一次。如果报错包含 `bing-search` 和你写的 endpoint,就是本 provider。测完改回去。
|
|
82
|
+
|
|
83
|
+
## 恢复默认搜索
|
|
84
|
+
|
|
85
|
+
把本插件 patch 中的 `searchProvider` 改回 `deepseek-official`(或从 bundles 移除本包),重启即可。
|
|
86
|
+
|
|
87
|
+
## 工作原理
|
|
88
|
+
|
|
89
|
+
调用 `cn.bing.com/search` → 解析 HTML 搜索结果(`<li class="b_algo">` 块)→ 提取标题/URL/摘要 → 返回给 DSH 的 `web_search` 工具。
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](LICENSE)
|
package/README_EN.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# dsh-bing-search
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-bing-search)
|
|
6
|
+
|
|
7
|
+
> Free Bing-backed web search provider for DeepSeek Harness (DSH). No API key needed, no search quota consumed.
|
|
8
|
+
|
|
9
|
+
<p align="right">
|
|
10
|
+
<b>English</b> | <a href="README.md">中文</a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
> This is a maintenance fork of [godchen520/dsh-web-search-bing](https://github.com/godchen520/dsh-web-search-bing), updated for DSH `0.1.5-rc.1`. The package is `dsh-bing-search` because `dsh-web-search-bing` is taken on npm and `dsh-websearch-bing` was rejected as too similar.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Completely Free** — Uses Bing's public HTML search page, no API key required
|
|
18
|
+
- **China Accessible** — Defaults to `cn.bing.com`, works in mainland China
|
|
19
|
+
- **Zero Quota** — Doesn't consume DeepSeek or any LLM search quota
|
|
20
|
+
- **Plug & Play** — Switches `web_search` to `bing-free` after install
|
|
21
|
+
- **Configurable** — Switch endpoints, language, and result count
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
npm:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
dsh plugin --profile desktop add dsh-bing-search
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
GitHub:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
dsh plugin --profile desktop add github:whiteS18/dsh-bing-search
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Use `web` instead of `desktop` for the web profile. Restart DSH after install.
|
|
38
|
+
|
|
39
|
+
Or install by hand in the profile directory:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
cd $DSH_HOME/profiles/desktop # usually ~/.dsh/profiles/desktop for the desktop app
|
|
43
|
+
pnpm add dsh-bing-search
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Then add `"dsh-bing-search"` to `dsh.profile.bundles` (after `@deepseek-ai/dsh-web-app`) and restart DSH.
|
|
47
|
+
|
|
48
|
+
**Note:** the directory name under `node_modules` must equal `package.json` `name`. Do not link this package as `dsh-web-search-bing`, or the desktop app will fail with `profile package identity is invalid`.
|
|
49
|
+
|
|
50
|
+
## Configuration
|
|
51
|
+
|
|
52
|
+
This version has no Settings card. Edit `~/.dsh/settings.yaml` (hot-reloaded):
|
|
53
|
+
|
|
54
|
+
```yaml
|
|
55
|
+
bing-search:
|
|
56
|
+
endpoint: https://cn.bing.com/search
|
|
57
|
+
maxResults: 20
|
|
58
|
+
ensearch: 0
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Or override in the profile `cordis.patch.yml` and restart:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- id: bing-search
|
|
65
|
+
config:
|
|
66
|
+
endpoint: https://cn.bing.com/search
|
|
67
|
+
maxResults: 15
|
|
68
|
+
ensearch: 0
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Option | Default | Description |
|
|
72
|
+
|--------|---------|-------------|
|
|
73
|
+
| `endpoint` | `https://cn.bing.com/search` | Search endpoint |
|
|
74
|
+
| `maxResults` | `20` | Max results parsed per search |
|
|
75
|
+
| `ensearch` | `0` | `0` = Chinese results, `1` = English |
|
|
76
|
+
|
|
77
|
+
The **Web Search** card under Settings → Plugins → Plugin configuration belongs to official `web-search-deepseek`, not this plugin.
|
|
78
|
+
|
|
79
|
+
## How to confirm Bing is in use
|
|
80
|
+
|
|
81
|
+
Temporarily set `endpoint` to an invalid URL and search again. If the error names `bing-search` and the endpoint you set, this provider handled the request. Restore the endpoint afterwards.
|
|
82
|
+
|
|
83
|
+
## Restoring the default search
|
|
84
|
+
|
|
85
|
+
Change `searchProvider` in this plugin's patch back to `deepseek-official` (or remove this package from `bundles`) and restart.
|
|
86
|
+
|
|
87
|
+
## How It Works
|
|
88
|
+
|
|
89
|
+
Queries `cn.bing.com/search` → parses HTML result blocks (`<li class="b_algo">`) → extracts title/URL/snippet → returns to DSH's `web_search` tool.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# dsh-bing-search profile patch layer.
|
|
2
|
+
#
|
|
3
|
+
# Two effects, both in the HOST composition (`ctx.web` is host-plane):
|
|
4
|
+
#
|
|
5
|
+
# 1. Insert one loader row for this plugin package, which registers the
|
|
6
|
+
# `bing-free` search provider via `ctx.web.registerSearchProvider(...)`.
|
|
7
|
+
# It runs after dsh-web-app's own patch layer because this bundle is listed
|
|
8
|
+
# after `@deepseek-ai/dsh-web-app` in the profile's `dsh.profile.bundles`.
|
|
9
|
+
#
|
|
10
|
+
# 2. Override the base `web` row's provider selection from `deepseek-official`
|
|
11
|
+
# (which spends a DeepSeek model turn per search) to `bing-free` (which is
|
|
12
|
+
# a free public HTML query via cn.bing.com, accessible from mainland China).
|
|
13
|
+
#
|
|
14
|
+
# A patch REPLACES the targeted row's whole `config` rather than merging into
|
|
15
|
+
# it, so this layer must restate every key the base row carries: dropping
|
|
16
|
+
# `fetchProvider: http` here would silently undo the base's explicit fetch
|
|
17
|
+
# selection. Keep the two keys in sync with dsh-base's `web` row when
|
|
18
|
+
# upgrading.
|
|
19
|
+
#
|
|
20
|
+
# Because `bing-free` is explicitly configured, the seam never hits
|
|
21
|
+
# WEB_PROVIDER_AMBIGUOUS even though `deepseek-official` may also be registered
|
|
22
|
+
# and available. To fall back to DeepSeek search later, change
|
|
23
|
+
# `searchProvider` below back to `deepseek-official` (and optionally remove this
|
|
24
|
+
# insert row).
|
|
25
|
+
- id: web
|
|
26
|
+
config:
|
|
27
|
+
searchProvider: bing-free
|
|
28
|
+
fetchProvider: http
|
|
29
|
+
|
|
30
|
+
- insert:
|
|
31
|
+
- id: bing-search
|
|
32
|
+
name: 'dsh-bing-search'
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { WebError } from "@deepseek-ai/dsh-web";
|
|
3
|
+
|
|
4
|
+
//#region provider
|
|
5
|
+
/**
|
|
6
|
+
* Bing-backed free web search provider for the `ctx.web` seam.
|
|
7
|
+
*
|
|
8
|
+
* This provider performs a plain HTTPS GET against Bing's public HTML search
|
|
9
|
+
* page (cn.bing.com for Chinese results, or any Bing endpoint you configure)
|
|
10
|
+
* and parses the result blocks it returns. It needs NO API key and makes NO
|
|
11
|
+
* model call, so the `web_search` tool works here without spending DeepSeek
|
|
12
|
+
* (or any LLM) search quota.
|
|
13
|
+
*
|
|
14
|
+
* cn.bing.com is accessible from mainland China, making this a practical
|
|
15
|
+
* default for domestic DSH deployments where DuckDuckGo is blocked.
|
|
16
|
+
*
|
|
17
|
+
* The endpoint is fully configurable via the `bing-search` settings
|
|
18
|
+
* section or the cordis config, so you can switch to any search engine that
|
|
19
|
+
* serves an HTML results page.
|
|
20
|
+
* @module dsh-bing-search/provider
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Stable id this provider registers under. */
|
|
24
|
+
const PROVIDER_ID = "bing-free";
|
|
25
|
+
|
|
26
|
+
/** Default endpoint: Bing China, Chinese results. `q=` is appended at call time. */
|
|
27
|
+
const DEFAULT_ENDPOINT = "https://cn.bing.com/search";
|
|
28
|
+
|
|
29
|
+
/** Default `ensearch` param: `0` = Chinese results, `1` = English. */
|
|
30
|
+
const DEFAULT_ENSEARCH = 0;
|
|
31
|
+
|
|
32
|
+
/** Default upper bound on results parsed per page. */
|
|
33
|
+
const DEFAULT_MAX_RESULTS = 20;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Browser-ish User-Agent. Bing may reject bare Node/undici agents; a
|
|
37
|
+
* conservative desktop UA keeps it serving normal result pages.
|
|
38
|
+
*/
|
|
39
|
+
const USER_AGENT =
|
|
40
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* HTML-decode an attribute/value minus markup. Bing escapes `&`, `<`
|
|
44
|
+
* etc. and may include `&#xNN;` or `&#NNN;` entities; decoding keeps titles
|
|
45
|
+
* and snippets human-readable.
|
|
46
|
+
*
|
|
47
|
+
* @param {string | null | undefined} value - the raw string to decode.
|
|
48
|
+
* @returns {string} the decoded string.
|
|
49
|
+
*/
|
|
50
|
+
function decodeHtml(value) {
|
|
51
|
+
if (value == null) return "";
|
|
52
|
+
return value
|
|
53
|
+
.replace(/&#x([0-9a-fA-F]+);/g, (_m, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
54
|
+
.replace(/&#(\d+);/g, (_m, dec) => String.fromCodePoint(parseInt(dec, 10)))
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/'/g, "'")
|
|
57
|
+
.replace(/</g, "<")
|
|
58
|
+
.replace(/>/g, ">")
|
|
59
|
+
.replace(/&/g, "&")
|
|
60
|
+
.replace(/ /g, " ")
|
|
61
|
+
.replace(/ /g, " ")
|
|
62
|
+
.replace(/ /g, " ")
|
|
63
|
+
.replace(/ /g, " ")
|
|
64
|
+
.replace(/–/g, "–")
|
|
65
|
+
.replace(/—/g, "—")
|
|
66
|
+
.replace(/…/g, "…")
|
|
67
|
+
.replace(/<[^>]*>/g, "")
|
|
68
|
+
.replace(/\s+/g, " ")
|
|
69
|
+
.trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse one `<li class="b_algo">` block from Bing's HTML into a source.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} blockHtml - the inner HTML of one result `<li>`.
|
|
76
|
+
* @returns {{ url: string, title: string, snippet: string } | null} the parsed
|
|
77
|
+
* fields, or `null` when the block has no usable link.
|
|
78
|
+
*/
|
|
79
|
+
function parseBingResultBlock(blockHtml) {
|
|
80
|
+
// Title + URL: <h2><a href="URL" ...>Title</a></h2>
|
|
81
|
+
const titleMatch = /<h2[^>]*>\s*<a[^>]*href\s*=\s*["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/i.exec(blockHtml);
|
|
82
|
+
if (titleMatch == null) return null;
|
|
83
|
+
const href = decodeHtml(titleMatch[1].trim());
|
|
84
|
+
const title = decodeHtml(titleMatch[2]);
|
|
85
|
+
// Resolve URL — Bing sometimes uses relative or redirect URLs.
|
|
86
|
+
let url;
|
|
87
|
+
try {
|
|
88
|
+
if (href.startsWith("//")) url = `https:${href}`;
|
|
89
|
+
else if (href.startsWith("/")) url = `https://cn.bing.com${href}`;
|
|
90
|
+
else url = new URL(href).href;
|
|
91
|
+
} catch {
|
|
92
|
+
url = href;
|
|
93
|
+
}
|
|
94
|
+
if (!(url.startsWith("https://") || url.startsWith("http://"))) return null;
|
|
95
|
+
// Snippet: try multiple selectors Bing uses across versions.
|
|
96
|
+
let snippet = "";
|
|
97
|
+
const snippetPatterns = [
|
|
98
|
+
/<p[^>]*class\s*=\s*["'][^"']*\bb_lineclamp[^"']*["'][^>]*>([\s\S]*?)<\/p>/i,
|
|
99
|
+
/<div[^>]*class\s*=\s*["'][^"']*\bcaption\b[^"']*["'][^>]*>\s*<p[^>]*>([\s\S]*?)<\/p>/i,
|
|
100
|
+
/<p[^>]*class\s*=\s*["'][^"']*\bb_algoSlug\b[^"']*["'][^>]*>([\s\S]*?)<\/p>/i,
|
|
101
|
+
/<div[^>]*>\s*<p[^>]*>([\s\S]*?)<\/p>\s*<\/div>/i
|
|
102
|
+
];
|
|
103
|
+
for (const pat of snippetPatterns) {
|
|
104
|
+
const m = pat.exec(blockHtml);
|
|
105
|
+
if (m != null && m[1].length > snippet.length) snippet = decodeHtml(m[1]);
|
|
106
|
+
}
|
|
107
|
+
return { url, title, snippet };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse Bing's HTML results page into source objects.
|
|
112
|
+
*
|
|
113
|
+
* Bing renders organic results as `<li class="b_algo">` inside an `<ol>`.
|
|
114
|
+
* Parsing is tolerant: it extracts every `<li class="b_algo">` block,
|
|
115
|
+
* parses title + URL + snippet from each, and dedupes by URL.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} html - the raw result page body.
|
|
118
|
+
* @param {number | undefined} maxResults - parsed source cap; unset means parse the whole page.
|
|
119
|
+
* @returns {Array<{ url: string, title?: string, snippet?: string }>} the normalized sources.
|
|
120
|
+
*/
|
|
121
|
+
function parseBingHtml(html, maxResults) {
|
|
122
|
+
const sources = [];
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
// Split on <li class="b_algo"> boundaries — each block is one result.
|
|
125
|
+
const blocks = html.split(/<li\s+class\s*=\s*["']?\s*b_algo\b/i);
|
|
126
|
+
// Skip the first element (everything before the first result).
|
|
127
|
+
for (let i = 1; i < blocks.length && (maxResults == null || sources.length < maxResults); i++) {
|
|
128
|
+
// Trim to the next </li> to avoid parsing across blocks.
|
|
129
|
+
const block = blocks[i].split(/<\/li>/i)[0] ?? blocks[i];
|
|
130
|
+
const parsed = parseBingResultBlock(block);
|
|
131
|
+
if (parsed == null || seen.has(parsed.url)) continue;
|
|
132
|
+
seen.add(parsed.url);
|
|
133
|
+
sources.push({
|
|
134
|
+
url: parsed.url,
|
|
135
|
+
...(parsed.title.length > 0 ? { title: parsed.title } : {}),
|
|
136
|
+
...(parsed.snippet.length > 0 ? { snippet: parsed.snippet } : {})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return sources;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* True when an otherwise-2xx Bing page is actually a captcha/bot gate.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} html - the response body.
|
|
146
|
+
* @returns {boolean} whether the page looks like a block/captcha gate.
|
|
147
|
+
*/
|
|
148
|
+
function looksBlocked(html) {
|
|
149
|
+
const lower = html.slice(0, 20000).toLowerCase();
|
|
150
|
+
return /(captcha|verify you are human|are you a robot|unusual traffic|access denied|challenge-platform)/.test(lower);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Build a fetch error that the seam surfaces as a structured provider failure.
|
|
155
|
+
* The endpoint is named so configuration mistakes are recoverable from the
|
|
156
|
+
* error text alone.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} endpoint - the endpoint the request used.
|
|
159
|
+
* @param {string} message - the human-readable failure.
|
|
160
|
+
* @param {unknown} [cause] - the underlying error, when any.
|
|
161
|
+
* @returns {WebError} a {@link WebError} with code `WEB_PROVIDER_ERROR`.
|
|
162
|
+
*/
|
|
163
|
+
function providerError(endpoint, message, cause) {
|
|
164
|
+
return new WebError(
|
|
165
|
+
`${message}\n\nThe web search request used endpoint ${JSON.stringify(endpoint)}. If that endpoint is not intended, set bing-search.endpoint in ~/.dsh/settings.yaml, or set the endpoint key of the bing-search cordis config. Only the user should choose or change the endpoint.`,
|
|
166
|
+
"WEB_PROVIDER_ERROR",
|
|
167
|
+
cause === undefined ? undefined : { cause }
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Throw the provider's stable cancellation error when the caller already aborted. */
|
|
172
|
+
function throwIfSearchAborted(signal) {
|
|
173
|
+
if (signal?.aborted === true) throw searchAborted(signal);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Build the provider's stable cancellation error while retaining the caller's reason. */
|
|
177
|
+
function searchAborted(signal, fallback) {
|
|
178
|
+
return new WebError("Bing free search aborted", "WEB_ABORTED", { cause: signal?.aborted === true ? signal.reason : fallback });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** True for a fetch/`AbortSignal` abort, surfaced as `WEB_ABORTED`. */
|
|
182
|
+
function isAbortError(error) {
|
|
183
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The Bing-backed free search provider.
|
|
188
|
+
*
|
|
189
|
+
* `available()` reports whether the configured endpoint is a parseable URL:
|
|
190
|
+
* the provider needs no key, credential, or environment setup, so a valid
|
|
191
|
+
* endpoint is the only precondition.
|
|
192
|
+
*/
|
|
193
|
+
class BingFreeSearchProvider {
|
|
194
|
+
id = PROVIDER_ID;
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {() => { endpoint: string, maxResults: number, ensearch: number }} resolveOptions -
|
|
198
|
+
* the options for the NEXT operation, snapshotted once at each operation's
|
|
199
|
+
* entry so one search never mixes two sections. A thunk rather than a value
|
|
200
|
+
* because the plugin's settings section can change between searches, and
|
|
201
|
+
* re-registering the provider to carry a new endpoint would make the seam's
|
|
202
|
+
* selection observable to the user as a flicker.
|
|
203
|
+
*/
|
|
204
|
+
constructor(resolveOptions) {
|
|
205
|
+
this.resolveOptions = resolveOptions;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
available() {
|
|
209
|
+
const options = this.resolveOptions();
|
|
210
|
+
return URL.canParse(options.endpoint) && Number.isInteger(options.maxResults) && options.maxResults > 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async search(request, signal) {
|
|
214
|
+
const options = this.resolveOptions();
|
|
215
|
+
const query = (request.query ?? "").trim();
|
|
216
|
+
if (query.length === 0) throw providerError(options.endpoint, "Bing free search requires a non-empty query");
|
|
217
|
+
throwIfSearchAborted(signal);
|
|
218
|
+
const params = new URLSearchParams({ q: query, ensearch: String(options.ensearch) });
|
|
219
|
+
const url = `${options.endpoint}?${params.toString()}`;
|
|
220
|
+
let response;
|
|
221
|
+
try {
|
|
222
|
+
response = await fetch(url, {
|
|
223
|
+
method: "GET",
|
|
224
|
+
redirect: "follow",
|
|
225
|
+
headers: {
|
|
226
|
+
"user-agent": USER_AGENT,
|
|
227
|
+
accept: "text/html,application/xhtml+xml",
|
|
228
|
+
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8"
|
|
229
|
+
},
|
|
230
|
+
...(signal !== undefined ? { signal } : {})
|
|
231
|
+
});
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
234
|
+
throw providerError(options.endpoint, `Bing free search request failed: ${String(error)}`, error);
|
|
235
|
+
}
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw providerError(options.endpoint, `Bing search returned HTTP ${response.status}`);
|
|
238
|
+
}
|
|
239
|
+
let html;
|
|
240
|
+
try {
|
|
241
|
+
html = await response.text();
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
244
|
+
throw providerError(options.endpoint, `Bing returned an unreadable response body: ${String(error)}`, error);
|
|
245
|
+
}
|
|
246
|
+
if (looksBlocked(html)) {
|
|
247
|
+
throw providerError(
|
|
248
|
+
options.endpoint,
|
|
249
|
+
"Bing answered with a captcha/gate instead of results. Retry later, or switch the searchProvider to another backend."
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const sources = parseBingHtml(html, options.maxResults);
|
|
253
|
+
return { sources, truncated: false };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
//#endregion
|
|
257
|
+
|
|
258
|
+
//#region index
|
|
259
|
+
/**
|
|
260
|
+
* Register a free Bing-backed provider in `ctx.web`. It calls Bing's public
|
|
261
|
+
* HTML endpoint and needs no API key, so `web_search` runs without any
|
|
262
|
+
* DeepSeek/model search quota. The provider id is `bing-free`; the bundle's
|
|
263
|
+
* `cordis.patch.yml` sets `web.searchProvider` to it.
|
|
264
|
+
*
|
|
265
|
+
* Optional settings expose the endpoint, language mode, and parse cap for
|
|
266
|
+
* deployments that need a different mirror, language, or page budget.
|
|
267
|
+
* @module dsh-bing-search
|
|
268
|
+
*/
|
|
269
|
+
|
|
270
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
271
|
+
const name = "bing-search";
|
|
272
|
+
|
|
273
|
+
/** The web seam this provider registers into. */
|
|
274
|
+
const inject = ["web"];
|
|
275
|
+
|
|
276
|
+
const Config = z.object({
|
|
277
|
+
endpoint: z.string().default(DEFAULT_ENDPOINT),
|
|
278
|
+
maxResults: z.number().step(1).min(1).default(DEFAULT_MAX_RESULTS),
|
|
279
|
+
ensearch: z.number().step(1).min(0).max(1).default(DEFAULT_ENSEARCH)
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
/** Settings namespace carrying this provider's endpoint and parse cap. */
|
|
283
|
+
const WEB_SEARCH_BING_SETTINGS_NAMESPACE = "bing-search";
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Project one resolved section into the options the provider serves its next
|
|
287
|
+
* search with: every value it reads is already fully defaulted.
|
|
288
|
+
*
|
|
289
|
+
* @param {unknown} config - the currently authoritative section.
|
|
290
|
+
* @returns {{ endpoint: string, maxResults: number, ensearch: number }} options for one search.
|
|
291
|
+
*/
|
|
292
|
+
function resolveOptions(config) {
|
|
293
|
+
return {
|
|
294
|
+
endpoint: config?.endpoint ?? DEFAULT_ENDPOINT,
|
|
295
|
+
maxResults: config?.maxResults ?? DEFAULT_MAX_RESULTS,
|
|
296
|
+
ensearch: config?.ensearch ?? DEFAULT_ENSEARCH
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** Register the free Bing search provider with `ctx.web`. */
|
|
301
|
+
function apply(ctx, config) {
|
|
302
|
+
let current = () => config;
|
|
303
|
+
ctx.inject(["settings"], (settingsCtx) => {
|
|
304
|
+
settingsCtx.settings.installSection(ctx, WEB_SEARCH_BING_SETTINGS_NAMESPACE, Config, config, {
|
|
305
|
+
setSource: (source) => {
|
|
306
|
+
current = source;
|
|
307
|
+
},
|
|
308
|
+
onChange: () => {}
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
ctx.web.registerSearchProvider(new BingFreeSearchProvider(() => resolveOptions(current())));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export {
|
|
315
|
+
Config,
|
|
316
|
+
DEFAULT_ENDPOINT,
|
|
317
|
+
DEFAULT_ENSEARCH,
|
|
318
|
+
DEFAULT_MAX_RESULTS,
|
|
319
|
+
PROVIDER_ID,
|
|
320
|
+
BingFreeSearchProvider,
|
|
321
|
+
WEB_SEARCH_BING_SETTINGS_NAMESPACE,
|
|
322
|
+
apply,
|
|
323
|
+
inject,
|
|
324
|
+
name
|
|
325
|
+
};
|
|
326
|
+
//#endregion
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the Bing-backed free web search provider bundle.
|
|
3
|
+
* @module dsh-bing-search
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** One normalized search hit returned by a provider. */
|
|
7
|
+
export interface WebSource {
|
|
8
|
+
url: string;
|
|
9
|
+
title?: string;
|
|
10
|
+
snippet?: string;
|
|
11
|
+
publishedAt?: string;
|
|
12
|
+
}
|
|
13
|
+
/** The request the web seam forwards to a search provider. */
|
|
14
|
+
export interface WebSearchRequest {
|
|
15
|
+
query: string;
|
|
16
|
+
maxResults?: number;
|
|
17
|
+
}
|
|
18
|
+
/** The normalized result a search provider resolves with. */
|
|
19
|
+
export interface WebSearchResult {
|
|
20
|
+
sources: WebSource[];
|
|
21
|
+
truncated: boolean;
|
|
22
|
+
}
|
|
23
|
+
/** The provider contract `ctx.web.registerSearchProvider` accepts. */
|
|
24
|
+
export interface WebSearchProvider {
|
|
25
|
+
readonly id: string;
|
|
26
|
+
available(): boolean;
|
|
27
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
28
|
+
}
|
|
29
|
+
/** Fully-defaulted options one search executes with. */
|
|
30
|
+
export interface BingFreeSearchOptions {
|
|
31
|
+
endpoint: string;
|
|
32
|
+
maxResults: number;
|
|
33
|
+
ensearch: number;
|
|
34
|
+
}
|
|
35
|
+
/** Stable provider id (bing-free). */
|
|
36
|
+
export declare const PROVIDER_ID: string;
|
|
37
|
+
/** Default public HTML search endpoint (cn.bing.com/search). */
|
|
38
|
+
export declare const DEFAULT_ENDPOINT: string;
|
|
39
|
+
/** Default ensearch param (0 = Chinese results). */
|
|
40
|
+
export declare const DEFAULT_ENSEARCH: number;
|
|
41
|
+
/** Default upper bound on parsed results per page. */
|
|
42
|
+
export declare const DEFAULT_MAX_RESULTS: number;
|
|
43
|
+
/** Settings namespace for this provider (`bing-search`). */
|
|
44
|
+
export declare const WEB_SEARCH_BING_SETTINGS_NAMESPACE: string;
|
|
45
|
+
/**
|
|
46
|
+
* The Bing-backed free provider, satisfying {@link WebSearchProvider}.
|
|
47
|
+
* `available()` reports whether the configured endpoint is a parseable URL —
|
|
48
|
+
* no key, credential, or environment setup is required.
|
|
49
|
+
*/
|
|
50
|
+
export declare class BingFreeSearchProvider implements WebSearchProvider {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
constructor(resolveOptions: () => BingFreeSearchOptions);
|
|
53
|
+
available(): boolean;
|
|
54
|
+
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
|
|
55
|
+
}
|
|
56
|
+
/** Cordis plugin name. */
|
|
57
|
+
export declare const name: string;
|
|
58
|
+
/** Services injected by this plugin. */
|
|
59
|
+
export declare const inject: string[];
|
|
60
|
+
/** Schema for the config / settings section. */
|
|
61
|
+
export declare const Config: unknown;
|
|
62
|
+
/** Cordis plugin apply entry: registers the provider with `ctx.web`. */
|
|
63
|
+
export declare function apply(ctx: any, config: any): void;
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-bing-search",
|
|
3
|
+
"description": "Free Bing-backed web search provider for DeepSeek Harness (DSH). No API key needed, uses cn.bing.com for mainland China accessibility.",
|
|
4
|
+
"version": "1.1.3",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"types": "lib/types/index.d.ts",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/whiteS18/dsh-bing-search.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/whiteS18/dsh-bing-search/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/whiteS18/dsh-bing-search#readme",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./lib/types/index.d.ts",
|
|
20
|
+
"default": "./lib/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib/index.js",
|
|
27
|
+
"lib/types",
|
|
28
|
+
"cordis.patch.yml",
|
|
29
|
+
"README.md",
|
|
30
|
+
"README_EN.md",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"CHANGELOG.md"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=18"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@deepseek-ai/dsh-settings": ">=0.1.5-rc.1 <0.2.0-0",
|
|
44
|
+
"@deepseek-ai/dsh-web": ">=0.1.5-rc.1 <0.2.0-0",
|
|
45
|
+
"@deepseek-ai/cordis": "^4.0.2"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"deepseek-harness",
|
|
52
|
+
"dsh",
|
|
53
|
+
"dsh-plugin",
|
|
54
|
+
"plugin",
|
|
55
|
+
"search",
|
|
56
|
+
"web-search",
|
|
57
|
+
"bing",
|
|
58
|
+
"free",
|
|
59
|
+
"cn-bing"
|
|
60
|
+
]
|
|
61
|
+
}
|