opencode-search-plugin 2.2.0 → 3.0.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/README.md CHANGED
@@ -2,24 +2,41 @@
2
2
 
3
3
  **English** · [简体中文](README.zh-CN.md)
4
4
 
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.
5
+ [![npm version](https://img.shields.io/npm/v/opencode-search-plugin.svg)](https://www.npmjs.com/package/opencode-search-plugin)
6
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-search-plugin.svg)](https://www.npmjs.com/package/opencode-search-plugin)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
8
 
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/)
9
+ **Web search for OpenCode that works out of the box, no API key needed.**
10
+
11
+ One command adds thirteen search providers to OpenCode v2, growing the pool from five to eighteen. AnySearch and Keenable work with no setup at all; the other eleven only activate once you add a key.
12
+
13
+ ```sh
14
+ opencode plugin add opencode-search-plugin
15
+ ```
16
+
17
+ Install it, then just ask OpenCode something that needs the web.
18
+
19
+ <!-- TODO: replace this comment with a 10-second GIF once you record one:
20
+ ask a question → the websearch tool fires → results come back -->
21
+
22
+ Thirteen providers in one package, use whichever you like:
23
+
24
+ [AnySearch](https://www.anysearch.com/) · [Bocha](https://bocha.ai/) · [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/) · [Ollama](https://ollama.com/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
8
25
 
9
26
  ## Why this exists
10
27
 
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.
28
+ 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
29
 
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.
30
+ This repository adds plugins for thirteen more of them, taking the pool from five providers to eighteen. AnySearch and Keenable run without a key at all; the other eleven 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
31
 
15
32
  ## Install
16
33
 
17
- Three equivalent ways to install; pick one. **npm is recommended**: a version published to the registry is immutable, so a pinned `@2.2.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 eleven); use the third for one or two files only.
34
+ Three equivalent ways to install; pick one. **npm is recommended**: a version published to the registry is immutable, so a pinned `@3.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 thirteen); use the third for one or two files only.
18
35
 
19
36
  ### 1. From npm (recommended)
20
37
 
21
38
  ```sh
22
- # add @2.2.0 to pin
39
+ # add @3.0.0 to pin
23
40
  opencode plugin add opencode-search-plugin
24
41
  ```
25
42
 
@@ -35,7 +52,7 @@ or declare it in `opencode.json(c)`:
35
52
  ### 2. From GitHub
36
53
 
37
54
  ```sh
38
- # add #v2.2.0 to pin
55
+ # add #v3.0.0 to pin
39
56
  opencode plugin add github:guotree/opencode-search-plugin
40
57
  ```
41
58
 
@@ -67,7 +84,7 @@ For a location that is not auto-discovered, list it explicitly:
67
84
 
68
85
  ### Skip the services you have no key for
69
86
 
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:
87
+ Loading the package registers all thirteen, 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
88
 
72
89
  ```jsonc
73
90
  {
@@ -80,8 +97,8 @@ Loading the package registers all eleven, but a provider without a key never joi
80
97
  }
81
98
  ```
82
99
 
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.
100
+ - `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.
101
+ - `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
102
 
86
103
  When both are set, **`disable` wins on any overlap**. A per-provider exclusion is never overridden by a bulk allow-list:
87
104
 
@@ -96,7 +113,7 @@ When both are set, **`disable` wins on any overlap**. A per-provider exclusion i
96
113
  }
97
114
  ```
98
115
 
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.
116
+ 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
117
 
101
118
  ```text
102
119
  enable = {osp-brave, osp-jina, osp-serper, osp-keenable, osp-linkup}
@@ -104,7 +121,7 @@ disable = {osp-keenable, osp-linkup, osp-youcom}
104
121
  pool = osp-brave, osp-jina -- enable minus disable
105
122
  ```
106
123
 
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.
124
+ 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.
108
125
 
109
126
  ## Upgrade
110
127
 
@@ -117,7 +134,7 @@ opencode plugin update github:guotree/opencode-search-plugin
117
134
  opencode plugin update
118
135
  ```
119
136
 
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.2.0` on npm, `#v2.2.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.
137
+ 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 (`@3.0.0` on npm, `#v3.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.
121
138
 
122
139
  ## Uninstall
123
140
 
@@ -128,18 +145,20 @@ opencode plugin remove opencode-search-plugin
128
145
  opencode plugin remove github:guotree/opencode-search-plugin
129
146
  ```
130
147
 
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.
148
+ 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. To turn off a few providers instead of the whole plugin, use `enable`/`disable` above.
132
149
 
133
150
  ## Keys
134
151
 
135
152
  Every provider except AnySearch and Keenable needs an API key. Give it one of two ways:
136
153
 
137
- - **`/connect`** inside OpenCode — stored per provider, rotated without a restart, and survives environment changes.
138
- - **Environment variable** — read as a fallback, so it works in headless and CI contexts.
154
+ - **`/connect`** inside OpenCode: stored per provider, rotated without a restart, survives environment changes.
155
+ - **Environment variable**: read as a fallback, so it works in headless and CI contexts.
156
+
157
+ Environment variables are namespaced `OSP_<PROVIDER>_API_KEY` (say `OSP_BRAVE_API_KEY`), so the key this plugin reads never collides with the same vendor name exported for another tool. A credential stored in `/connect` outranks the environment either way.
139
158
 
140
159
  A provider with no key stays unbound: it is absent from the websearch pool, so `"provider": "random"` can never route a query to a service that would immediately fail.
141
160
 
142
- AnySearch and Keenable are the exceptions. Both have an anonymous tier, so they are always registered and only attach their credential header when a key exists. For Keenable the two tiers draw on separate budgets: a key spends the free allowance of 100,000 requests a month, while a keyless call goes to the `/public` endpoint instead and is metered by rate — 1,000 requests an hour and 10 a second per IP, on a pool shared with everyone else on your egress address. A key is what makes that shared headroom irrelevant.
161
+ AnySearch and Keenable are the exceptions. Both have an anonymous tier, so they are always registered and only attach their credential header when a key exists. For Keenable the two tiers draw on separate budgets: a key spends the free allowance of 100,000 requests a month, while a keyless call goes to the `/public` endpoint instead and is metered by rate: 1,000 requests an hour and 10 a second per IP, on a pool shared with everyone else on your egress address. A key is what makes that shared headroom irrelevant.
143
162
 
144
163
  Once keys are in place, pick how OpenCode chooses among them:
145
164
 
@@ -151,29 +170,31 @@ Once keys are in place, pick how OpenCode chooses among them:
151
170
 
152
171
  ## Providers
153
172
 
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`.
173
+ Every ID below carries an `osp-` prefix; it is 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
174
 
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.
175
+ 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.
157
176
 
158
177
  | Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
159
178
  | --- | --- | --- | --- | --- |
160
- | AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
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 |
163
- | Jina | `osp-jina` | `JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
164
- | Keenable | `osp-keenable` | `KEENABLE_API_KEY` | yes | 100,000 requests a month with a key; keyless tier is per IP |
165
- | Linkup | `osp-linkup` | `LINKUP_API_KEY` | no | `flash`/`fast`/`standard`/`deep` reasoning depth |
166
- | Metaso | `osp-metaso` | `METASO_API_KEY` | no | Billed per search, not per result |
167
- | Querit | `osp-querit` | `QUERIT_API_KEY` | no | Falls back to sentence matches when a snippet is missing |
168
- | SerpApi | `osp-serpapi` | `SERPAPI_API_KEY` | no | Google via SerpApi; also has its own free 1-hour server cache |
169
- | Serper | `osp-serper` | `SERPER_API_KEY` | no | Google via Serper; billed per query in credits |
170
- | You.com | `osp-youcom` | `YDC_API_KEY` | no | Web and news merged; extraction modes |
179
+ | AnySearch | `osp-anysearch` | `OSP_ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
180
+ | Bocha | `osp-bocha` | `OSP_BOCHA_API_KEY` | no | Bing-compatible results; billed per search, and by the token when a summary is asked for |
181
+ | Brave Search | `osp-brave` | `OSP_BRAVE_SEARCH_API_KEY` | no | Independent index, 20 results max |
182
+ | DeepSeek | `osp-deepseek` | `OSP_DEEPSEEK_API_KEY` | no | In-house index; $0.75 per 1,000 successful requests; 5 QPS cap |
183
+ | Jina | `osp-jina` | `OSP_JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
184
+ | Keenable | `osp-keenable` | `OSP_KEENABLE_API_KEY` | yes | 100,000 requests a month with a key; keyless tier is per IP |
185
+ | Linkup | `osp-linkup` | `OSP_LINKUP_API_KEY` | no | `flash`/`fast`/`standard`/`deep` reasoning depth |
186
+ | Metaso | `osp-metaso` | `OSP_METASO_API_KEY` | no | Billed per search, not per result |
187
+ | Ollama | `osp-ollama` | `OSP_OLLAMA_API_KEY` | no | Free personal tier; each hit carries the full page as markdown, so results are context-heavy, capped at 10, and carry no dates |
188
+ | Querit | `osp-querit` | `OSP_QUERIT_API_KEY` | no | Falls back to sentence matches when a snippet is missing |
189
+ | SerpApi | `osp-serpapi` | `OSP_SERPAPI_API_KEY` | no | Google via SerpApi; also has its own free 1-hour server cache |
190
+ | Serper | `osp-serper` | `OSP_SERPER_API_KEY` | no | Google via Serper; billed per query in credits |
191
+ | You.com | `osp-youcom` | `OSP_YDC_API_KEY` | no | Web and news merged; extraction modes |
171
192
 
172
193
  ## Configuration
173
194
 
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.
195
+ 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.
175
196
 
176
- ### Shared by all eleven
197
+ ### Shared by all thirteen
177
198
 
178
199
  | Variable | Default | Meaning |
179
200
  | --- | --- | --- |
@@ -189,7 +210,15 @@ Knobs are environment variables, read once when OpenCode starts — change one a
189
210
  | `ANYSEARCH_ZONE` | — | `cn` or `intl` |
190
211
  | `ANYSEARCH_LANGUAGE` | — | Preferred result language, e.g. `zh-CN` |
191
212
  | `ANYSEARCH_FORMAT` | `markdown` | `markdown` or `json` for the content field |
192
- | `ANYSEARCH_API_KEY` | — | Optional; anonymous access works without it |
213
+ | `OSP_ANYSEARCH_API_KEY` | — | Optional; anonymous access works without it |
214
+
215
+ ### Bocha
216
+
217
+ | Variable | Default | Meaning |
218
+ | --- | --- | --- |
219
+ | `BOCHA_COUNT` | `10` | Results per search, clamped to 1–50 (Bocha's maximum) |
220
+ | `BOCHA_FRESHNESS` | — | `noLimit`, `oneDay`, `oneWeek`, `oneMonth`, `oneYear`, a `YYYY-MM-DD` date, or a `YYYY-MM-DD..YYYY-MM-DD` range. Unset omits the parameter, which the docs recommend: Bocha rewrites time ranges itself, and a pinned range often finds nothing within it |
221
+ | `BOCHA_SUMMARY` | off | `true` swaps each snippet for a generated summary — more context to read, at the higher per-call price |
193
222
 
194
223
  ### Brave Search
195
224
 
@@ -211,7 +240,7 @@ Knobs are environment variables, read once when OpenCode starts — change one a
211
240
  | Variable | Default | Meaning |
212
241
  | --- | --- | --- |
213
242
  | `JINA_NUM` | `0` | Results per search, 1–100. `0` omits the parameter, which keeps Jina's faster and wider result types |
214
- | `JINA_INCLUDE_CONTENT` | off | `true` returns each hit's full page markdown — far more context and billed tokens |
243
+ | `JINA_INCLUDE_CONTENT` | off | `true` returns each hit's full page markdown, far more context and billed tokens |
215
244
  | `JINA_GL` / `JINA_HL` | — | Country / language code |
216
245
  | `JINA_LOCATION` | — | City-level origin to simulate, e.g. `Berlin` |
217
246
  | `JINA_TIMEOUT` | `25000`, or `60000` with `JINA_INCLUDE_CONTENT` | Only provider whose deadline varies by mode: fetching whole pages is legitimately slower than a snippet search |
@@ -223,7 +252,7 @@ Knobs are environment variables, read once when OpenCode starts — change one a
223
252
  | `KEENABLE_MAX_RESULTS` | `10` | Results per search, clamped to 1–50 |
224
253
  | `KEENABLE_MODE` | — | `pro` (deeper retrieval) or `realtime` (fastest); unset leaves Keenable's own default |
225
254
  | `KEENABLE_TITLE` | `opencode-search-plugin` | App name sent as `X-Keenable-Title`, which the keyless endpoint requires to attribute the call |
226
- | `KEENABLE_API_KEY` | — | Optional; keyless access works without it |
255
+ | `OSP_KEENABLE_API_KEY` | — | Optional; keyless access works without it |
227
256
 
228
257
  ### Linkup
229
258
 
@@ -239,6 +268,12 @@ Knobs are environment variables, read once when OpenCode starts — change one a
239
268
  | `METASO_SEARCH_SIZE` | `20` | Results per search, clamped to 1–100 |
240
269
  | `METASO_INCLUDE_RAW_CONTENT` | off | `true` returns every hit's full source text at the same search cost, but far more context tokens |
241
270
 
271
+ ### Ollama
272
+
273
+ | Variable | Default | Meaning |
274
+ | --- | --- | --- |
275
+ | `OLLAMA_MAX_RESULTS` | `5` | Results per search, clamped to 1–10 (the API's maximum). `content` is a whole page of markdown rather than a snippet, so even the default 5 results run to thousands of characters of context |
276
+
242
277
  ### Querit
243
278
 
244
279
  | Variable | Default | Meaning |
@@ -274,12 +309,12 @@ There is deliberately no result-count knob: Google withdrew the `num` parameter,
274
309
  ## How a query flows
275
310
 
276
311
  1. OpenCode asks the selected provider for the query.
277
- 2. The normalized query (trimmed, whitespace collapsed) is checked against an in-memory cache. A hit returns immediately — repeat queries are free and instant.
312
+ 2. The normalized query (trimmed, whitespace collapsed) is checked against an in-memory cache. A hit returns immediately; repeat queries are free and instant.
278
313
  3. On a miss the key is resolved again, so a key rotated in `/connect` applies to the very next search.
279
314
  4. The request goes out under a deadline (`<PROVIDER>_TIMEOUT`, default 25s). OpenCode applies no timeout of its own around a search, so without this a provider that accepted the connection and then went silent would hang the whole turn. Failures surface the HTTP status and response body, or name the timeout when that is what happened.
280
315
  5. The response is schema-decoded, mapped to `{ url, title, content, time }`, and cached.
281
316
 
282
- Dates are parsed from relative phrases (`3 hours ago`, `3小时前`), absolute formats (`Sep 30, 2026`, `31 Mar 2026`), and ISO 8601. Strings whose lenient parsing would invent a wrong year — `Nov 1`, which `Date.parse` reads as 2001 — are dropped rather than reported as a bogus timestamp.
317
+ Dates are parsed from relative phrases (`3 hours ago`, `3小时前`), absolute formats (`Sep 30, 2026`, `31 Mar 2026`), and ISO 8601. Strings whose lenient parsing would invent a wrong year (`Nov 1`, which `Date.parse` reads as 2001) are dropped rather than reported as a bogus timestamp.
283
318
 
284
319
  ## Troubleshooting
285
320
 
@@ -295,12 +330,15 @@ opencode api get "/api/websearch/provider"
295
330
 
296
331
  Also worth checking:
297
332
 
298
- - **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.
299
- - **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.
300
- - **A search fails with `Connect X in /connect, or set X_API_KEY`** — the key disappeared between binding and searching. Reconnect or re-export.
301
- - **A search fails with `X search timed out after 25000ms`** — the provider accepted the request and never answered within the deadline. Retrying usually works; if a provider is consistently slow for you, raise its `<PROVIDER>_TIMEOUT`. This is a deliberate bound: without one a stalled provider hangs the whole turn, because OpenCode applies no timeout of its own around a search.
302
- - **Everything is slow or burning credits** — leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
303
- - **Nothing happens after editing a plugin file** — OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
333
+ - **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.
334
+ - **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 `OSP_KEENABLE_API_KEY` to draw on your own allowance instead.
335
+ - **A search fails with `Ollama API error 401`**: `OSP_OLLAMA_API_KEY` must be an ollama.com account key from [ollama.com/settings/keys](https://ollama.com/settings/keys). If it instead holds a bearer token for an Ollama-compatible proxy (a documented OpenCode config shape), store the real key in `/connect` for **Ollama Search (osp)** — a /connect credential outranks the environment.
336
+ - **A search fails with `Bocha balance is exhausted (HTTP 403)`**: the account balance is spent; top up at [open.bocha.cn](https://open.bocha.cn). The message carries Bocha's own `message` text and a `log_id` to quote to their support.
337
+ - **A search fails with `Bocha rate limited the request (HTTP 429)`**: the per-account request cap scales with the total recharge amount, so a small balance means a small ceiling. The report includes a `retry after Ns` hint when Bocha sent one.
338
+ - **A search fails with `Connect X in /connect, or set OSP_X_API_KEY`**: the key disappeared between binding and searching. Reconnect or re-export.
339
+ - **A search fails with `X search timed out after 25000ms`**: the provider accepted the request and never answered within the deadline. Retrying usually works; if a provider is consistently slow for you, raise its `<PROVIDER>_TIMEOUT`. This is a deliberate bound: without one a stalled provider hangs the whole turn, because OpenCode applies no timeout of its own around a search.
340
+ - **Everything is slow or burning credits**: leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
341
+ - **Nothing happens after editing a plugin file**: OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
304
342
 
305
343
  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.
306
344
 
@@ -311,10 +349,10 @@ npm install
311
349
  npm run check # typecheck + tests
312
350
  ```
313
351
 
314
- - `npm run typecheck` — strict TypeScript across every plugin and test.
315
- - `npm test` — the shared provider contract, per-provider behaviour, date parsing, aggregate registration, and repository-structure guards.
352
+ - `npm run typecheck`: strict TypeScript across every plugin and test.
353
+ - `npm test`: the shared provider contract, per-provider behaviour, date parsing, aggregate registration, and repository-structure guards.
316
354
 
317
- `@opencode/plugin` and `effect` are declared as **devDependencies**, not `dependencies`. OpenCode provides both to plugins at runtime — the same mechanism that lets a single `.ts` file run from `~/.config/opencode/plugins/` with no `node_modules` at all. Declaring them as runtime dependencies would make `opencode plugin add` install a 127 MB tree (`effect` alone is 47 MB, and `@opencode/plugin` pulls in `@aws-sdk`, `google-auth-library` and `@opentelemetry`) to reach 100 KB of plugin source; as devDependencies they are present for typechecking and tests, and absent from a consumer's install.
355
+ `@opencode/plugin` and `effect` are declared as **devDependencies**, not `dependencies`. OpenCode provides both to plugins at runtime; that is also what lets a single `.ts` file run from `~/.config/opencode/plugins/` with no `node_modules` at all. Declaring them as runtime dependencies would make `opencode plugin add` install a 127 MB tree (`effect` alone is 47 MB, and `@opencode/plugin` pulls in `@aws-sdk`, `google-auth-library` and `@opentelemetry`) to reach 100 KB of plugin source; as devDependencies they are present for typechecking and tests, and absent from a consumer's install.
318
356
 
319
357
  The structure tests exist because each file must remain independently copyable: they fail if a plugin starts importing a sibling module, if the shared blocks stop being byte-identical copies, or if a provider's IDs drift apart. See [CONTRIBUTING.md](CONTRIBUTING.md).
320
358
 
package/README.zh-CN.md CHANGED
@@ -2,24 +2,41 @@
2
2
 
3
3
  # opencode-search-plugin
4
4
 
5
- 十一个相互独立的 [OpenCode](https://opencode.ai) v2 插件,各自注册一个网页搜索 provider。每个文件都能单独使用:复制一个进你的插件目录,或者安装这个包一次拿全十一个。
5
+ [![npm version](https://img.shields.io/npm/v/opencode-search-plugin.svg)](https://www.npmjs.com/package/opencode-search-plugin)
6
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-search-plugin.svg)](https://www.npmjs.com/package/opencode-search-plugin)
7
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
8
 
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/)
9
+ **不用 API key,装上就能搜。**
10
+
11
+ 一条命令,给 OpenCode v2 加上十三个搜索 provider,搜索池从五家扩到十八家。AnySearch 和 Keenable 不用 key 直接能用;其余十一家等你配好 key 才会启用。
12
+
13
+ ```sh
14
+ opencode plugin add opencode-search-plugin
15
+ ```
16
+
17
+ 装完就能问 OpenCode 需要联网的问题。
18
+
19
+ <!-- TODO: 录一段 10 秒 GIF 后替换本注释:
20
+ 提问 → websearch 工具触发 → 结果返回 -->
21
+
22
+ 十三个 provider 装一次全有,想用哪家用哪家:
23
+
24
+ [AnySearch](https://www.anysearch.com/) · [Bocha](https://bocha.ai/) · [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/) · [Ollama](https://ollama.com/) · [Querit](https://querit.ai/) · [SerpApi](https://serpapi.com/) · [Serper](https://serper.dev/) · [You.com](https://you.com/)
8
25
 
9
26
  ## 为什么做这个插件
10
27
 
11
28
  OpenCode v2 内置了五个网页搜索 provider:`exa`、`firecrawl`、`parallel`、`tavily`、`tinyfish`。在这个范围之外,还有大量搜索 API 提供免费额度或免费层级,足以支撑 agent 的日常检索而不必为此付费。
12
29
 
13
- 本仓库为其中十一家补上了插件,把可用池从五个 provider 扩到十六个。AnySearch 和 Keenable 无需任何 key 即可使用;其余九家只在你提供 key 之后才会绑定,因此装上这个包也不会把查询路由到一个注定失败的服务上。至于哪几家值得用,取决于它们各自当前的免费额度,长期依赖某家之前建议先确认它的定价。
30
+ 本仓库为其中十三家补上了插件,把可用池从五个 provider 扩到十八个。AnySearch 和 Keenable 无需任何 key 即可使用;其余十一家只在你提供 key 之后才会绑定,因此装上这个包也不会把查询路由到一个注定失败的服务上。至于哪几家值得用,取决于它们各自当前的免费额度,长期依赖某家之前建议先确认它的定价。
14
31
 
15
32
  ## 安装
16
33
 
17
- 三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@2.2.0` 后每次安装都拿到同一份字节;GitHub 的 tag 在理论上可以被维护者移动、指向别的 commit。前两种装的是同一个完整包(十一个全有),只想用一两个就用第三种。
34
+ 三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@3.0.0` 后每次安装都拿到同一份字节;GitHub 的 tag 在理论上可以被维护者移动、指向别的 commit。前两种装的是同一个完整包(十三个全有),只想用一两个就用第三种。
18
35
 
19
36
  ### 1. 从 npm 安装(推荐)
20
37
 
21
38
  ```sh
22
- # 追加 @2.2.0 可固定到该版本
39
+ # 追加 @3.0.0 可固定到该版本
23
40
  opencode plugin add opencode-search-plugin
24
41
  ```
25
42
 
@@ -35,7 +52,7 @@ opencode plugin add opencode-search-plugin
35
52
  ### 2. 从 GitHub 安装
36
53
 
37
54
  ```sh
38
- # 追加 #v2.2.0 可固定到该 tag
55
+ # 追加 #v3.0.0 可固定到该 tag
39
56
  opencode plugin add github:guotree/opencode-search-plugin
40
57
  ```
41
58
 
@@ -67,7 +84,7 @@ opencode plugin add github:guotree/opencode-search-plugin
67
84
 
68
85
  ### 跳过你没有 key 的服务
69
86
 
70
- 装包会注册全部十一家,但没有 key 的 provider 永远不会进入搜索池——见下文[密钥](#密钥)。想让它们连注册表都不出现,可以单独或同时使用下面两个可选选项:
87
+ 装包会注册全部十三家,但没有 key 的 provider 永远不会进入搜索池(见下文[密钥](#密钥))。想让它们连注册表都不出现,可以单独或同时使用下面两个可选选项:
71
88
 
72
89
  ```jsonc
73
90
  {
@@ -81,7 +98,7 @@ opencode plugin add github:guotree/opencode-search-plugin
81
98
  ```
82
99
 
83
100
  - `disable` 是黑名单:它列出的 provider ID(见下文[支持的服务](#支持的服务)的表格)永远不会注册。AnySearch 和 Keenable 不需要 key,想把这两家也排除在搜索池之外,只能用 `disable`。
84
- - `enable` 是白名单:只注册列出的 ID,其余一律跳过——`"enable": ["osp-brave", "osp-jina"]` 表示只跑这两家。
101
+ - `enable` 是白名单:只注册列出的 ID,其余一律跳过;`"enable": ["osp-brave", "osp-jina"]` 只跑这两家。
85
102
 
86
103
  两者同时配置时,**交集由 `disable` 胜出**:逐家的排除绝不会被批量白名单覆盖。
87
104
 
@@ -96,7 +113,7 @@ opencode plugin add github:guotree/opencode-search-plugin
96
113
  }
97
114
  ```
98
115
 
99
- 用集合运算来读:实际注册的池 = `enable − disable`。交集被剔除;`disable` 里不在 `enable` 中的 ID 从一开始就不在候选内——属于无害的冗余,不会被报告。唯一会触发警告的冗余是交集,日志会点出其中的 ID。
116
+ 用集合运算来读:实际注册的池 = `enable − disable`。交集被剔除;`disable` 里不在 `enable` 中的 ID 从一开始就不在候选内,属于无害的冗余,不报警告。唯一会触发警告的冗余是交集,日志会点出其中的 ID。
100
117
 
101
118
  ```text
102
119
  enable = {osp-brave, osp-jina, osp-serper, osp-keenable, osp-linkup}
@@ -104,7 +121,7 @@ disable = {osp-keenable, osp-linkup, osp-youcom}
104
121
  pool = osp-brave, osp-jina -- enable 减 disable
105
122
  ```
106
123
 
107
- 无法生效的配置会在日志里提示,而不是被悄悄丢弃:写错的 ID(本包没注册的名字)会被点名,选项不是数组也会被点名。`enable` 如果最终一个已知 provider 都没匹配上,搜索池会是空的——日志里会写明这一点。
124
+ 无法生效的配置会在日志里提示,而不是被悄悄丢弃:写错的 ID(本包没注册的名字)会被点名,选项不是数组也会被点名。`enable` 如果最终一个已知 provider 都没匹配上,搜索池会是空的,日志里会写明这一点。
108
125
 
109
126
  ## 升级
110
127
 
@@ -117,7 +134,7 @@ opencode plugin update github:guotree/opencode-search-plugin
117
134
  opencode plugin update
118
135
  ```
119
136
 
120
- 升级目标用的是配置时的说明符:npm 安装用包名,GitHub 安装只认自己的 `github:` 形式——用裸包名会报 `Plugin is not configured`。先跑 `opencode plugin check` 查看有哪些过期。未固定版本的安装会跟上新版本;固定过的——npm 形式的 `@2.2.0`、GitHub 形式的 `#v2.2.0`——会停在原地,直到你用新 pin 重新执行 `plugin add`,或直接修改 `opencode.json(c)` 里的说明符。本地复制的文件则用新版本覆盖旧文件来升级。
137
+ 升级目标用的是配置时的说明符:npm 安装用包名,GitHub 安装只认自己的 `github:` 形式,用裸包名会报 `Plugin is not configured`。先跑 `opencode plugin check` 查看有哪些过期。未固定版本的安装会跟上新版本;固定过的会停在原地(npm 形式是 `@3.0.0`,GitHub 形式是 `#v3.0.0`),直到你用新 pin 重新执行 `plugin add`,或直接修改 `opencode.json(c)` 里的说明符。本地复制的文件用新版本覆盖旧文件即可。
121
138
 
122
139
  ## 卸载
123
140
 
@@ -128,18 +145,20 @@ opencode plugin remove opencode-search-plugin
128
145
  opencode plugin remove github:guotree/opencode-search-plugin
129
146
  ```
130
147
 
131
- 同上:传入安装时使用的说明符。该命令修改的是全局配置;若插件是写在 `opencode.json(c)` 的 `plugins` 条目里或本地复制的文件,删掉对应条目、或删掉复制的 `.ts` 文件即可。`/connect` 里保存的密钥不受影响——重新装回就能继续用——只想关掉其中几家而不是整个插件,用上文的 `enable`/`disable`。
148
+ 同上:传入安装时使用的说明符。该命令修改的是全局配置;若插件是写在 `opencode.json(c)` 的 `plugins` 条目里或本地复制的文件,删掉对应条目、或删掉复制的 `.ts` 文件即可。`/connect` 里保存的密钥不受影响,重新装回就能继续用。只想关掉其中几家而不是整个插件,用上文的 `enable`/`disable`。
132
149
 
133
150
  ## 密钥
134
151
 
135
152
  除 AnySearch 和 Keenable 外,每家服务都需要 API key。两种给法二选一:
136
153
 
137
- - **OpenCode 内的 `/connect`** —— 按 provider 存储,轮换后无需重启,且不受环境变量变动影响。
138
- - **环境变量** —— 作为兜底读取,因此在无界面和 CI 场景下也能工作。
154
+ - **OpenCode 内的 `/connect`**:按 provider 存储,轮换后无需重启,也不受环境变量变动影响。
155
+ - **环境变量**:作为兜底读取,无界面和 CI 场景下也能工作。
156
+
157
+ 环境变量带命名空间前缀,形如 `OSP_<PROVIDER>_API_KEY`(例如 `OSP_BRAVE_API_KEY`),因此本插件读的 key 不会和其他工具导出的同厂商变量名撞车。存在 `/connect` 里的凭据无论如何都优先于环境变量。
139
158
 
140
159
  没有 key 的 provider 会保持未绑定状态:它不在 websearch 池中,所以 `"provider": "random"` 绝不会把查询路由到一个注定失败的服务上。
141
160
 
142
- AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册,只有存在 key 时才附带各自的凭据头。Keenable 的两层额度互不通用:有 key 时消耗每月 100,000 次的免费额度,无 key 的请求则改走 `/public` 端点,按速率限制——每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的所有人共用。想摆脱这个共享池的不确定性,就得配一把 key。
161
+ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册,只有存在 key 时才附带各自的凭据头。Keenable 的两层额度互不通用:有 key 时消耗每月 100,000 次的免费额度,无 key 的请求则改走 `/public` 端点,按速率限制:每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的所有人共用。想摆脱这个共享池的不确定性,就得配一把 key。
143
162
 
144
163
  密钥就位后,再决定 OpenCode 如何在它们之间做选择:
145
164
 
@@ -151,29 +170,31 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
151
170
 
152
171
  ## 支持的服务
153
172
 
154
- 下表每个 ID 都带 `osp-` 前缀——这是本包的命名空间。OpenCode 内置 provider 用的是裸厂商名(`exa`、`tavily`),厂商自己的插件也会注册裸名(Keenable 就已经有一个),所以前缀是避免撞名的唯一保障:既避开搜索池里的重复,也避开 `/connect` 里按 integration ID 存凭据的那张表。在 `websearch.provider` 和 `enable`/`disable` 里请原样使用带前缀的 ID。
173
+ 下表每个 ID 都带 `osp-` 前缀,这是本包的命名空间。OpenCode 内置 provider 用的是裸厂商名(`exa`、`tavily`),厂商自己的插件也会注册裸名(Keenable 就已经有一个),所以前缀是避免撞名的唯一保障:既避开搜索池里的重复,也避开 `/connect` 里按 integration ID 存凭据的那张表。在 `websearch.provider` 和 `enable`/`disable` 里请原样使用带前缀的 ID。
155
174
 
156
- 命名空间同样体现在显示名上:`/connect` 里本包的每个条目都带 `(osp)` 标记——内置的 `DeepSeek` 旁边是 `DeepSeek (osp)`——存在这里的 key 不会和内置 provider 的混淆。搜索池里仍用纯品牌名,因为池中只有搜索 provider,不存在撞名问题。
175
+ 命名空间同样体现在显示名上:`/connect` 里本包的每个条目都带 `(osp)` 标记(内置的 `DeepSeek` 旁边是 `DeepSeek (osp)`),存在这里的 key 不会和内置 provider 的混淆。搜索池里仍用纯品牌名,因为池中只有搜索 provider,不存在撞名问题。
157
176
 
158
177
  | 服务 | ID | 密钥(环境变量 / `/connect`) | 匿名可用 | 说明 |
159
178
  | --- | --- | --- | --- | --- |
160
- | AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | 是 | 按 IP 的免费额度;意图路由,`cn` / `intl` 分区 |
161
- | Brave Search | `osp-brave` | `BRAVE_SEARCH_API_KEY` | 否 | 独立索引,最多 20 条结果 |
162
- | DeepSeek | `osp-deepseek` | `DEEPSEEK_API_KEY` | 否 | 自研搜索引擎;每千次成功请求 5 元;限速 5 QPS |
163
- | Jina | `osp-jina` | `JINA_API_KEY` | 否 | 页面正文按 token 计费,因此默认不取 |
164
- | Keenable | `osp-keenable` | `KEENABLE_API_KEY` | 是 | 有 key 每月 100,000 次免费请求;无 key 层级按 IP 限速 |
165
- | Linkup | `osp-linkup` | `LINKUP_API_KEY` | 否 | `flash` / `fast` / `standard` / `deep` 四档推理深度 |
166
- | Metaso | `osp-metaso` | `METASO_API_KEY` | 否 | 按次搜索计费,不按结果条数 |
167
- | Querit | `osp-querit` | `QUERIT_API_KEY` | 否 | 摘要缺失时回退到句子级匹配 |
168
- | SerpApi | `osp-serpapi` | `SERPAPI_API_KEY` | 否 | 经 SerpApi 调用 Google;另有其自带的免费 1 小时服务端缓存 |
169
- | Serper | `osp-serper` | `SERPER_API_KEY` | 否 | 经 Serper 调用 Google;按查询消耗积分计费 |
170
- | You.com | `osp-youcom` | `YDC_API_KEY` | 否 | 网页与新闻结果合并;多种正文抽取模式 |
179
+ | AnySearch | `osp-anysearch` | `OSP_ANYSEARCH_API_KEY` | 是 | 按 IP 的免费额度;意图路由,`cn` / `intl` 分区 |
180
+ | Bocha | `osp-bocha` | `OSP_BOCHA_API_KEY` | 否 | 结果格式兼容 Bing;按搜索次数计费,要求摘要时另按 token 计费 |
181
+ | Brave Search | `osp-brave` | `OSP_BRAVE_SEARCH_API_KEY` | 否 | 独立索引,最多 20 条结果 |
182
+ | DeepSeek | `osp-deepseek` | `OSP_DEEPSEEK_API_KEY` | 否 | 自研搜索引擎;每千次成功请求 5 元;限速 5 QPS |
183
+ | Jina | `osp-jina` | `OSP_JINA_API_KEY` | 否 | 页面正文按 token 计费,因此默认不取 |
184
+ | Keenable | `osp-keenable` | `OSP_KEENABLE_API_KEY` | 是 | 有 key 每月 100,000 次免费请求;无 key 层级按 IP 限速 |
185
+ | Linkup | `osp-linkup` | `OSP_LINKUP_API_KEY` | 否 | `flash` / `fast` / `standard` / `deep` 四档推理深度 |
186
+ | Metaso | `osp-metaso` | `OSP_METASO_API_KEY` | 否 | 按次搜索计费,不按结果条数 |
187
+ | Ollama | `osp-ollama` | `OSP_OLLAMA_API_KEY` | 否 | 个人档免费;每条 content 是整页 markdown,上下文偏重;上限 10 条,且不带日期 |
188
+ | Querit | `osp-querit` | `OSP_QUERIT_API_KEY` | 否 | 摘要缺失时回退到句子级匹配 |
189
+ | SerpApi | `osp-serpapi` | `OSP_SERPAPI_API_KEY` | 否 | 经 SerpApi 调用 Google;另有其自带的免费 1 小时服务端缓存 |
190
+ | Serper | `osp-serper` | `OSP_SERPER_API_KEY` | 否 | 经 Serper 调用 Google;按查询消耗积分计费 |
191
+ | You.com | `osp-youcom` | `OSP_YDC_API_KEY` | 否 | 网页与新闻结果合并;多种正文抽取模式 |
171
192
 
172
193
  ## 配置
173
194
 
174
- 所有配置项都是环境变量,OpenCode 启动时读取一次——改了要重启。默认值取自各家 API 文档,所以一开始什么都不设就是合理的起点。
195
+ 所有配置项都是环境变量,OpenCode 启动时读取一次,改了要重启。默认值取自各家 API 文档,所以一开始什么都不设就是合理的起点。
175
196
 
176
- ### 十一家通用
197
+ ### 十三家通用
177
198
 
178
199
  | 变量 | 默认值 | 含义 |
179
200
  | --- | --- | --- |
@@ -189,7 +210,15 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
189
210
  | `ANYSEARCH_ZONE` | — | `cn` 或 `intl` |
190
211
  | `ANYSEARCH_LANGUAGE` | — | 首选结果语言,例如 `zh-CN` |
191
212
  | `ANYSEARCH_FORMAT` | `markdown` | content 字段用 `markdown` 还是 `json` |
192
- | `ANYSEARCH_API_KEY` | — | 可选;不设也能匿名使用 |
213
+ | `OSP_ANYSEARCH_API_KEY` | — | 可选;不设也能匿名使用 |
214
+
215
+ ### Bocha
216
+
217
+ | 变量 | 默认值 | 含义 |
218
+ | --- | --- | --- |
219
+ | `BOCHA_COUNT` | `10` | 每次搜索的结果数,限制在 1–50(博查的上限) |
220
+ | `BOCHA_FRESHNESS` | — | `noLimit`、`oneDay`、`oneWeek`、`oneMonth`、`oneYear`、`YYYY-MM-DD` 单日,或 `YYYY-MM-DD..YYYY-MM-DD` 区间。不设即不传该参数,这也是文档的建议:博查会自行改写时间范围,而指定区间往往在该区间内找不到结果 |
221
+ | `BOCHA_SUMMARY` | 关 | `true` 用生成的摘要替换每条 snippet——上下文更多,单次调用的价格也更高 |
193
222
 
194
223
  ### Brave Search
195
224
 
@@ -211,7 +240,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
211
240
  | 变量 | 默认值 | 含义 |
212
241
  | --- | --- | --- |
213
242
  | `JINA_NUM` | `0` | 每次搜索的结果数,1–100。`0` 表示不传该参数,从而保留 Jina 更快、类型更全的结果 |
214
- | `JINA_INCLUDE_CONTENT` | 关 | `true` 返回每条命中的整页 markdown——上下文和计费 token 都会大幅增加 |
243
+ | `JINA_INCLUDE_CONTENT` | 关 | `true` 返回每条命中的整页 markdown,上下文和计费 token 都会大幅增加 |
215
244
  | `JINA_GL` / `JINA_HL` | — | 国家 / 语言代码 |
216
245
  | `JINA_LOCATION` | — | 模拟的城市级来源,例如 `Berlin` |
217
246
  | `JINA_TIMEOUT` | `25000`;开启 `JINA_INCLUDE_CONTENT` 时为 `60000` | 唯一一个默认值随模式变化的 provider:抓取整页本来就比取摘要慢得多 |
@@ -223,7 +252,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
223
252
  | `KEENABLE_MAX_RESULTS` | `10` | 每次搜索的结果数,限制在 1–50 |
224
253
  | `KEENABLE_MODE` | — | `pro`(更深的检索)或 `realtime`(最快);不设则沿用 Keenable 自己的默认值 |
225
254
  | `KEENABLE_TITLE` | `opencode-search-plugin` | 作为 `X-Keenable-Title` 发出的应用名,无 key 端点靠它归属调用方 |
226
- | `KEENABLE_API_KEY` | — | 可选;不设也能匿名使用 |
255
+ | `OSP_KEENABLE_API_KEY` | — | 可选;不设也能匿名使用 |
227
256
 
228
257
  ### Linkup
229
258
 
@@ -239,6 +268,12 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
239
268
  | `METASO_SEARCH_SIZE` | `20` | 每次搜索的结果数,限制在 1–100 |
240
269
  | `METASO_INCLUDE_RAW_CONTENT` | 关 | `true` 返回每条命中的完整原文,搜索成本不变,但上下文 token 大幅增加 |
241
270
 
271
+ ### Ollama
272
+
273
+ | 变量 | 默认值 | 含义 |
274
+ | --- | --- | --- |
275
+ | `OLLAMA_MAX_RESULTS` | `5` | 每次搜索的结果数,限制在 1–10(API 的上限)。`content` 是整页 markdown 而非 snippet,即使默认 5 条也是数千字符的上下文 |
276
+
242
277
  ### Querit
243
278
 
244
279
  | 变量 | 默认值 | 含义 |
@@ -274,12 +309,12 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
274
309
  ## 一次查询的完整流程
275
310
 
276
311
  1. OpenCode 向选定的 provider 发起查询。
277
- 2. 归一化后的查询(去首尾、压缩空白)先查内存缓存;命中则直接返回——重复查询免费且即时。
312
+ 2. 归一化后的查询(去首尾、压缩空白)先查内存缓存;命中则直接返回,重复查询免费且即时。
278
313
  3. 未命中时重新解析密钥,因此在 `/connect` 里轮换的 key 立即对下一次搜索生效。
279
314
  4. 发出请求,且带期限(`<PROVIDER>_TIMEOUT`,默认 25 秒)。OpenCode 自身并不给搜索加任何超时,所以没有这道期限的话,一个接受了连接却不再应答的 provider 会挂住整轮对话。失败时会带上 HTTP 状态码和响应体;如果是超时,报错会直接点明是超时。
280
315
  5. 响应经 schema 解码,映射为 `{ url, title, content, time }`,然后写入缓存。
281
316
 
282
- 日期解析支持相对表述(`3 hours ago`、`3小时前`)、绝对格式(`Sep 30, 2026`、`31 Mar 2026`)和 ISO 8601。凡是宽松解析会凭空造出错误年份的字符串——比如 `Nov 1`,`Date.parse` 会读成 2001 年——一律丢弃,而不是返回一个错误的时间戳。
317
+ 日期解析支持相对表述(`3 hours ago`、`3小时前`)、绝对格式(`Sep 30, 2026`、`31 Mar 2026`)和 ISO 8601。凡是宽松解析会凭空造出错误年份的字符串(比如 `Nov 1`,`Date.parse` 会读成 2001 年)一律丢弃,而不是返回一个错误的时间戳。
283
318
 
284
319
  ## 排错
285
320
 
@@ -295,12 +330,15 @@ opencode api get "/api/websearch/provider"
295
330
 
296
331
  另外几个值得检查的地方:
297
332
 
298
- - **某家 provider 不在池里** —— 它没有 key,或者被 `disable`(或收窄后的 `enable`)过滤掉了。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。本该在却不在的名字多半是拼写错误:日志会点出无法识别的 ID 和 `enable`/`disable` 的交集。
299
- - **搜索报错 `Keenable rate limited the request (HTTP 429)`** —— 无 key 层级每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的其他人共用。改用 `KEENABLE_API_KEY` 就会记到你自己的额度上。
300
- - **搜索报错 `Connect X in /connect, or set X_API_KEY`** —— key 在绑定和搜索之间消失了。重新连接或重新导出。
301
- - **搜索报错 `X search timed out after 25000ms`** —— provider 接受了请求,但在期限内没有返回。重试通常就好;如果某家 provider 在你这边一贯偏慢,调大它的 `<PROVIDER>_TIMEOUT`。这道期限是刻意加的:没有它,一个卡住的 provider 会挂住整轮对话,因为 OpenCode 自身并不给搜索设置任何超时。
302
- - **一切都很慢、或者积分烧得飞快** —— 把 `*_CACHE_TTL` 保持在默认值。时间窗内的重复查询由内存直接返回。
303
- - **改了插件文件却没反应** —— OpenCode 会监视插件并自动重载;如果没有,执行 `opencode service restart`。
333
+ - 某家 provider 不在池里:它没有 key,或者被 `disable`(或收窄后的 `enable`)过滤掉了。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。本该在却不在的名字多半是拼写错误:日志会点出无法识别的 ID 和 `enable`/`disable` 的交集。
334
+ - 搜索报错 `Keenable rate limited the request (HTTP 429)`:无 key 层级每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的其他人共用。改用 `OSP_KEENABLE_API_KEY` 就会记到你自己的额度上。
335
+ - 搜索报错 `Ollama API error 401`:`OSP_OLLAMA_API_KEY` 必须是 [ollama.com/settings/keys](https://ollama.com/settings/keys) 的账号 key。如果它存的是某个 Ollama 兼容代理的 token(OpenCode 官方文档里就有这种配置形状),到 `/connect` 给 **Ollama Search (osp)** 存一把真 key——/connect 凭据优先于环境变量。
336
+ - 搜索报错 `Bocha balance is exhausted (HTTP 403)`:账户余额用尽;到 [open.bocha.cn](https://open.bocha.cn) 充值。报错会带上博查自己的 `message` 文本和一个 `log_id`,联系他们客服时请附上。
337
+ - 搜索报错 `Bocha rate limited the request (HTTP 429)`:单账户的请求上限与总充值金额挂钩,余额少则天花板低。博查返回了 `Retry-After` 时,报错里会带上 `retry after Ns` 提示。
338
+ - 搜索报错 `Connect X in /connect, or set OSP_X_API_KEY`:key 在绑定和搜索之间消失了。重新连接或重新导出。
339
+ - 搜索报错 `X search timed out after 25000ms`:provider 接受了请求,但在期限内没有返回。重试通常就好;如果某家 provider 在你这边一贯偏慢,调大它的 `<PROVIDER>_TIMEOUT`。这道期限是刻意加的:没有它,一个卡住的 provider 会挂住整轮对话,因为 OpenCode 自身并不给搜索设置任何超时。
340
+ - 一切都很慢,或者积分烧得飞快:把 `*_CACHE_TTL` 保持在默认值。时间窗内的重复查询由内存直接返回。
341
+ - 改了插件文件却没反应:OpenCode 会监视插件并自动重载;如果没有,执行 `opencode service restart`。
304
342
 
305
343
  日志在 `~/.local/share/opencode/log/opencode.log`;过滤 `loading plugin` 可以看到 OpenCode 解析到的每个插件及其入口文件。本插件关于 `enable`/`disable` 的警告也落在同一个文件里,带 `[opencode-search-plugin]` 前缀。
306
344
 
@@ -311,10 +349,10 @@ npm install
311
349
  npm run check # 类型检查 + 测试
312
350
  ```
313
351
 
314
- - `npm run typecheck` —— 对所有插件和测试做严格 TypeScript 检查。
315
- - `npm test` —— 覆盖通用 provider 契约、各家行为、日期解析、聚合注册,以及仓库结构守卫。
352
+ - `npm run typecheck`:对所有插件和测试做严格 TypeScript 检查。
353
+ - `npm test`:覆盖通用 provider 契约、各家行为、日期解析、聚合注册,以及仓库结构守卫。
316
354
 
317
- `@opencode/plugin` 和 `effect` 声明为 **devDependencies**,而不是 `dependencies`。这两个模块由 OpenCode 在运行时提供——这正是单个 `.ts` 文件可以放进 `~/.config/opencode/plugins/` 且完全不需要 `node_modules` 的原因。若写成运行时依赖,`opencode plugin add` 会为了 100 KB 的插件源码安装 127 MB 的依赖树(仅 `effect` 就 47 MB,`@opencode/plugin` 还会拖进 `@aws-sdk`、`google-auth-library` 和 `@opentelemetry`);放进 devDependencies 后,类型检查和测试照常可用,而使用者安装时拿不到这份依赖树。
355
+ `@opencode/plugin` 和 `effect` 声明为 **devDependencies**,而不是 `dependencies`。这两个模块由 OpenCode 在运行时提供,这也是单个 `.ts` 文件可以放进 `~/.config/opencode/plugins/` 且完全不需要 `node_modules` 的原因。若写成运行时依赖,`opencode plugin add` 会为了 100 KB 的插件源码安装 127 MB 的依赖树(仅 `effect` 就 47 MB,`@opencode/plugin` 还会拖进 `@aws-sdk`、`google-auth-library` 和 `@opentelemetry`);放进 devDependencies 后,类型检查和测试照常可用,而使用者安装时拿不到这份依赖树。
318
356
 
319
357
  存在结构测试,是因为每个文件必须保持可独立复制:一旦某个插件开始 import 同级模块、共享代码块不再是逐字相同的副本、或者某家的 ID 出现偏移,测试就会失败。详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
320
358