opencode-search-plugin 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +233 -2
- package/README.zh-CN.md +234 -0
- package/index.ts +59 -0
- package/package.json +57 -4
- package/plugins/anysearch-search.ts +227 -0
- package/plugins/brave-search.ts +270 -0
- package/plugins/jina-search.ts +242 -0
- package/plugins/linkup-search.ts +213 -0
- package/plugins/metaso-search.ts +268 -0
- package/plugins/querit-search.ts +290 -0
- package/plugins/serpapi-search.ts +269 -0
- package/plugins/serper-search.ts +272 -0
- package/plugins/youcom-search.ts +313 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 guotree
|
|
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
CHANGED
|
@@ -1,3 +1,234 @@
|
|
|
1
|
-
#
|
|
1
|
+
# opencode-search-plugin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**English** · [简体中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
Nine 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 nine.
|
|
6
|
+
|
|
7
|
+
[AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [Jina](https://jina.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
|
+
|
|
9
|
+
## Why this exists
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
This repository adds plugins for nine more of them, taking the pool from five providers to fourteen. AnySearch runs 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.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
### All nine, as a package
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# from GitHub (add #v1.0.1 to pin)
|
|
21
|
+
opencode plugin add github:guotree/opencode-search-plugin
|
|
22
|
+
|
|
23
|
+
# or from npm (add @1.0.1 to pin)
|
|
24
|
+
opencode plugin add opencode-search-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
or declare it in `opencode.json(c)`:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://opencode.ai/config.json",
|
|
32
|
+
"plugins": ["github:guotree/opencode-search-plugin"]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### One or two, by copying a file
|
|
37
|
+
|
|
38
|
+
Every plugin is a single `.ts` file with no relative imports and no build step. Drop it where OpenCode auto-discovers plugins:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
~/.config/opencode/plugins/ # global
|
|
42
|
+
<project>/.opencode/plugins/ # one project
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a location that is not auto-discovered, list it explicitly:
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
{ "plugins": ["./plugins/brave-search.ts"] }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> Do not install the package *and* copy its files. Both register the same websearch provider IDs, so the pool would contain each service twice.
|
|
52
|
+
|
|
53
|
+
### Skip the services you have no key for
|
|
54
|
+
|
|
55
|
+
Loading the package registers all nine, but a provider without a key never joins the search pool — see [Keys](#keys). To keep them out of the registry entirely:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"plugins": [
|
|
60
|
+
{
|
|
61
|
+
"package": "github:guotree/opencode-search-plugin",
|
|
62
|
+
"options": { "disable": ["serper", "serpapi", "youcom"] }
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`disable` takes the provider IDs listed in the table below.
|
|
69
|
+
|
|
70
|
+
## Keys
|
|
71
|
+
|
|
72
|
+
Every provider except AnySearch needs an API key. Give it one of two ways:
|
|
73
|
+
|
|
74
|
+
- **`/connect`** inside OpenCode — stored per provider, rotated without a restart, and survives environment changes.
|
|
75
|
+
- **Environment variable** — read as a fallback, so it works in headless and CI contexts.
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
AnySearch is the exception. It has an anonymous tier, so it is always registered and only attaches an `Authorization` header when a key exists.
|
|
80
|
+
|
|
81
|
+
Once keys are in place, pick how OpenCode chooses among them:
|
|
82
|
+
|
|
83
|
+
```jsonc
|
|
84
|
+
{ "websearch": { "provider": "random" } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`random` spreads queries across every bound provider. Set it to a specific ID such as `"brave"` to pin one, or `false` to turn web search off.
|
|
88
|
+
|
|
89
|
+
## Providers
|
|
90
|
+
|
|
91
|
+
| Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
|
|
92
|
+
| --- | --- | --- | --- | --- |
|
|
93
|
+
| AnySearch | `anysearch` | `ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
|
|
94
|
+
| Brave Search | `brave` | `BRAVE_SEARCH_API_KEY` | no | Independent index, 20 results max |
|
|
95
|
+
| Jina | `jina` | `JINA_API_KEY` | no | Page content is opt-in because it is token-billed |
|
|
96
|
+
| Linkup | `linkup` | `LINKUP_API_KEY` | no | `flash`/`fast`/`standard`/`deep` reasoning depth |
|
|
97
|
+
| Metaso | `metaso` | `METASO_API_KEY` | no | Billed per search, not per result |
|
|
98
|
+
| Querit | `querit` | `QUERIT_API_KEY` | no | Falls back to sentence matches when a snippet is missing |
|
|
99
|
+
| SerpApi | `serpapi` | `SERPAPI_API_KEY` | no | Google via SerpApi; also has its own free 1-hour server cache |
|
|
100
|
+
| Serper | `serper` | `SERPER_API_KEY` | no | Google via Serper; billed per query in credits |
|
|
101
|
+
| You.com | `youcom` | `YDC_API_KEY` | no | Web and news merged; extraction modes |
|
|
102
|
+
|
|
103
|
+
## Configuration
|
|
104
|
+
|
|
105
|
+
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.
|
|
106
|
+
|
|
107
|
+
### Shared by all nine
|
|
108
|
+
|
|
109
|
+
| Variable | Default | Meaning |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `<PROVIDER>_CACHE_TTL` | `600000` | Milliseconds to reuse a response for the same normalized query. `0` disables caching. |
|
|
112
|
+
|
|
113
|
+
### AnySearch
|
|
114
|
+
|
|
115
|
+
| Variable | Default | Meaning |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `ANYSEARCH_MAX_RESULTS` | `10` | Results per search, clamped to the API's 1–10 |
|
|
118
|
+
| `ANYSEARCH_TAG` | — | Pin the intent router to a sub-domain, e.g. `code.doc` |
|
|
119
|
+
| `ANYSEARCH_ZONE` | — | `cn` or `intl` |
|
|
120
|
+
| `ANYSEARCH_LANGUAGE` | — | Preferred result language, e.g. `zh-CN` |
|
|
121
|
+
| `ANYSEARCH_FORMAT` | `markdown` | `markdown` or `json` for the content field |
|
|
122
|
+
| `ANYSEARCH_API_KEY` | — | Optional; anonymous access works without it |
|
|
123
|
+
|
|
124
|
+
### Brave Search
|
|
125
|
+
|
|
126
|
+
| Variable | Default | Meaning |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `BRAVE_NUM` | `8` | Results per search, clamped to 1–20 (Brave's maximum) |
|
|
129
|
+
| `BRAVE_COUNTRY` | — | Two-letter country code, or `ALL` |
|
|
130
|
+
| `BRAVE_LANG` | — | Result language, e.g. `en`, `zh-hans`, `pt-br` |
|
|
131
|
+
| `BRAVE_FRESHNESS` | — | `pd` (24h), `pw` (7d), `pm` (31d), `py` (365d), or `YYYY-MM-DDtoYYYY-MM-DD` |
|
|
132
|
+
|
|
133
|
+
### Jina
|
|
134
|
+
|
|
135
|
+
| Variable | Default | Meaning |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `JINA_NUM` | `0` | Results per search, 1–100. `0` omits the parameter, which keeps Jina's faster and wider result types |
|
|
138
|
+
| `JINA_INCLUDE_CONTENT` | off | `true` returns each hit's full page markdown — far more context and billed tokens |
|
|
139
|
+
| `JINA_GL` / `JINA_HL` | — | Country / language code |
|
|
140
|
+
| `JINA_LOCATION` | — | City-level origin to simulate, e.g. `Berlin` |
|
|
141
|
+
|
|
142
|
+
### Linkup
|
|
143
|
+
|
|
144
|
+
| Variable | Default | Meaning |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `LINKUP_DEPTH` | `fast` | `flash`, `fast`, `standard`, or `deep` |
|
|
147
|
+
| `LINKUP_MAX_RESULTS` | `10` | Results per search, clamped to 1–100 |
|
|
148
|
+
|
|
149
|
+
### Metaso
|
|
150
|
+
|
|
151
|
+
| Variable | Default | Meaning |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `METASO_SEARCH_SIZE` | `20` | Results per search, clamped to 1–100 |
|
|
154
|
+
| `METASO_INCLUDE_RAW_CONTENT` | off | `true` returns every hit's full source text at the same search cost, but far more context tokens |
|
|
155
|
+
|
|
156
|
+
### Querit
|
|
157
|
+
|
|
158
|
+
| Variable | Default | Meaning |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `QUERIT_COUNT` | `8` | Results per search |
|
|
161
|
+
|
|
162
|
+
### SerpApi
|
|
163
|
+
|
|
164
|
+
| Variable | Default | Meaning |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `SERPAPI_GL` / `SERPAPI_HL` | — | Country / language code |
|
|
167
|
+
| `SERPAPI_TBS` | — | Advanced filter, e.g. `qdr:d` (past day), `qdr:y` (past year) |
|
|
168
|
+
|
|
169
|
+
There is deliberately no result-count knob: Google withdrew the `num` parameter, so SerpApi cannot honour one.
|
|
170
|
+
|
|
171
|
+
### Serper
|
|
172
|
+
|
|
173
|
+
| Variable | Default | Meaning |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `SERPER_NUM` | `10` | Results per search, clamped to 1–100 |
|
|
176
|
+
| `SERPER_GL` / `SERPER_HL` | — | Country / language code |
|
|
177
|
+
| `SERPER_TBS` | — | Time filter: `qdr:h`, `qdr:d`, `qdr:w`, `qdr:m`, `qdr:y` |
|
|
178
|
+
|
|
179
|
+
### You.com
|
|
180
|
+
|
|
181
|
+
| Variable | Default | Meaning |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `YOUCOM_COUNT` | `10` | Results per section (web and news each), clamped to 1–100 |
|
|
184
|
+
| `YOUCOM_EXTRACTION` | `highlights` | `highlights` returns answering passages, `none` returns bare snippets, `full_page` crawls every hit and bills per live-fetched page |
|
|
185
|
+
| `YOUCOM_LANGUAGE` / `YOUCOM_COUNTRY` | — | e.g. `EN` / `US` |
|
|
186
|
+
| `YOUCOM_FRESHNESS` | — | `day`, `week`, `month`, `year`, or `YYYY-MM-DDtoYYYY-MM-DD` |
|
|
187
|
+
|
|
188
|
+
## How a query flows
|
|
189
|
+
|
|
190
|
+
1. OpenCode asks the selected provider for the query.
|
|
191
|
+
2. The normalized query (trimmed, whitespace collapsed) is checked against an in-memory cache. A hit returns immediately — repeat queries are free and instant.
|
|
192
|
+
3. On a miss the key is resolved again, so a key rotated in `/connect` applies to the very next search.
|
|
193
|
+
4. The request goes out; failures surface the HTTP status and response body so you can see what the provider actually said.
|
|
194
|
+
5. The response is schema-decoded, mapped to `{ url, title, content, time }`, and cached.
|
|
195
|
+
|
|
196
|
+
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.
|
|
197
|
+
|
|
198
|
+
## Troubleshooting
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
# Is the plugin loaded, and did it fail?
|
|
202
|
+
opencode plugin list
|
|
203
|
+
|
|
204
|
+
# Which providers are actually in the pool?
|
|
205
|
+
# A provider missing here has no key, or is listed in `disable`.
|
|
206
|
+
opencode api get "/api/websearch/provider"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Also worth checking:
|
|
210
|
+
|
|
211
|
+
- **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.
|
|
212
|
+
- **A search fails with `Connect X in /connect, or set X_API_KEY`** — the key disappeared between binding and searching. Reconnect or re-export.
|
|
213
|
+
- **Everything is slow or burning credits** — leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
|
|
214
|
+
- **Nothing happens after editing a plugin file** — OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
|
|
215
|
+
|
|
216
|
+
Logs live at `~/.local/share/opencode/log/opencode.log`; filter for `loading plugin` to see every plugin OpenCode resolved, with its entrypoint.
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npm install
|
|
222
|
+
npm run check # typecheck + tests
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `npm run typecheck` — strict TypeScript across every plugin and test.
|
|
226
|
+
- `npm test` — the shared provider contract, per-provider behaviour, date parsing, aggregate registration, and repository-structure guards.
|
|
227
|
+
|
|
228
|
+
`@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.
|
|
229
|
+
|
|
230
|
+
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).
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
[MIT](LICENSE)
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
[English](README.md) · **简体中文**
|
|
2
|
+
|
|
3
|
+
# opencode-search-plugin
|
|
4
|
+
|
|
5
|
+
九个相互独立的 [OpenCode](https://opencode.ai) v2 插件,各自注册一个网页搜索 provider。每个文件都能单独使用:复制一个进你的插件目录,或者安装这个包一次拿全九个。
|
|
6
|
+
|
|
7
|
+
[AnySearch](https://www.anysearch.com/) · [Brave Search](https://brave.com/search/api/) · [Jina](https://jina.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
|
+
|
|
9
|
+
## 为什么做这个插件
|
|
10
|
+
|
|
11
|
+
OpenCode v2 内置了五个网页搜索 provider:`exa`、`firecrawl`、`parallel`、`tavily`、`tinyfish`。在这个范围之外,还有大量搜索 API 提供免费额度或免费层级,足以支撑 agent 的日常检索而不必为此付费。
|
|
12
|
+
|
|
13
|
+
本仓库为其中九家补上了插件,把可用池从五个 provider 扩到十四个。AnySearch 无需任何 key 即可使用;其余八家只在你提供 key 之后才会绑定,因此装上这个包也不会把查询路由到一个注定失败的服务上。至于哪几家值得用,取决于它们各自当前的免费额度,长期依赖某家之前建议先确认它的定价。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
### 一次性装全九个:包安装
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
# 从 GitHub 安装(追加 #v1.0.1 可固定到该 tag)
|
|
21
|
+
opencode plugin add github:guotree/opencode-search-plugin
|
|
22
|
+
|
|
23
|
+
# 或从 npm 安装(追加 @1.0.1 可固定到该版本)
|
|
24
|
+
opencode plugin add opencode-search-plugin
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
或者写进 `opencode.json(c)`:
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
{
|
|
31
|
+
"$schema": "https://opencode.ai/config.json",
|
|
32
|
+
"plugins": ["github:guotree/opencode-search-plugin"]
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 只要一两个:直接复制文件
|
|
37
|
+
|
|
38
|
+
每个插件都是单个 `.ts` 文件,没有相对导入,也没有构建步骤。放进 OpenCode 会自动发现插件的位置即可:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
~/.config/opencode/plugins/ # 全局
|
|
42
|
+
<project>/.opencode/plugins/ # 单个项目
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
如果位置不在自动发现范围内,就显式列出:
|
|
46
|
+
|
|
47
|
+
```jsonc
|
|
48
|
+
{ "plugins": ["./plugins/brave-search.ts"] }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
> **不要既装包、又复制它的文件。** 两者注册的是同一批 websearch provider ID,搜索池里每家服务会出现两次。
|
|
52
|
+
|
|
53
|
+
### 跳过你没有 key 的服务
|
|
54
|
+
|
|
55
|
+
装包会注册全部九家,但没有 key 的 provider 永远不会进入搜索池——见下文[密钥](#密钥)。想让它们连注册表都不出现:
|
|
56
|
+
|
|
57
|
+
```jsonc
|
|
58
|
+
{
|
|
59
|
+
"plugins": [
|
|
60
|
+
{
|
|
61
|
+
"package": "github:guotree/opencode-search-plugin",
|
|
62
|
+
"options": { "disable": ["serper", "serpapi", "youcom"] }
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`disable` 填的是下表中的 provider ID。
|
|
69
|
+
|
|
70
|
+
## 密钥
|
|
71
|
+
|
|
72
|
+
除 AnySearch 外,每家服务都需要 API key。两种给法二选一:
|
|
73
|
+
|
|
74
|
+
- **OpenCode 内的 `/connect`** —— 按 provider 存储,轮换后无需重启,且不受环境变量变动影响。
|
|
75
|
+
- **环境变量** —— 作为兜底读取,因此在无界面和 CI 场景下也能工作。
|
|
76
|
+
|
|
77
|
+
没有 key 的 provider 会保持未绑定状态:它不在 websearch 池中,所以 `"provider": "random"` 绝不会把查询路由到一个注定失败的服务上。
|
|
78
|
+
|
|
79
|
+
AnySearch 是例外。它有匿名额度,因此始终注册,只有存在 key 时才附带 `Authorization` 头。
|
|
80
|
+
|
|
81
|
+
密钥就位后,再决定 OpenCode 如何在它们之间做选择:
|
|
82
|
+
|
|
83
|
+
```jsonc
|
|
84
|
+
{ "websearch": { "provider": "random" } }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`random` 会把查询分散到所有已绑定的 provider。填具体 ID(例如 `"brave"`)则固定用某一家,填 `false` 则完全关闭网页搜索。
|
|
88
|
+
|
|
89
|
+
## 支持的服务
|
|
90
|
+
|
|
91
|
+
| 服务 | ID | 密钥(环境变量 / `/connect`) | 匿名可用 | 说明 |
|
|
92
|
+
| --- | --- | --- | --- | --- |
|
|
93
|
+
| AnySearch | `anysearch` | `ANYSEARCH_API_KEY` | 是 | 按 IP 的免费额度;意图路由,`cn` / `intl` 分区 |
|
|
94
|
+
| Brave Search | `brave` | `BRAVE_SEARCH_API_KEY` | 否 | 独立索引,最多 20 条结果 |
|
|
95
|
+
| Jina | `jina` | `JINA_API_KEY` | 否 | 页面正文按 token 计费,因此默认不取 |
|
|
96
|
+
| Linkup | `linkup` | `LINKUP_API_KEY` | 否 | `flash` / `fast` / `standard` / `deep` 四档推理深度 |
|
|
97
|
+
| Metaso | `metaso` | `METASO_API_KEY` | 否 | 按次搜索计费,不按结果条数 |
|
|
98
|
+
| Querit | `querit` | `QUERIT_API_KEY` | 否 | 摘要缺失时回退到句子级匹配 |
|
|
99
|
+
| SerpApi | `serpapi` | `SERPAPI_API_KEY` | 否 | 经 SerpApi 调用 Google;另有其自带的免费 1 小时服务端缓存 |
|
|
100
|
+
| Serper | `serper` | `SERPER_API_KEY` | 否 | 经 Serper 调用 Google;按查询消耗积分计费 |
|
|
101
|
+
| You.com | `youcom` | `YDC_API_KEY` | 否 | 网页与新闻结果合并;多种正文抽取模式 |
|
|
102
|
+
|
|
103
|
+
## 配置
|
|
104
|
+
|
|
105
|
+
所有配置项都是环境变量,OpenCode 启动时读取一次——改了要重启。默认值取自各家 API 文档,所以一开始什么都不设就是合理的起点。
|
|
106
|
+
|
|
107
|
+
### 九家通用
|
|
108
|
+
|
|
109
|
+
| 变量 | 默认值 | 含义 |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `<PROVIDER>_CACHE_TTL` | `600000` | 同一条归一化查询可复用响应的毫秒数。`0` 关闭缓存。 |
|
|
112
|
+
|
|
113
|
+
### AnySearch
|
|
114
|
+
|
|
115
|
+
| 变量 | 默认值 | 含义 |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `ANYSEARCH_MAX_RESULTS` | `10` | 每次搜索的结果数,限制在 API 允许的 1–10 |
|
|
118
|
+
| `ANYSEARCH_TAG` | — | 把意图路由器固定到某个子域,例如 `code.doc` |
|
|
119
|
+
| `ANYSEARCH_ZONE` | — | `cn` 或 `intl` |
|
|
120
|
+
| `ANYSEARCH_LANGUAGE` | — | 首选结果语言,例如 `zh-CN` |
|
|
121
|
+
| `ANYSEARCH_FORMAT` | `markdown` | content 字段用 `markdown` 还是 `json` |
|
|
122
|
+
| `ANYSEARCH_API_KEY` | — | 可选;不设也能匿名使用 |
|
|
123
|
+
|
|
124
|
+
### Brave Search
|
|
125
|
+
|
|
126
|
+
| 变量 | 默认值 | 含义 |
|
|
127
|
+
| --- | --- | --- |
|
|
128
|
+
| `BRAVE_NUM` | `8` | 每次搜索的结果数,限制在 1–20(Brave 的上限) |
|
|
129
|
+
| `BRAVE_COUNTRY` | — | 两位国家代码,或 `ALL` |
|
|
130
|
+
| `BRAVE_LANG` | — | 结果语言,例如 `en`、`zh-hans`、`pt-br` |
|
|
131
|
+
| `BRAVE_FRESHNESS` | — | `pd`(24 小时)、`pw`(7 天)、`pm`(31 天)、`py`(365 天),或 `YYYY-MM-DDtoYYYY-MM-DD` |
|
|
132
|
+
|
|
133
|
+
### Jina
|
|
134
|
+
|
|
135
|
+
| 变量 | 默认值 | 含义 |
|
|
136
|
+
| --- | --- | --- |
|
|
137
|
+
| `JINA_NUM` | `0` | 每次搜索的结果数,1–100。`0` 表示不传该参数,从而保留 Jina 更快、类型更全的结果 |
|
|
138
|
+
| `JINA_INCLUDE_CONTENT` | 关 | `true` 返回每条命中的整页 markdown——上下文和计费 token 都会大幅增加 |
|
|
139
|
+
| `JINA_GL` / `JINA_HL` | — | 国家 / 语言代码 |
|
|
140
|
+
| `JINA_LOCATION` | — | 模拟的城市级来源,例如 `Berlin` |
|
|
141
|
+
|
|
142
|
+
### Linkup
|
|
143
|
+
|
|
144
|
+
| 变量 | 默认值 | 含义 |
|
|
145
|
+
| --- | --- | --- |
|
|
146
|
+
| `LINKUP_DEPTH` | `fast` | `flash`、`fast`、`standard` 或 `deep` |
|
|
147
|
+
| `LINKUP_MAX_RESULTS` | `10` | 每次搜索的结果数,限制在 1–100 |
|
|
148
|
+
|
|
149
|
+
### Metaso
|
|
150
|
+
|
|
151
|
+
| 变量 | 默认值 | 含义 |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| `METASO_SEARCH_SIZE` | `20` | 每次搜索的结果数,限制在 1–100 |
|
|
154
|
+
| `METASO_INCLUDE_RAW_CONTENT` | 关 | `true` 返回每条命中的完整原文,搜索成本不变,但上下文 token 大幅增加 |
|
|
155
|
+
|
|
156
|
+
### Querit
|
|
157
|
+
|
|
158
|
+
| 变量 | 默认值 | 含义 |
|
|
159
|
+
| --- | --- | --- |
|
|
160
|
+
| `QUERIT_COUNT` | `8` | 每次搜索的结果数 |
|
|
161
|
+
|
|
162
|
+
### SerpApi
|
|
163
|
+
|
|
164
|
+
| 变量 | 默认值 | 含义 |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `SERPAPI_GL` / `SERPAPI_HL` | — | 国家 / 语言代码 |
|
|
167
|
+
| `SERPAPI_TBS` | — | 高级筛选,例如 `qdr:d`(一天内)、`qdr:y`(一年内) |
|
|
168
|
+
|
|
169
|
+
这里**刻意没有**结果条数的配置项:Google 已经废弃了 `num` 参数,SerpApi 无法遵守它。
|
|
170
|
+
|
|
171
|
+
### Serper
|
|
172
|
+
|
|
173
|
+
| 变量 | 默认值 | 含义 |
|
|
174
|
+
| --- | --- | --- |
|
|
175
|
+
| `SERPER_NUM` | `10` | 每次搜索的结果数,限制在 1–100 |
|
|
176
|
+
| `SERPER_GL` / `SERPER_HL` | — | 国家 / 语言代码 |
|
|
177
|
+
| `SERPER_TBS` | — | 时间筛选:`qdr:h`、`qdr:d`、`qdr:w`、`qdr:m`、`qdr:y` |
|
|
178
|
+
|
|
179
|
+
### You.com
|
|
180
|
+
|
|
181
|
+
| 变量 | 默认值 | 含义 |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `YOUCOM_COUNT` | `10` | 每个分区(网页、新闻各自)的结果数,限制在 1–100 |
|
|
184
|
+
| `YOUCOM_EXTRACTION` | `highlights` | `highlights` 返回直接回答问题的段落,`none` 只给原始摘要,`full_page` 会抓取每个命中的页面并按实时抓取的页数计费 |
|
|
185
|
+
| `YOUCOM_LANGUAGE` / `YOUCOM_COUNTRY` | — | 例如 `EN` / `US` |
|
|
186
|
+
| `YOUCOM_FRESHNESS` | — | `day`、`week`、`month`、`year`,或 `YYYY-MM-DDtoYYYY-MM-DD` |
|
|
187
|
+
|
|
188
|
+
## 一次查询的完整流程
|
|
189
|
+
|
|
190
|
+
1. OpenCode 向选定的 provider 发起查询。
|
|
191
|
+
2. 归一化后的查询(去首尾、压缩空白)先查内存缓存;命中则直接返回——重复查询免费且即时。
|
|
192
|
+
3. 未命中时重新解析密钥,因此在 `/connect` 里轮换的 key 立即对下一次搜索生效。
|
|
193
|
+
4. 发出请求;失败时会带上 HTTP 状态码和响应体,让你看到服务方到底说了什么。
|
|
194
|
+
5. 响应经 schema 解码,映射为 `{ url, title, content, time }`,然后写入缓存。
|
|
195
|
+
|
|
196
|
+
日期解析支持相对表述(`3 hours ago`、`3小时前`)、绝对格式(`Sep 30, 2026`、`31 Mar 2026`)和 ISO 8601。凡是宽松解析会凭空造出错误年份的字符串——比如 `Nov 1`,`Date.parse` 会读成 2001 年——一律丢弃,而不是返回一个错误的时间戳。
|
|
197
|
+
|
|
198
|
+
## 排错
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
# 插件加载了吗?失败了吗?
|
|
202
|
+
opencode plugin list
|
|
203
|
+
|
|
204
|
+
# 搜索池里到底有哪些 provider?
|
|
205
|
+
# 这里少了一家,说明它没有 key,或者被写进了 `disable`。
|
|
206
|
+
opencode api get "/api/websearch/provider"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
另外几个值得检查的地方:
|
|
210
|
+
|
|
211
|
+
- **某家 provider 不在池里** —— 它没有 key。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。
|
|
212
|
+
- **搜索报错 `Connect X in /connect, or set X_API_KEY`** —— key 在绑定和搜索之间消失了。重新连接或重新导出。
|
|
213
|
+
- **一切都很慢、或者积分烧得飞快** —— 把 `*_CACHE_TTL` 保持在默认值。时间窗内的重复查询由内存直接返回。
|
|
214
|
+
- **改了插件文件却没反应** —— OpenCode 会监视插件并自动重载;如果没有,执行 `opencode service restart`。
|
|
215
|
+
|
|
216
|
+
日志在 `~/.local/share/opencode/log/opencode.log`;过滤 `loading plugin` 可以看到 OpenCode 解析到的每个插件及其入口文件。
|
|
217
|
+
|
|
218
|
+
## 开发
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npm install
|
|
222
|
+
npm run check # 类型检查 + 测试
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- `npm run typecheck` —— 对所有插件和测试做严格 TypeScript 检查。
|
|
226
|
+
- `npm test` —— 覆盖通用 provider 契约、各家行为、日期解析、聚合注册,以及仓库结构守卫。
|
|
227
|
+
|
|
228
|
+
`@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 后,类型检查和测试照常可用,而使用者安装时拿不到这份依赖树。
|
|
229
|
+
|
|
230
|
+
存在结构测试,是因为每个文件必须保持可独立复制:一旦某个插件开始 import 同级模块、共享代码块不再是逐字相同的副本、或者某家的 ID 出现偏移,测试就会失败。详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
231
|
+
|
|
232
|
+
## 许可证
|
|
233
|
+
|
|
234
|
+
[MIT](LICENSE)
|
package/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { Plugin } from "@opencode/plugin/effect"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import type { Scope } from "effect"
|
|
4
|
+
|
|
5
|
+
import { effect as anysearch } from "./plugins/anysearch-search"
|
|
6
|
+
import { effect as brave } from "./plugins/brave-search"
|
|
7
|
+
import { effect as jina } from "./plugins/jina-search"
|
|
8
|
+
import { effect as linkup } from "./plugins/linkup-search"
|
|
9
|
+
import { effect as metaso } from "./plugins/metaso-search"
|
|
10
|
+
import { effect as querit } from "./plugins/querit-search"
|
|
11
|
+
import { effect as serpapi } from "./plugins/serpapi-search"
|
|
12
|
+
import { effect as serper } from "./plugins/serper-search"
|
|
13
|
+
import { effect as youcom } from "./plugins/youcom-search"
|
|
14
|
+
|
|
15
|
+
type ProviderEffect = (ctx: Plugin.Context) => Effect.Effect<void, never, Scope.Scope>
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The nine providers this package registers, in alphabetical order. Each entry
|
|
19
|
+
* is the same effect its standalone file uses as a default export, so a file
|
|
20
|
+
* copied out of this repository behaves exactly as it does here.
|
|
21
|
+
*
|
|
22
|
+
* The key of each entry is the websearch provider ID -- the one that appears in
|
|
23
|
+
* `/api/websearch/provider`, in `websearch.provider`, and in the `disable`
|
|
24
|
+
* option below. Renaming one would strand existing configuration.
|
|
25
|
+
*/
|
|
26
|
+
const PROVIDERS: ReadonlyArray<readonly [id: string, run: ProviderEffect]> = [
|
|
27
|
+
["anysearch", anysearch],
|
|
28
|
+
["brave", brave],
|
|
29
|
+
["jina", jina],
|
|
30
|
+
["linkup", linkup],
|
|
31
|
+
["metaso", metaso],
|
|
32
|
+
["querit", querit],
|
|
33
|
+
["serpapi", serpapi],
|
|
34
|
+
["serper", serper],
|
|
35
|
+
["youcom", youcom],
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Reads the `disable` plugin option. A key-requiring provider that is disabled
|
|
40
|
+
* here is never registered, which is how a user trims the pool down to the
|
|
41
|
+
* services they actually hold keys for.
|
|
42
|
+
*/
|
|
43
|
+
function disabledProviders(ctx: Plugin.Context): ReadonlySet<string> {
|
|
44
|
+
const requested: unknown = ctx.options["disable"]
|
|
45
|
+
if (!Array.isArray(requested)) return new Set()
|
|
46
|
+
return new Set(requested.filter((entry): entry is string => typeof entry === "string"))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default Plugin.define({
|
|
50
|
+
id: "opencode-search-plugin",
|
|
51
|
+
effect: (ctx) =>
|
|
52
|
+
Effect.gen(function* () {
|
|
53
|
+
const disabled = disabledProviders(ctx)
|
|
54
|
+
for (const [id, run] of PROVIDERS) {
|
|
55
|
+
if (disabled.has(id)) continue
|
|
56
|
+
yield* run(ctx)
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-search-plugin",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Nine self-contained websearch provider plugins for OpenCode v2: AnySearch, Brave, Jina, Linkup, Metaso, Querit, SerpApi, Serper and You.com.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"opencode",
|
|
9
|
+
"opencode-plugin",
|
|
10
|
+
"websearch",
|
|
11
|
+
"search",
|
|
12
|
+
"serp",
|
|
13
|
+
"agent"
|
|
14
|
+
],
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/guotree/opencode-search-plugin.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/guotree/opencode-search-plugin#readme",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/guotree/opencode-search-plugin/issues"
|
|
22
|
+
},
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./index.ts",
|
|
25
|
+
"./anysearch-search": "./plugins/anysearch-search.ts",
|
|
26
|
+
"./brave-search": "./plugins/brave-search.ts",
|
|
27
|
+
"./jina-search": "./plugins/jina-search.ts",
|
|
28
|
+
"./linkup-search": "./plugins/linkup-search.ts",
|
|
29
|
+
"./metaso-search": "./plugins/metaso-search.ts",
|
|
30
|
+
"./querit-search": "./plugins/querit-search.ts",
|
|
31
|
+
"./serpapi-search": "./plugins/serpapi-search.ts",
|
|
32
|
+
"./serper-search": "./plugins/serper-search.ts",
|
|
33
|
+
"./youcom-search": "./plugins/youcom-search.ts",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"plugins",
|
|
38
|
+
"index.ts",
|
|
39
|
+
"README.md",
|
|
40
|
+
"README.zh-CN.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"typecheck": "tsc --noEmit",
|
|
45
|
+
"test": "vitest run",
|
|
46
|
+
"test:watch": "vitest",
|
|
47
|
+
"check": "npm run typecheck && npm run test"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@opencode/plugin": "2.0.30",
|
|
54
|
+
"@types/node": "^26.6.5",
|
|
55
|
+
"effect": "4.0.1",
|
|
56
|
+
"typescript": "^7.0.2",
|
|
57
|
+
"vitest": "^5.0.5"
|
|
58
|
+
}
|
|
59
|
+
}
|