opencode-search-plugin 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,24 +2,24 @@
2
2
 
3
3
  **English** · [简体中文](README.zh-CN.md)
4
4
 
5
- Ten self-contained [OpenCode](https://opencode.ai) v2 plugins that register web search providers. Each file stands on its own: copy one into your plugins directory, or install the package to get all ten.
5
+ Eleven self-contained [OpenCode](https://opencode.ai) v2 plugins that register web search providers. Each file stands on its own: copy one into your plugins directory, or install the package to get all eleven.
6
6
 
7
- [AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
7
+ [AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [DeepSeek](https://api-docs.deepseek.com/guides/search/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
8
8
 
9
9
  ## Why this exists
10
10
 
11
11
  OpenCode v2 ships with five web search providers built in: `exa`, `firecrawl`, `parallel`, `tavily` and `tinyfish`. Outside that set, a much wider field of search APIs offers free quotas or free tiers — enough to run an agent's searches without paying for the privilege.
12
12
 
13
- This repository adds plugins for ten more of them, taking the pool from five providers to fifteen. AnySearch and Keenable run without a key at all; the other eight bind only when you supply one, so installing this package never routes a query to a provider that would immediately fail. Which of them are worth your time depends on the free tier each one currently offers, so check a provider's pricing before you rely on it.
13
+ This repository adds plugins for eleven more of them, taking the pool from five providers to sixteen. AnySearch and Keenable run without a key at all; the other nine bind only when you supply one, so installing this package never routes a query to a provider that would immediately fail. Which of them are worth your time depends on the free tier each one currently offers, so check a provider's pricing before you rely on it.
14
14
 
15
15
  ## Install
16
16
 
17
- Three equivalent ways to install; pick one. **npm is recommended**: a version published to the registry is immutable, so a pinned `@2.0.0` always installs the same bytes, whereas a GitHub tag can in principle be moved by a maintainer to point at a different commit. The first two install the same complete package (all ten); use the third for one or two files only.
17
+ Three equivalent ways to install; pick one. **npm is recommended**: a version published to the registry is immutable, so a pinned `@2.1.1` always installs the same bytes, whereas a GitHub tag can in principle be moved by a maintainer to point at a different commit. The first two install the same complete package (all eleven); use the third for one or two files only.
18
18
 
19
19
  ### 1. From npm (recommended)
20
20
 
21
21
  ```sh
22
- # add @2.0.0 to pin
22
+ # add @2.1.1 to pin
23
23
  opencode plugin add opencode-search-plugin
24
24
  ```
25
25
 
@@ -35,7 +35,7 @@ or declare it in `opencode.json(c)`:
35
35
  ### 2. From GitHub
36
36
 
37
37
  ```sh
38
- # add #v2.0.0 to pin
38
+ # add #v2.1.1 to pin
39
39
  opencode plugin add github:guotree/opencode-search-plugin
40
40
  ```
41
41
 
@@ -67,7 +67,7 @@ For a location that is not auto-discovered, list it explicitly:
67
67
 
68
68
  ### Skip the services you have no key for
69
69
 
70
- Loading the package registers all ten, but a provider without a key never joins the search pool — see [Keys](#keys). To keep them out of the registry entirely:
70
+ Loading the package registers all eleven, but a provider without a key never joins the search pool — see [Keys](#keys). To keep some out of the registry entirely, use either or both of these optional options:
71
71
 
72
72
  ```jsonc
73
73
  {
@@ -80,7 +80,31 @@ Loading the package registers all ten, but a provider without a key never joins
80
80
  }
81
81
  ```
82
82
 
83
- `disable` takes the provider IDs listed in the table below. AnySearch and Keenable need no key, so `disable` is the only way to keep those two out of the pool.
83
+ - `disable` is a deny-list: it never registers the IDs it names — the provider IDs in the table below. AnySearch and Keenable need no key, so `disable` is the only way to keep those two out of the pool.
84
+ - `enable` is an allow-list: only the IDs it names are registered, and everything else is skipped — `"enable": ["osp-brave", "osp-jina"]` runs those two alone.
85
+
86
+ When both are set, **`disable` wins on any overlap**. A per-provider exclusion is never overridden by a bulk allow-list:
87
+
88
+ ```jsonc
89
+ {
90
+ "options": {
91
+ "enable": ["osp-brave", "osp-jina", "osp-serper"],
92
+ "disable": ["osp-serper", "osp-youcom"]
93
+ // osp-serper stays out: disable overrides enable.
94
+ // osp-youcom was never allowed in; that entry is inert.
95
+ }
96
+ }
97
+ ```
98
+
99
+ Read as set arithmetic, the registered pool is `enable − disable`. The overlap drops out; a `disable` ID the allow-list never contained was never a candidate to begin with — redundant, but harmless and unreported. The overlap is the only redundancy that earns a warning, and the log names the IDs in it.
100
+
101
+ ```text
102
+ enable = {osp-brave, osp-jina, osp-serper, osp-keenable, osp-linkup}
103
+ disable = {osp-keenable, osp-linkup, osp-youcom}
104
+ pool = osp-brave, osp-jina -- enable minus disable
105
+ ```
106
+
107
+ Configuration that cannot take effect is reported in the log rather than silently dropped: an ID this package does not register (a typo) is named, as is an option that is not an array. An `enable` list that ends up naming no known provider leaves the pool empty — the log says so.
84
108
 
85
109
  ## Upgrade
86
110
 
@@ -93,7 +117,7 @@ opencode plugin update github:guotree/opencode-search-plugin
93
117
  opencode plugin update
94
118
  ```
95
119
 
96
- The target is the specifier that was configured: an npm install is named by its package name, a GitHub install answers only to its `github:` form — naming it by its bare package name fails with `Plugin is not configured`. Run `opencode plugin check` first to see what is outdated. An install without a pinned version follows new releases; a pinned one — `@2.0.0` on npm, `#v2.0.0` on GitHub — stays on its pin until you re-run `plugin add` with a newer pin or edit the specifier in `opencode.json(c)`. A locally copied file is upgraded by copying the new version over the old one.
120
+ The target is the specifier that was configured: an npm install is named by its package name, a GitHub install answers only to its `github:` form — naming it by its bare package name fails with `Plugin is not configured`. Run `opencode plugin check` first to see what is outdated. An install without a pinned version follows new releases; a pinned one — `@2.1.1` on npm, `#v2.1.1` on GitHub — stays on its pin until you re-run `plugin add` with a newer pin or edit the specifier in `opencode.json(c)`. A locally copied file is upgraded by copying the new version over the old one.
97
121
 
98
122
  ## Uninstall
99
123
 
@@ -104,7 +128,7 @@ opencode plugin remove opencode-search-plugin
104
128
  opencode plugin remove github:guotree/opencode-search-plugin
105
129
  ```
106
130
 
107
- The same rule: pass the specifier you installed with. The command edits the global configuration; if the plugin is instead a `plugins` entry in `opencode.json(c)` or a locally copied file, delete the entry or the copied `.ts` file. Saved `/connect` credentials are untouched — a reinstall finds them again — and to turn off a few providers instead of the whole plugin, use `disable` above.
131
+ The same rule: pass the specifier you installed with. The command edits the global configuration; if the plugin is instead a `plugins` entry in `opencode.json(c)` or a locally copied file, delete the entry or the copied `.ts` file. Saved `/connect` credentials are untouched — a reinstall finds them again — and to turn off a few providers instead of the whole plugin, use `enable`/`disable` above.
108
132
 
109
133
  ## Keys
110
134
 
@@ -127,12 +151,15 @@ Once keys are in place, pick how OpenCode chooses among them:
127
151
 
128
152
  ## Providers
129
153
 
130
- Every ID below carries an `osp-` prefix — this package's namespace. OpenCode's own providers register bare vendor names (`exa`, `tavily`), and a vendor's official plugin would do the same, so the prefix is what keeps those entries from colliding with ours — both in the websearch pool and in the `/connect` table, where credentials are keyed by integration ID. Use the prefixed form verbatim in `websearch.provider` and in `disable`.
154
+ Every ID below carries an `osp-` prefix — this package's namespace. OpenCode's own providers register bare vendor names (`exa`, `tavily`), and a vendor's official plugin would do the same, so the prefix is what keeps those entries from colliding with ours — both in the websearch pool and in the `/connect` table, where credentials are keyed by integration ID. Use the prefixed form verbatim in `websearch.provider` and in `enable`/`disable`.
155
+
156
+ The namespace also shows on the display name: `/connect` lists each of this package's entries with an `(osp)` marker — `DeepSeek (osp)` beside a built-in `DeepSeek` — so a key saved here cannot be mistaken for a built-in provider's. The websearch pool keeps the plain provider name, since the pool holds only search providers and the collision does not exist there.
131
157
 
132
158
  | Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
133
159
  | --- | --- | --- | --- | --- |
134
160
  | AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
135
161
  | Brave Search | `osp-brave` | `BRAVE_SEARCH_API_KEY` | no | Independent index, 20 results max |
162
+ | DeepSeek | `osp-deepseek` | `DEEPSEEK_API_KEY` | no | In-house index; $0.75 per 1,000 successful requests; 5 QPS cap |
136
163
  | Jina | `osp-jina` | `JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
137
164
  | Keenable | `osp-keenable` | `KEENABLE_API_KEY` | yes | 100,000 requests a month with a key; keyless tier is per IP |
138
165
  | Linkup | `osp-linkup` | `LINKUP_API_KEY` | no | `flash`/`fast`/`standard`/`deep` reasoning depth |
@@ -146,7 +173,7 @@ Every ID below carries an `osp-` prefix — this package's namespace. OpenCode's
146
173
 
147
174
  Knobs are environment variables, read once when OpenCode starts — change one and restart. Defaults match what each API documents, so leaving them unset is the right starting point.
148
175
 
149
- ### Shared by all ten
176
+ ### Shared by all eleven
150
177
 
151
178
  | Variable | Default | Meaning |
152
179
  | --- | --- | --- |
@@ -172,6 +199,12 @@ Knobs are environment variables, read once when OpenCode starts — change one a
172
199
  | `BRAVE_LANG` | — | Result language, e.g. `en`, `zh-hans`, `pt-br` |
173
200
  | `BRAVE_FRESHNESS` | — | `pd` (24h), `pw` (7d), `pm` (31d), `py` (365d), or `YYYY-MM-DDtoYYYY-MM-DD` |
174
201
 
202
+ ### DeepSeek
203
+
204
+ | Variable | Default | Meaning |
205
+ | --- | --- | --- |
206
+ | `DEEPSEEK_COUNT` | `10` | Results per search, clamped to 1–50 (DeepSeek's maximum). Price is per request, so more results cost nothing extra |
207
+
175
208
  ### Jina
176
209
 
177
210
  | Variable | Default | Meaning |
@@ -253,20 +286,20 @@ Dates are parsed from relative phrases (`3 hours ago`, `3小时前`), absolute f
253
286
  opencode plugin list
254
287
 
255
288
  # Which providers are actually in the pool?
256
- # A provider missing here has no key, or is listed in `disable`.
257
- # AnySearch and Keenable are always present, since neither needs one.
289
+ # A provider missing here has no key, or `enable`/`disable` kept it out.
290
+ # AnySearch and Keenable are always present, since neither needs a key.
258
291
  opencode api get "/api/websearch/provider"
259
292
  ```
260
293
 
261
294
  Also worth checking:
262
295
 
263
- - **A provider is missing from the pool** — it has no key. Run `/connect` or export its variable, then restart; credential changes made in `/connect` rebind without one.
296
+ - **A provider is missing from the pool** — it has no key, or `disable` (or a narrow `enable`) filtered it out. Run `/connect` or export its variable, then restart; credential changes made in `/connect` rebind without one. A name that should be there but is not is often a typo: the log flags unknown IDs and any `enable`/`disable` overlap.
264
297
  - **A search fails with `Keenable rate limited the request (HTTP 429)`** — the keyless pool is capped at 1,000 requests an hour and 10 a second per IP, shared with the rest of your egress address. Set `KEENABLE_API_KEY` to draw on your own allowance instead.
265
298
  - **A search fails with `Connect X in /connect, or set X_API_KEY`** — the key disappeared between binding and searching. Reconnect or re-export.
266
299
  - **Everything is slow or burning credits** — leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
267
300
  - **Nothing happens after editing a plugin file** — OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
268
301
 
269
- Logs live at `~/.local/share/opencode/log/opencode.log`; filter for `loading plugin` to see every plugin OpenCode resolved, with its entrypoint.
302
+ Logs live at `~/.local/share/opencode/log/opencode.log`; filter for `loading plugin` to see every plugin OpenCode resolved, with its entrypoint. Warnings this plugin emits about `enable`/`disable` land in the same file, carrying a `[opencode-search-plugin]` prefix.
270
303
 
271
304
  ## Development
272
305
 
package/README.zh-CN.md CHANGED
@@ -2,24 +2,24 @@
2
2
 
3
3
  # opencode-search-plugin
4
4
 
5
- 十个相互独立的 [OpenCode](https://opencode.ai) v2 插件,各自注册一个网页搜索 provider。每个文件都能单独使用:复制一个进你的插件目录,或者安装这个包一次拿全十个。
5
+ 十一个相互独立的 [OpenCode](https://opencode.ai) v2 插件,各自注册一个网页搜索 provider。每个文件都能单独使用:复制一个进你的插件目录,或者安装这个包一次拿全十一个。
6
6
 
7
- [AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
7
+ [AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [DeepSeek](https://api-docs.deepseek.com/guides/search/) · [Jina](https://jina.ai/) · [Keenable](https://keenable.ai/) · [Linkup](https://www.linkup.so/) · [Metaso](https://metaso.cn/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
8
8
 
9
9
  ## 为什么做这个插件
10
10
 
11
11
  OpenCode v2 内置了五个网页搜索 provider:`exa`、`firecrawl`、`parallel`、`tavily`、`tinyfish`。在这个范围之外,还有大量搜索 API 提供免费额度或免费层级,足以支撑 agent 的日常检索而不必为此付费。
12
12
 
13
- 本仓库为其中十家补上了插件,把可用池从五个 provider 扩到十五个。AnySearch 和 Keenable 无需任何 key 即可使用;其余八家只在你提供 key 之后才会绑定,因此装上这个包也不会把查询路由到一个注定失败的服务上。至于哪几家值得用,取决于它们各自当前的免费额度,长期依赖某家之前建议先确认它的定价。
13
+ 本仓库为其中十一家补上了插件,把可用池从五个 provider 扩到十六个。AnySearch 和 Keenable 无需任何 key 即可使用;其余九家只在你提供 key 之后才会绑定,因此装上这个包也不会把查询路由到一个注定失败的服务上。至于哪几家值得用,取决于它们各自当前的免费额度,长期依赖某家之前建议先确认它的定价。
14
14
 
15
15
  ## 安装
16
16
 
17
- 三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@2.0.0` 后每次安装都拿到同一份字节;GitHub 的 tag 在理论上可以被维护者移动、指向别的 commit。前两种装的是同一个完整包(十个全有),只想用一两个就用第三种。
17
+ 三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@2.1.1` 后每次安装都拿到同一份字节;GitHub 的 tag 在理论上可以被维护者移动、指向别的 commit。前两种装的是同一个完整包(十一个全有),只想用一两个就用第三种。
18
18
 
19
19
  ### 1. 从 npm 安装(推荐)
20
20
 
21
21
  ```sh
22
- # 追加 @2.0.0 可固定到该版本
22
+ # 追加 @2.1.1 可固定到该版本
23
23
  opencode plugin add opencode-search-plugin
24
24
  ```
25
25
 
@@ -35,7 +35,7 @@ opencode plugin add opencode-search-plugin
35
35
  ### 2. 从 GitHub 安装
36
36
 
37
37
  ```sh
38
- # 追加 #v2.0.0 可固定到该 tag
38
+ # 追加 #v2.1.1 可固定到该 tag
39
39
  opencode plugin add github:guotree/opencode-search-plugin
40
40
  ```
41
41
 
@@ -67,7 +67,7 @@ opencode plugin add github:guotree/opencode-search-plugin
67
67
 
68
68
  ### 跳过你没有 key 的服务
69
69
 
70
- 装包会注册全部十家,但没有 key 的 provider 永远不会进入搜索池——见下文[密钥](#密钥)。想让它们连注册表都不出现:
70
+ 装包会注册全部十一家,但没有 key 的 provider 永远不会进入搜索池——见下文[密钥](#密钥)。想让它们连注册表都不出现,可以单独或同时使用下面两个可选选项:
71
71
 
72
72
  ```jsonc
73
73
  {
@@ -80,7 +80,31 @@ opencode plugin add github:guotree/opencode-search-plugin
80
80
  }
81
81
  ```
82
82
 
83
- `disable` 填的是下表中的 provider ID。AnySearch 和 Keenable 不需要 key,想把这两家也排除在搜索池之外,只能用 `disable`。
83
+ - `disable` 是黑名单:它列出的 provider ID(见下文[支持的服务](#支持的服务)的表格)永远不会注册。AnySearch 和 Keenable 不需要 key,想把这两家也排除在搜索池之外,只能用 `disable`。
84
+ - `enable` 是白名单:只注册列出的 ID,其余一律跳过——`"enable": ["osp-brave", "osp-jina"]` 表示只跑这两家。
85
+
86
+ 两者同时配置时,**交集由 `disable` 胜出**:逐家的排除绝不会被批量白名单覆盖。
87
+
88
+ ```jsonc
89
+ {
90
+ "options": {
91
+ "enable": ["osp-brave", "osp-jina", "osp-serper"],
92
+ "disable": ["osp-serper", "osp-youcom"]
93
+ // osp-serper 仍然不注册:disable 压过 enable。
94
+ // osp-youcom 本来就不在白名单里,这条是冗余的。
95
+ }
96
+ }
97
+ ```
98
+
99
+ 用集合运算来读:实际注册的池 = `enable − disable`。交集被剔除;`disable` 里不在 `enable` 中的 ID 从一开始就不在候选内——属于无害的冗余,不会被报告。唯一会触发警告的冗余是交集,日志会点出其中的 ID。
100
+
101
+ ```text
102
+ enable = {osp-brave, osp-jina, osp-serper, osp-keenable, osp-linkup}
103
+ disable = {osp-keenable, osp-linkup, osp-youcom}
104
+ pool = osp-brave, osp-jina -- enable 减 disable
105
+ ```
106
+
107
+ 无法生效的配置会在日志里提示,而不是被悄悄丢弃:写错的 ID(本包没注册的名字)会被点名,选项不是数组也会被点名。`enable` 如果最终一个已知 provider 都没匹配上,搜索池会是空的——日志里会写明这一点。
84
108
 
85
109
  ## 升级
86
110
 
@@ -93,7 +117,7 @@ opencode plugin update github:guotree/opencode-search-plugin
93
117
  opencode plugin update
94
118
  ```
95
119
 
96
- 升级目标用的是配置时的说明符:npm 安装用包名,GitHub 安装只认自己的 `github:` 形式——用裸包名会报 `Plugin is not configured`。先跑 `opencode plugin check` 查看有哪些过期。未固定版本的安装会跟上新版本;固定过的——npm 形式的 `@2.0.0`、GitHub 形式的 `#v2.0.0`——会停在原地,直到你用新 pin 重新执行 `plugin add`,或直接修改 `opencode.json(c)` 里的说明符。本地复制的文件则用新版本覆盖旧文件来升级。
120
+ 升级目标用的是配置时的说明符:npm 安装用包名,GitHub 安装只认自己的 `github:` 形式——用裸包名会报 `Plugin is not configured`。先跑 `opencode plugin check` 查看有哪些过期。未固定版本的安装会跟上新版本;固定过的——npm 形式的 `@2.1.1`、GitHub 形式的 `#v2.1.1`——会停在原地,直到你用新 pin 重新执行 `plugin add`,或直接修改 `opencode.json(c)` 里的说明符。本地复制的文件则用新版本覆盖旧文件来升级。
97
121
 
98
122
  ## 卸载
99
123
 
@@ -104,7 +128,7 @@ opencode plugin remove opencode-search-plugin
104
128
  opencode plugin remove github:guotree/opencode-search-plugin
105
129
  ```
106
130
 
107
- 同上:传入安装时使用的说明符。该命令修改的是全局配置;若插件是写在 `opencode.json(c)` 的 `plugins` 条目里或本地复制的文件,删掉对应条目、或删掉复制的 `.ts` 文件即可。`/connect` 里保存的密钥不受影响——重新装回就能继续用——只想关掉其中几家而不是整个插件,用上文的 `disable`。
131
+ 同上:传入安装时使用的说明符。该命令修改的是全局配置;若插件是写在 `opencode.json(c)` 的 `plugins` 条目里或本地复制的文件,删掉对应条目、或删掉复制的 `.ts` 文件即可。`/connect` 里保存的密钥不受影响——重新装回就能继续用——只想关掉其中几家而不是整个插件,用上文的 `enable`/`disable`。
108
132
 
109
133
  ## 密钥
110
134
 
@@ -127,12 +151,15 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
127
151
 
128
152
  ## 支持的服务
129
153
 
130
- 下表每个 ID 都带 `osp-` 前缀——这是本包的命名空间。OpenCode 内置 provider 用的是裸厂商名(`exa`、`tavily`),厂商自己的插件也会注册裸名(Keenable 就已经有一个),所以前缀是避免撞名的唯一保障:既避开搜索池里的重复,也避开 `/connect` 里按 integration ID 存凭据的那张表。在 `websearch.provider` 和 `disable` 里请原样使用带前缀的 ID。
154
+ 下表每个 ID 都带 `osp-` 前缀——这是本包的命名空间。OpenCode 内置 provider 用的是裸厂商名(`exa`、`tavily`),厂商自己的插件也会注册裸名(Keenable 就已经有一个),所以前缀是避免撞名的唯一保障:既避开搜索池里的重复,也避开 `/connect` 里按 integration ID 存凭据的那张表。在 `websearch.provider` 和 `enable`/`disable` 里请原样使用带前缀的 ID。
155
+
156
+ 命名空间同样体现在显示名上:`/connect` 里本包的每个条目都带 `(osp)` 标记——内置的 `DeepSeek` 旁边是 `DeepSeek (osp)`——存在这里的 key 不会和内置 provider 的混淆。搜索池里仍用纯品牌名,因为池中只有搜索 provider,不存在撞名问题。
131
157
 
132
158
  | 服务 | ID | 密钥(环境变量 / `/connect`) | 匿名可用 | 说明 |
133
159
  | --- | --- | --- | --- | --- |
134
160
  | AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | 是 | 按 IP 的免费额度;意图路由,`cn` / `intl` 分区 |
135
161
  | Brave Search | `osp-brave` | `BRAVE_SEARCH_API_KEY` | 否 | 独立索引,最多 20 条结果 |
162
+ | DeepSeek | `osp-deepseek` | `DEEPSEEK_API_KEY` | 否 | 自研搜索引擎;每千次成功请求 5 元;限速 5 QPS |
136
163
  | Jina | `osp-jina` | `JINA_API_KEY` | 否 | 页面正文按 token 计费,因此默认不取 |
137
164
  | Keenable | `osp-keenable` | `KEENABLE_API_KEY` | 是 | 有 key 每月 100,000 次免费请求;无 key 层级按 IP 限速 |
138
165
  | Linkup | `osp-linkup` | `LINKUP_API_KEY` | 否 | `flash` / `fast` / `standard` / `deep` 四档推理深度 |
@@ -146,7 +173,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
146
173
 
147
174
  所有配置项都是环境变量,OpenCode 启动时读取一次——改了要重启。默认值取自各家 API 文档,所以一开始什么都不设就是合理的起点。
148
175
 
149
- ### 十家通用
176
+ ### 十一家通用
150
177
 
151
178
  | 变量 | 默认值 | 含义 |
152
179
  | --- | --- | --- |
@@ -172,6 +199,12 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
172
199
  | `BRAVE_LANG` | — | 结果语言,例如 `en`、`zh-hans`、`pt-br` |
173
200
  | `BRAVE_FRESHNESS` | — | `pd`(24 小时)、`pw`(7 天)、`pm`(31 天)、`py`(365 天),或 `YYYY-MM-DDtoYYYY-MM-DD` |
174
201
 
202
+ ### DeepSeek
203
+
204
+ | 变量 | 默认值 | 含义 |
205
+ | --- | --- | --- |
206
+ | `DEEPSEEK_COUNT` | `10` | 每次搜索的结果数,限制在 1–50(DeepSeek 的上限)。按请求计费,条数增多不加价 |
207
+
175
208
  ### Jina
176
209
 
177
210
  | 变量 | 默认值 | 含义 |
@@ -253,20 +286,20 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
253
286
  opencode plugin list
254
287
 
255
288
  # 搜索池里到底有哪些 provider?
256
- # 这里少了一家,说明它没有 key,或者被写进了 `disable`。
289
+ # 这里少了一家,说明它没有 key,或者被 `enable`/`disable` 排除了。
257
290
  # AnySearch 和 Keenable 永远在列,因为它们都不需要 key。
258
291
  opencode api get "/api/websearch/provider"
259
292
  ```
260
293
 
261
294
  另外几个值得检查的地方:
262
295
 
263
- - **某家 provider 不在池里** —— 它没有 key。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。
296
+ - **某家 provider 不在池里** —— 它没有 key,或者被 `disable`(或收窄后的 `enable`)过滤掉了。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。本该在却不在的名字多半是拼写错误:日志会点出无法识别的 ID 和 `enable`/`disable` 的交集。
264
297
  - **搜索报错 `Keenable rate limited the request (HTTP 429)`** —— 无 key 层级每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的其他人共用。改用 `KEENABLE_API_KEY` 就会记到你自己的额度上。
265
298
  - **搜索报错 `Connect X in /connect, or set X_API_KEY`** —— key 在绑定和搜索之间消失了。重新连接或重新导出。
266
299
  - **一切都很慢、或者积分烧得飞快** —— 把 `*_CACHE_TTL` 保持在默认值。时间窗内的重复查询由内存直接返回。
267
300
  - **改了插件文件却没反应** —— OpenCode 会监视插件并自动重载;如果没有,执行 `opencode service restart`。
268
301
 
269
- 日志在 `~/.local/share/opencode/log/opencode.log`;过滤 `loading plugin` 可以看到 OpenCode 解析到的每个插件及其入口文件。
302
+ 日志在 `~/.local/share/opencode/log/opencode.log`;过滤 `loading plugin` 可以看到 OpenCode 解析到的每个插件及其入口文件。本插件关于 `enable`/`disable` 的警告也落在同一个文件里,带 `[opencode-search-plugin]` 前缀。
270
303
 
271
304
  ## 开发
272
305
 
package/index.ts CHANGED
@@ -4,6 +4,7 @@ import type { Scope } from "effect"
4
4
 
5
5
  import { effect as anysearch } from "./plugins/anysearch-search"
6
6
  import { effect as brave } from "./plugins/brave-search"
7
+ import { effect as deepseek } from "./plugins/deepseek-search"
7
8
  import { effect as jina } from "./plugins/jina-search"
8
9
  import { effect as keenable } from "./plugins/keenable-search"
9
10
  import { effect as linkup } from "./plugins/linkup-search"
@@ -16,18 +17,19 @@ import { effect as youcom } from "./plugins/youcom-search"
16
17
  type ProviderEffect = (ctx: Plugin.Context) => Effect.Effect<void, never, Scope.Scope>
17
18
 
18
19
  /**
19
- * The ten providers this package registers, in alphabetical order. Each entry
20
+ * The eleven providers this package registers, in alphabetical order. Each entry
20
21
  * is the same effect its standalone file uses as a default export, so a file
21
22
  * copied out of this repository behaves exactly as it does here.
22
23
  *
23
24
  * The key of each entry is the websearch provider ID -- the one that appears in
24
- * `/api/websearch/provider`, in `websearch.provider`, and in the `disable`
25
- * option below. Renaming one would strand existing configuration. Each is
26
- * prefixed `osp-` to namespace it to this package; see CONTRIBUTING.md.
25
+ * `/api/websearch/provider`, in `websearch.provider`, and in the `enable` and
26
+ * `disable` options below. Renaming one would strand existing configuration.
27
+ * Each is prefixed `osp-` to namespace it to this package; see CONTRIBUTING.md.
27
28
  */
28
29
  const PROVIDERS: ReadonlyArray<readonly [id: string, run: ProviderEffect]> = [
29
30
  ["osp-anysearch", anysearch],
30
31
  ["osp-brave", brave],
32
+ ["osp-deepseek", deepseek],
31
33
  ["osp-jina", jina],
32
34
  ["osp-keenable", keenable],
33
35
  ["osp-linkup", linkup],
@@ -38,24 +40,82 @@ const PROVIDERS: ReadonlyArray<readonly [id: string, run: ProviderEffect]> = [
38
40
  ["osp-youcom", youcom],
39
41
  ]
40
42
 
43
+ /** Every ID this package registers, so a typo in an option can be named. */
44
+ const KNOWN_IDS: ReadonlySet<string> = new Set(PROVIDERS.map(([id]) => id))
45
+
46
+ const WARNING = "[opencode-search-plugin]"
47
+
41
48
  /**
42
- * Reads the `disable` plugin option. A key-requiring provider that is disabled
43
- * here is never registered, which is how a user trims the pool down to the
44
- * services they actually hold keys for.
49
+ * Reads the `enable` or `disable` plugin option: an array of provider IDs.
50
+ * Anything that cannot take effect is reported on stderr rather than acted on
51
+ * -- a wrong shape is ignored outright, and an ID naming no provider here is
52
+ * dropped. A typo in a knob must not cost the user every provider, but it must
53
+ * not vanish silently either: `disable` shrinks the pool by one entry when an
54
+ * ID misses, while `enable` can shrink it to nothing.
45
55
  */
46
- function disabledProviders(ctx: Plugin.Context): ReadonlySet<string> {
47
- const requested: unknown = ctx.options["disable"]
48
- if (!Array.isArray(requested)) return new Set()
49
- return new Set(requested.filter((entry): entry is string => typeof entry === "string"))
56
+ function readOption(
57
+ ctx: Plugin.Context,
58
+ option: "enable" | "disable",
59
+ ): ReadonlySet<string> | undefined {
60
+ const requested: unknown = ctx.options[option]
61
+ if (requested === undefined) return undefined
62
+ if (!Array.isArray(requested)) {
63
+ console.warn(`${WARNING} ${option} must be an array of provider IDs; ignoring it`)
64
+ return undefined
65
+ }
66
+ const ids = new Set(requested.filter((entry): entry is string => typeof entry === "string"))
67
+ const unknown = [...ids].filter((id) => !KNOWN_IDS.has(id))
68
+ if (unknown.length > 0) {
69
+ console.warn(
70
+ `${WARNING} ${option} names no provider this package registers: ${unknown.join(", ")}`,
71
+ )
72
+ }
73
+ return ids
74
+ }
75
+
76
+ /**
77
+ * Resolves both options into the sets the registration loop reads. `undefined`
78
+ * from `readOption` means "no opinion": without `enable` every provider is a
79
+ * candidate, and without `disable` nothing is vetoed, so each option on its
80
+ * own behaves exactly as it did before the other existed.
81
+ */
82
+ function registrationOptions(ctx: Plugin.Context): {
83
+ readonly allowed: ReadonlySet<string> | undefined
84
+ readonly disabled: ReadonlySet<string>
85
+ } {
86
+ const allowed = readOption(ctx, "enable")
87
+ const disabled = readOption(ctx, "disable") ?? new Set<string>()
88
+
89
+ // Deny wins on the overlap: a provider `disable` names stays out even when
90
+ // `enable` lists it, so a per-provider exclusion is never overridden by a
91
+ // bulk allow-list. The overlap is named because the config is redundant,
92
+ // not fatal -- loading continues under the deny.
93
+ const overlap = allowed ? [...allowed].filter((id) => disabled.has(id)) : []
94
+ if (overlap.length > 0) {
95
+ console.warn(
96
+ `${WARNING} disable overrides enable for: ${overlap.join(", ")} -- those providers stay out`,
97
+ )
98
+ }
99
+
100
+ // An allow-list that reaches no known provider registers nothing at all. The
101
+ // registration loop stays faithful to that, but the reason belongs in the
102
+ // log: an empty search pool is otherwise indistinguishable from a loaded
103
+ // plugin that silently did nothing.
104
+ if (allowed !== undefined && ![...allowed].some((id) => KNOWN_IDS.has(id))) {
105
+ console.warn(`${WARNING} enable lists no known provider; the search pool will be empty`)
106
+ }
107
+
108
+ return { allowed, disabled }
50
109
  }
51
110
 
52
111
  export default Plugin.define({
53
112
  id: "opencode-search-plugin",
54
113
  effect: (ctx) =>
55
114
  Effect.gen(function* () {
56
- const disabled = disabledProviders(ctx)
115
+ const { allowed, disabled } = registrationOptions(ctx)
57
116
  for (const [id, run] of PROVIDERS) {
58
117
  if (disabled.has(id)) continue
118
+ if (allowed !== undefined && !allowed.has(id)) continue
59
119
  yield* run(ctx)
60
120
  }
61
121
  }),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-search-plugin",
3
- "version": "2.0.0",
4
- "description": "Ten self-contained websearch provider plugins for OpenCode v2: AnySearch, Brave, Jina, Keenable, Linkup, Metaso, Querit, SerpApi, Serper and You.com.",
3
+ "version": "2.1.1",
4
+ "description": "Eleven self-contained websearch provider plugins for OpenCode v2: AnySearch, Brave, DeepSeek, Jina, Keenable, Linkup, Metaso, Querit, SerpApi, Serper and You.com.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -113,7 +113,7 @@ export const effect = (ctx: Plugin.Context) =>
113
113
  // Make "AnySearch" show up in /connect with an API-key method.
114
114
  yield* ctx.integration.transform((editor) => {
115
115
  editor.update(ID, (integration) => {
116
- integration.name = "AnySearch"
116
+ integration.name = "AnySearch (osp)"
117
117
  })
118
118
  editor.method.update({
119
119
  integrationID: ID,
@@ -148,7 +148,7 @@ export const effect = (ctx: Plugin.Context) =>
148
148
  // Make "Brave Search" show up in /connect with an API-key method.
149
149
  yield* ctx.integration.transform((editor) => {
150
150
  editor.update(ID, (integration) => {
151
- integration.name = "Brave Search"
151
+ integration.name = "Brave Search (osp)"
152
152
  })
153
153
  editor.method.update({
154
154
  integrationID: ID,
@@ -0,0 +1,270 @@
1
+ import { Plugin } from "@opencode/plugin/effect"
2
+ import { Effect, Schema, Stream } from "effect"
3
+
4
+ // DeepSeek opened the search engine behind its own app to developers on
5
+ // 2026-10-17. Billing is per successful request -- not per token, not per
6
+ // returned result -- so `count` costs nothing extra and every repeat query the
7
+ // cache answers is a request not billed at all. The content field is
8
+ // `summary`: the API documents it as 600-800 characters "intended to be read
9
+ // by a model", which is exactly what an agent consumes. `content` would only
10
+ // fill with whole-page text under `need_content`, which bloats the agent's
11
+ // context for no gain. Fields absent from a hit map the way the other
12
+ // providers map them: a missing title falls back to the URL, a missing
13
+ // summary to an empty string.
14
+ const DeepSeekResponse = Schema.Struct({
15
+ results: Schema.optional(
16
+ Schema.Array(
17
+ Schema.Struct({
18
+ title: Schema.optional(Schema.String),
19
+ url: Schema.String,
20
+ summary: Schema.optional(Schema.String),
21
+ // RFC 3339 in UTC, or null when the publication time is unknown.
22
+ date: Schema.optional(Schema.NullOr(Schema.String)),
23
+ }),
24
+ ),
25
+ ),
26
+ })
27
+
28
+ // The /connect integration ID, and the websearch provider ID it binds to.
29
+ // The `osp-` prefix is this package's namespace: OpenCode's built-ins and a
30
+ // vendor plugin's own entry are bare vendor names, so `deepseek` would collide
31
+ // with them, and with the opencode.db row keyed by integration ID.
32
+ const ID = "osp-deepseek"
33
+
34
+ function envInt(name: string, fallback: number): number {
35
+ const raw = (process.env[name] ?? "").trim()
36
+ if (raw === "") return fallback
37
+ const value = Number(raw)
38
+ return Number.isFinite(value) && value >= 0 ? value : fallback
39
+ }
40
+
41
+ // Search engines hand out dates in three flavours: relative ("28 minutes ago"),
42
+ // absolute ("Sep 30, 2026" in the US, "31 Mar 2026" elsewhere), and non-English
43
+ // relative ("4天前"). Relative values are rounded upstream, so month/year use
44
+ // 30/365 days. ISO 8601 timestamps are accepted as-is -- DeepSeek emits only
45
+ // that shape, RFC 3339 with a time component.
46
+ // See https://serpapi.com/blog/how-to-parse-dates-from-google-search/
47
+ const RELATIVE_MS: Record<string, number> = {
48
+ minute: 60_000,
49
+ hour: 3_600_000,
50
+ day: 86_400_000,
51
+ week: 604_800_000,
52
+ month: 2_592_000_000,
53
+ year: 31_536_000_000,
54
+ 分钟: 60_000,
55
+ 小时: 3_600_000,
56
+ 天: 86_400_000,
57
+ 周: 604_800_000,
58
+ 个月: 2_592_000_000,
59
+ 月: 2_592_000_000,
60
+ 年: 31_536_000_000,
61
+ }
62
+
63
+ const RELATIVE_DATE =
64
+ /^(\d+)\s*(minute|hour|day|week|month|year|分钟|小时|天|周|个月|月|年)s?\s*(?:ago|前)$/i
65
+
66
+ const ABSOLUTE_DATE = [
67
+ /^\d{1,2}\s+[A-Za-z]{3,9}\s+\d{4}$/, // "3 Jan 2022", "31 Mar 2026"
68
+ /^[A-Za-z]{3,9}\s+\d{1,2},?\s+\d{4}$/, // "Sep 30, 2026", "Jan 27, 2025"
69
+ /^\d{4}-\d{2}-\d{2}$/, // "2026-10-16"
70
+ // ISO 8601 with a time component, which some providers return verbatim.
71
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?$/,
72
+ ]
73
+
74
+ function parseSearchDate(value: string | undefined): number {
75
+ const text = value?.trim()
76
+ if (!text) return NaN
77
+
78
+ const relative = RELATIVE_DATE.exec(text)
79
+ if (relative) {
80
+ const unit = relative[2] !== undefined ? RELATIVE_MS[relative[2].toLowerCase()] : undefined
81
+ const amount = Number(relative[1])
82
+ if (unit === undefined || !Number.isFinite(amount)) return NaN
83
+ return Date.now() - amount * unit
84
+ }
85
+
86
+ // Only trust Date.parse on shapes these providers actually emit -- its lenient
87
+ // parser invents a year for strings like "Nov 1", which would date the result
88
+ // to 2001.
89
+ return ABSOLUTE_DATE.some((shape) => shape.test(text)) ? Date.parse(text) : NaN
90
+ }
91
+
92
+ function toResult(item: {
93
+ title?: string | undefined
94
+ url: string
95
+ summary?: string | undefined
96
+ date?: string | null | undefined
97
+ }) {
98
+ const published = parseSearchDate(item.date ?? undefined)
99
+ return {
100
+ url: item.url,
101
+ title: item.title || item.url,
102
+ content: item.summary ?? "",
103
+ time: Number.isFinite(published) ? { published } : {},
104
+ }
105
+ }
106
+
107
+ type SearchResult = ReturnType<typeof toResult>
108
+
109
+ // DeepSeek charges per successful search and has no anonymous tier. A repeat
110
+ // query is a fresh billed dispatch, so responses are cached per normalized
111
+ // query. Knobs are read once at load time, restart after changing them.
112
+ //
113
+ // DEEPSEEK_CACHE_TTL ms to reuse a response for the same query (0 disables)
114
+ // DEEPSEEK_COUNT results per search, clamped to 1-50
115
+ // DEEPSEEK_API_KEY required; from https://platform.deepseek.com
116
+ const CACHE_TTL = envInt("DEEPSEEK_CACHE_TTL", 600_000)
117
+ const COUNT = Math.min(Math.max(envInt("DEEPSEEK_COUNT", 10), 1), 50)
118
+
119
+ const CACHE_LIMIT = 200
120
+ const cache = new Map<string, { at: number; results: SearchResult[] }>()
121
+
122
+ function cacheKey(query: string): string {
123
+ return query.trim().replace(/\s+/g, " ")
124
+ }
125
+
126
+ function readCache(queryKey: string): SearchResult[] | undefined {
127
+ if (CACHE_TTL <= 0) return undefined
128
+ const hit = cache.get(queryKey)
129
+ return hit && Date.now() - hit.at < CACHE_TTL ? hit.results : undefined
130
+ }
131
+
132
+ function writeCache(queryKey: string, results: SearchResult[]): void {
133
+ if (CACHE_TTL <= 0) return
134
+ cache.set(queryKey, { at: Date.now(), results })
135
+ if (cache.size > CACHE_LIMIT) {
136
+ const oldest = cache.keys().next().value
137
+ if (oldest !== undefined) cache.delete(oldest)
138
+ }
139
+ }
140
+
141
+ export const effect = (ctx: Plugin.Context) =>
142
+ Effect.gen(function* () {
143
+ // Make "DeepSeek" show up in /connect with an API-key method.
144
+ yield* ctx.integration.transform((editor) => {
145
+ editor.update(ID, (integration) => {
146
+ integration.name = "DeepSeek (osp)"
147
+ })
148
+ editor.method.update({
149
+ integrationID: ID,
150
+ method: { type: "key", label: "DeepSeek API key" },
151
+ })
152
+ editor.method.update({
153
+ integrationID: ID,
154
+ method: { type: "env", names: ["DEEPSEEK_API_KEY"] },
155
+ })
156
+ })
157
+
158
+ // The host instantiates this API's failure channel as `unknown`, which would
159
+ // make the whole plugin effect unrunnable. A credential read must never take
160
+ // the plugin down: a failed read degrades to "no stored key", and the
161
+ // environment fallback below applies exactly as it does when the credential
162
+ // simply does not exist.
163
+ const soften = <A>(read: Effect.Effect<A, unknown>): Effect.Effect<A | undefined, never> =>
164
+ read.pipe(Effect.catch(() => Effect.succeed(undefined)))
165
+
166
+ // The key comes from the /connect credential, falling back to the
167
+ // environment. Resolved per call so a rotated key needs no restart.
168
+ const resolveKey = () =>
169
+ Effect.gen(function* () {
170
+ const connection = yield* soften(ctx.integration.connection.active(ID))
171
+ const credential = connection
172
+ ? yield* soften(ctx.integration.connection.resolve(connection))
173
+ : undefined
174
+ return credential?.type === "key" ? credential.key : process.env.DEEPSEEK_API_KEY
175
+ })
176
+
177
+ // DeepSeek is bound to websearch only while a key exists. Deleting the key
178
+ // unbinds the provider, so `random` can never pick a keyless DeepSeek.
179
+ // The transform replays on reload and reads `bound` as it stands then.
180
+ let bound = Boolean(yield* resolveKey())
181
+
182
+ yield* ctx.websearch.transform((editor) => {
183
+ if (!bound) return
184
+ editor.add({
185
+ id: ID,
186
+ name: "DeepSeek",
187
+ execute: ({ query }) =>
188
+ Effect.gen(function* () {
189
+ const queryKey = cacheKey(query)
190
+ const cached = readCache(queryKey)
191
+ if (cached) return cached
192
+
193
+ // Rechecked per call: binding can lag a key deletion by the
194
+ // event round-trip, so this keeps that window explicit.
195
+ const key = yield* resolveKey()
196
+ if (!key) {
197
+ return yield* Effect.fail(
198
+ new Error("Connect DeepSeek in /connect, or set DEEPSEEK_API_KEY"),
199
+ )
200
+ }
201
+
202
+ const response = yield* Effect.tryPromise({
203
+ try: () =>
204
+ fetch("https://api.deepseek.com/v1/tools/search", {
205
+ method: "POST",
206
+ headers: {
207
+ Accept: "application/json",
208
+ "Content-Type": "application/json",
209
+ Authorization: "Bearer " + key,
210
+ },
211
+ body: JSON.stringify({
212
+ query: queryKey,
213
+ count: COUNT,
214
+ }),
215
+ }),
216
+ catch: (error) => new Error("DeepSeek request failed: " + String(error)),
217
+ })
218
+
219
+ if (!response.ok) {
220
+ const detail = yield* Effect.tryPromise({
221
+ try: () => response.text(),
222
+ catch: () => new Error("response body unavailable"),
223
+ })
224
+ return yield* Effect.fail(
225
+ new Error("DeepSeek API error " + response.status + ": " + detail),
226
+ )
227
+ }
228
+
229
+ const json = yield* Effect.tryPromise({
230
+ try: () => response.json(),
231
+ catch: (error) => new Error("Invalid DeepSeek response: " + String(error)),
232
+ })
233
+
234
+ const body = yield* Schema.decodeUnknownEffect(DeepSeekResponse)(json)
235
+ const results = (body.results ?? []).map(toResult)
236
+
237
+ // DeepSeek charges per dispatch and does not bill failures or empty
238
+ // results, so a hit that never enters the cache is a query paid for
239
+ // again on every repeat.
240
+ writeCache(queryKey, results)
241
+ return results
242
+ }),
243
+ })
244
+ })
245
+
246
+ // Rebind on credential changes: connect, delete, or rotate a key and the
247
+ // provider joins or leaves the websearch pool without a restart. Env-var
248
+ // keys cannot change under a running process, so they bind at startup.
249
+ yield* Effect.forkScoped(
250
+ ctx.event.subscribe().pipe(
251
+ Stream.filter(
252
+ (event) =>
253
+ event.type === "credential.updated" ||
254
+ event.type === "credential.switched" ||
255
+ event.type === "integration.updated",
256
+ ),
257
+ Stream.mapEffect(() =>
258
+ Effect.gen(function* () {
259
+ const next = Boolean(yield* resolveKey())
260
+ if (next === bound) return
261
+ bound = next
262
+ yield* ctx.websearch.reload()
263
+ }),
264
+ ),
265
+ Stream.runDrain,
266
+ ),
267
+ )
268
+ })
269
+
270
+ export default Plugin.define({ id: "deepseek-search", effect })
@@ -112,7 +112,7 @@ export const effect = (ctx: Plugin.Context) =>
112
112
  // Make "Jina" show up in /connect with an API-key method.
113
113
  yield* ctx.integration.transform((editor) => {
114
114
  editor.update(ID, (integration) => {
115
- integration.name = "Jina"
115
+ integration.name = "Jina (osp)"
116
116
  })
117
117
  editor.method.update({
118
118
  integrationID: ID,
@@ -163,7 +163,7 @@ export const effect = (ctx: Plugin.Context) =>
163
163
  // Make "Keenable" show up in /connect with an API-key method.
164
164
  yield* ctx.integration.transform((editor) => {
165
165
  editor.update(ID, (integration) => {
166
- integration.name = "Keenable"
166
+ integration.name = "Keenable (osp)"
167
167
  })
168
168
  editor.method.update({
169
169
  integrationID: ID,
@@ -88,7 +88,7 @@ export const effect = (ctx: Plugin.Context) =>
88
88
  // Make "Linkup" show up in /connect with an API-key method.
89
89
  yield* ctx.integration.transform((editor) => {
90
90
  editor.update(ID, (integration) => {
91
- integration.name = "Linkup"
91
+ integration.name = "Linkup (osp)"
92
92
  })
93
93
  editor.method.update({
94
94
  integrationID: ID,
@@ -143,7 +143,7 @@ export const effect = (ctx: Plugin.Context) =>
143
143
  // Make "Metaso" show up in /connect with an API-key method.
144
144
  yield* ctx.integration.transform((editor) => {
145
145
  editor.update(ID, (integration) => {
146
- integration.name = "Metaso Search"
146
+ integration.name = "Metaso Search (osp)"
147
147
  })
148
148
  editor.method.update({
149
149
  integrationID: ID,
@@ -153,7 +153,7 @@ export const effect = (ctx: Plugin.Context) =>
153
153
  // Make "Querit" show up in /connect with an API-key method.
154
154
  yield* ctx.integration.transform((editor) => {
155
155
  editor.update(ID, (integration) => {
156
- integration.name = "Querit"
156
+ integration.name = "Querit (osp)"
157
157
  })
158
158
  editor.method.update({
159
159
  integrationID: ID,
@@ -147,7 +147,7 @@ export const effect = (ctx: Plugin.Context) =>
147
147
  // Make "SerpApi" show up in /connect with an API-key method.
148
148
  yield* ctx.integration.transform((editor) => {
149
149
  editor.update(ID, (integration) => {
150
- integration.name = "SerpApi"
150
+ integration.name = "SerpApi (osp)"
151
151
  })
152
152
  editor.method.update({
153
153
  integrationID: ID,
@@ -146,7 +146,7 @@ export const effect = (ctx: Plugin.Context) =>
146
146
  // Make "Serper" show up in /connect with an API-key method.
147
147
  yield* ctx.integration.transform((editor) => {
148
148
  editor.update(ID, (integration) => {
149
- integration.name = "Serper"
149
+ integration.name = "Serper (osp)"
150
150
  })
151
151
  editor.method.update({
152
152
  integrationID: ID,
@@ -177,7 +177,7 @@ export const effect = (ctx: Plugin.Context) =>
177
177
  // Make "You.com" show up in /connect with an API-key method.
178
178
  yield* ctx.integration.transform((editor) => {
179
179
  editor.update(ID, (integration) => {
180
- integration.name = "You.com"
180
+ integration.name = "You.com (osp)"
181
181
  })
182
182
  editor.method.update({
183
183
  integrationID: ID,