opencode-search-plugin 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -6
- package/README.zh-CN.md +11 -6
- package/package.json +1 -1
- package/plugins/anysearch-search.ts +21 -3
- package/plugins/brave-search.ts +22 -3
- package/plugins/deepseek-search.ts +21 -3
- package/plugins/jina-search.ts +28 -3
- package/plugins/keenable-search.ts +21 -3
- package/plugins/linkup-search.ts +21 -3
- package/plugins/metaso-search.ts +21 -3
- package/plugins/querit-search.ts +21 -3
- package/plugins/serpapi-search.ts +21 -3
- package/plugins/serper-search.ts +21 -3
- package/plugins/youcom-search.ts +21 -3
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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
|
|
|
@@ -14,12 +14,12 @@ This repository adds plugins for eleven more of them, taking the pool from five
|
|
|
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.
|
|
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.
|
|
18
18
|
|
|
19
19
|
### 1. From npm (recommended)
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
# add @2.
|
|
22
|
+
# add @2.2.0 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.
|
|
38
|
+
# add #v2.2.0 to pin
|
|
39
39
|
opencode plugin add github:guotree/opencode-search-plugin
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -117,7 +117,7 @@ opencode plugin update github:guotree/opencode-search-plugin
|
|
|
117
117
|
opencode plugin update
|
|
118
118
|
```
|
|
119
119
|
|
|
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.
|
|
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.
|
|
121
121
|
|
|
122
122
|
## Uninstall
|
|
123
123
|
|
|
@@ -153,6 +153,8 @@ Once keys are in place, pick how OpenCode chooses among them:
|
|
|
153
153
|
|
|
154
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
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.
|
|
157
|
+
|
|
156
158
|
| Provider | ID | Key (env / `/connect`) | Anonymous | Notes |
|
|
157
159
|
| --- | --- | --- | --- | --- |
|
|
158
160
|
| AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | yes | Free tier per IP; intent router, `cn`/`intl` zones |
|
|
@@ -176,6 +178,7 @@ Knobs are environment variables, read once when OpenCode starts — change one a
|
|
|
176
178
|
| Variable | Default | Meaning |
|
|
177
179
|
| --- | --- | --- |
|
|
178
180
|
| `<PROVIDER>_CACHE_TTL` | `600000` | Milliseconds to reuse a response for the same normalized query. `0` disables caching. |
|
|
181
|
+
| `<PROVIDER>_TIMEOUT` | `25000` | Milliseconds to wait for the provider before failing. `0` removes the deadline but keeps cancellation. |
|
|
179
182
|
|
|
180
183
|
### AnySearch
|
|
181
184
|
|
|
@@ -211,6 +214,7 @@ Knobs are environment variables, read once when OpenCode starts — change one a
|
|
|
211
214
|
| `JINA_INCLUDE_CONTENT` | off | `true` returns each hit's full page markdown — far more context and billed tokens |
|
|
212
215
|
| `JINA_GL` / `JINA_HL` | — | Country / language code |
|
|
213
216
|
| `JINA_LOCATION` | — | City-level origin to simulate, e.g. `Berlin` |
|
|
217
|
+
| `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 |
|
|
214
218
|
|
|
215
219
|
### Keenable
|
|
216
220
|
|
|
@@ -272,7 +276,7 @@ There is deliberately no result-count knob: Google withdrew the `num` parameter,
|
|
|
272
276
|
1. OpenCode asks the selected provider for the query.
|
|
273
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.
|
|
274
278
|
3. On a miss the key is resolved again, so a key rotated in `/connect` applies to the very next search.
|
|
275
|
-
4. The request goes out
|
|
279
|
+
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.
|
|
276
280
|
5. The response is schema-decoded, mapped to `{ url, title, content, time }`, and cached.
|
|
277
281
|
|
|
278
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.
|
|
@@ -294,6 +298,7 @@ Also worth checking:
|
|
|
294
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.
|
|
295
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.
|
|
296
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.
|
|
297
302
|
- **Everything is slow or burning credits** — leave `*_CACHE_TTL` at its default. Repeated queries within the window are served from memory.
|
|
298
303
|
- **Nothing happens after editing a plugin file** — OpenCode watches plugins and reloads them; if it does not, `opencode service restart`.
|
|
299
304
|
|
package/README.zh-CN.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
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
|
|
|
@@ -14,12 +14,12 @@ OpenCode v2 内置了五个网页搜索 provider:`exa`、`firecrawl`、`parall
|
|
|
14
14
|
|
|
15
15
|
## 安装
|
|
16
16
|
|
|
17
|
-
三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@2.
|
|
17
|
+
三种方式等价,任选其一。**推荐 npm**:已发布到 registry 的版本不可变,固定 `@2.2.0` 后每次安装都拿到同一份字节;GitHub 的 tag 在理论上可以被维护者移动、指向别的 commit。前两种装的是同一个完整包(十一个全有),只想用一两个就用第三种。
|
|
18
18
|
|
|
19
19
|
### 1. 从 npm 安装(推荐)
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
# 追加 @2.
|
|
22
|
+
# 追加 @2.2.0 可固定到该版本
|
|
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.
|
|
38
|
+
# 追加 #v2.2.0 可固定到该 tag
|
|
39
39
|
opencode plugin add github:guotree/opencode-search-plugin
|
|
40
40
|
```
|
|
41
41
|
|
|
@@ -117,7 +117,7 @@ opencode plugin update github:guotree/opencode-search-plugin
|
|
|
117
117
|
opencode plugin update
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
升级目标用的是配置时的说明符:npm 安装用包名,GitHub 安装只认自己的 `github:` 形式——用裸包名会报 `Plugin is not configured`。先跑 `opencode plugin check` 查看有哪些过期。未固定版本的安装会跟上新版本;固定过的——npm 形式的 `@2.
|
|
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)` 里的说明符。本地复制的文件则用新版本覆盖旧文件来升级。
|
|
121
121
|
|
|
122
122
|
## 卸载
|
|
123
123
|
|
|
@@ -153,6 +153,8 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
|
|
|
153
153
|
|
|
154
154
|
下表每个 ID 都带 `osp-` 前缀——这是本包的命名空间。OpenCode 内置 provider 用的是裸厂商名(`exa`、`tavily`),厂商自己的插件也会注册裸名(Keenable 就已经有一个),所以前缀是避免撞名的唯一保障:既避开搜索池里的重复,也避开 `/connect` 里按 integration ID 存凭据的那张表。在 `websearch.provider` 和 `enable`/`disable` 里请原样使用带前缀的 ID。
|
|
155
155
|
|
|
156
|
+
命名空间同样体现在显示名上:`/connect` 里本包的每个条目都带 `(osp)` 标记——内置的 `DeepSeek` 旁边是 `DeepSeek (osp)`——存在这里的 key 不会和内置 provider 的混淆。搜索池里仍用纯品牌名,因为池中只有搜索 provider,不存在撞名问题。
|
|
157
|
+
|
|
156
158
|
| 服务 | ID | 密钥(环境变量 / `/connect`) | 匿名可用 | 说明 |
|
|
157
159
|
| --- | --- | --- | --- | --- |
|
|
158
160
|
| AnySearch | `osp-anysearch` | `ANYSEARCH_API_KEY` | 是 | 按 IP 的免费额度;意图路由,`cn` / `intl` 分区 |
|
|
@@ -176,6 +178,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
|
|
|
176
178
|
| 变量 | 默认值 | 含义 |
|
|
177
179
|
| --- | --- | --- |
|
|
178
180
|
| `<PROVIDER>_CACHE_TTL` | `600000` | 同一条归一化查询可复用响应的毫秒数。`0` 关闭缓存。 |
|
|
181
|
+
| `<PROVIDER>_TIMEOUT` | `25000` | 等待 provider 返回的毫秒数,超时即失败。`0` 取消该期限,但保留取消能力。 |
|
|
179
182
|
|
|
180
183
|
### AnySearch
|
|
181
184
|
|
|
@@ -211,6 +214,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
|
|
|
211
214
|
| `JINA_INCLUDE_CONTENT` | 关 | `true` 返回每条命中的整页 markdown——上下文和计费 token 都会大幅增加 |
|
|
212
215
|
| `JINA_GL` / `JINA_HL` | — | 国家 / 语言代码 |
|
|
213
216
|
| `JINA_LOCATION` | — | 模拟的城市级来源,例如 `Berlin` |
|
|
217
|
+
| `JINA_TIMEOUT` | `25000`;开启 `JINA_INCLUDE_CONTENT` 时为 `60000` | 唯一一个默认值随模式变化的 provider:抓取整页本来就比取摘要慢得多 |
|
|
214
218
|
|
|
215
219
|
### Keenable
|
|
216
220
|
|
|
@@ -272,7 +276,7 @@ AnySearch 和 Keenable 是例外。两家都有匿名层级,因此始终注册
|
|
|
272
276
|
1. OpenCode 向选定的 provider 发起查询。
|
|
273
277
|
2. 归一化后的查询(去首尾、压缩空白)先查内存缓存;命中则直接返回——重复查询免费且即时。
|
|
274
278
|
3. 未命中时重新解析密钥,因此在 `/connect` 里轮换的 key 立即对下一次搜索生效。
|
|
275
|
-
4.
|
|
279
|
+
4. 发出请求,且带期限(`<PROVIDER>_TIMEOUT`,默认 25 秒)。OpenCode 自身并不给搜索加任何超时,所以没有这道期限的话,一个接受了连接却不再应答的 provider 会挂住整轮对话。失败时会带上 HTTP 状态码和响应体;如果是超时,报错会直接点明是超时。
|
|
276
280
|
5. 响应经 schema 解码,映射为 `{ url, title, content, time }`,然后写入缓存。
|
|
277
281
|
|
|
278
282
|
日期解析支持相对表述(`3 hours ago`、`3小时前`)、绝对格式(`Sep 30, 2026`、`31 Mar 2026`)和 ISO 8601。凡是宽松解析会凭空造出错误年份的字符串——比如 `Nov 1`,`Date.parse` 会读成 2001 年——一律丢弃,而不是返回一个错误的时间戳。
|
|
@@ -294,6 +298,7 @@ opencode api get "/api/websearch/provider"
|
|
|
294
298
|
- **某家 provider 不在池里** —— 它没有 key,或者被 `disable`(或收窄后的 `enable`)过滤掉了。执行 `/connect` 或导出对应环境变量,然后重启;在 `/connect` 里做的凭据变更会自动重绑,不需要重启。本该在却不在的名字多半是拼写错误:日志会点出无法识别的 ID 和 `enable`/`disable` 的交集。
|
|
295
299
|
- **搜索报错 `Keenable rate limited the request (HTTP 429)`** —— 无 key 层级每个 IP 每小时 1,000 次、每秒 10 次,且与同一出口地址上的其他人共用。改用 `KEENABLE_API_KEY` 就会记到你自己的额度上。
|
|
296
300
|
- **搜索报错 `Connect X in /connect, or set X_API_KEY`** —— key 在绑定和搜索之间消失了。重新连接或重新导出。
|
|
301
|
+
- **搜索报错 `X search timed out after 25000ms`** —— provider 接受了请求,但在期限内没有返回。重试通常就好;如果某家 provider 在你这边一贯偏慢,调大它的 `<PROVIDER>_TIMEOUT`。这道期限是刻意加的:没有它,一个卡住的 provider 会挂住整轮对话,因为 OpenCode 自身并不给搜索设置任何超时。
|
|
297
302
|
- **一切都很慢、或者积分烧得飞快** —— 把 `*_CACHE_TTL` 保持在默认值。时间窗内的重复查询由内存直接返回。
|
|
298
303
|
- **改了插件文件却没反应** —— OpenCode 会监视插件并自动重载;如果没有,执行 `opencode service restart`。
|
|
299
304
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-search-plugin",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
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",
|
|
@@ -73,8 +73,12 @@ function envStr(name: string): string {
|
|
|
73
73
|
// ANYSEARCH_ZONE "cn" or "intl"
|
|
74
74
|
// ANYSEARCH_LANGUAGE preferred result language, e.g. "zh-CN"
|
|
75
75
|
// ANYSEARCH_FORMAT "markdown" (default) or "json" for the content field
|
|
76
|
+
// ANYSEARCH_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
76
77
|
// ANYSEARCH_API_KEY optional; anonymous access works without it
|
|
77
78
|
const CACHE_TTL = envInt("ANYSEARCH_CACHE_TTL", 600_000)
|
|
79
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
80
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
81
|
+
const SEARCH_TIMEOUT = envInt("ANYSEARCH_TIMEOUT", 25_000)
|
|
78
82
|
const SEARCH_SIZE = Math.min(Math.max(envInt("ANYSEARCH_MAX_RESULTS", 10), 1), 10)
|
|
79
83
|
const TAG = envStr("ANYSEARCH_TAG")
|
|
80
84
|
const ZONE = envStr("ANYSEARCH_ZONE")
|
|
@@ -103,6 +107,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
111
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
112
|
+
// removes the deadline but never the cancellation, because
|
|
113
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
114
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
115
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
116
|
+
}
|
|
117
|
+
|
|
106
118
|
// Unlike the other providers in this repo, AnySearch never gates on `bound`:
|
|
107
119
|
// anonymous access is a first-class mode, so the provider is always in the
|
|
108
120
|
// pool. The key is still read per call, which is why no credential-event
|
|
@@ -113,7 +125,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
113
125
|
// Make "AnySearch" show up in /connect with an API-key method.
|
|
114
126
|
yield* ctx.integration.transform((editor) => {
|
|
115
127
|
editor.update(ID, (integration) => {
|
|
116
|
-
integration.name = "AnySearch"
|
|
128
|
+
integration.name = "AnySearch (osp)"
|
|
117
129
|
})
|
|
118
130
|
editor.method.update({
|
|
119
131
|
integrationID: ID,
|
|
@@ -159,7 +171,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
159
171
|
if (key) headers.Authorization = "Bearer " + key
|
|
160
172
|
|
|
161
173
|
const response = yield* Effect.tryPromise({
|
|
162
|
-
try: () =>
|
|
174
|
+
try: (signal) =>
|
|
163
175
|
fetch("https://api.anysearch.com/v1/search", {
|
|
164
176
|
method: "POST",
|
|
165
177
|
headers,
|
|
@@ -171,8 +183,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
171
183
|
...(LANGUAGE ? { language: LANGUAGE } : {}),
|
|
172
184
|
format: FORMAT,
|
|
173
185
|
}),
|
|
186
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
174
187
|
}),
|
|
175
|
-
|
|
188
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
189
|
+
// for a key problem that was never there.
|
|
190
|
+
catch: (error) =>
|
|
191
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
192
|
+
? new Error("AnySearch search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
193
|
+
: new Error("AnySearch request failed: " + String(error)),
|
|
176
194
|
})
|
|
177
195
|
|
|
178
196
|
if (!response.ok) {
|
package/plugins/brave-search.ts
CHANGED
|
@@ -114,12 +114,17 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
114
114
|
// BRAVE_LANG result language code, e.g. "en", "zh-hans", "pt-br"
|
|
115
115
|
// BRAVE_FRESHNESS "pd" (24h) "pw" (7d) "pm" (31d) "py" (365d) or a
|
|
116
116
|
// "YYYY-MM-DDtoYYYY-MM-DD" range
|
|
117
|
+
// BRAVE_TIMEOUT ms to wait for Brave before failing (0 removes the
|
|
118
|
+
// deadline; cancellation still applies)
|
|
117
119
|
// BRAVE_SEARCH_API_KEY required; from https://brave.com/search/api/
|
|
118
120
|
const CACHE_TTL = envInt("BRAVE_CACHE_TTL", 600_000)
|
|
119
121
|
const SEARCH_SIZE = Math.min(Math.max(envInt("BRAVE_NUM", 8), 1), 20)
|
|
120
122
|
const COUNTRY = envStr("BRAVE_COUNTRY")
|
|
121
123
|
const SEARCH_LANG = envStr("BRAVE_LANG")
|
|
122
124
|
const FRESHNESS = envStr("BRAVE_FRESHNESS")
|
|
125
|
+
// OpenCode's own providers each bound themselves at 25s, and the host adds no
|
|
126
|
+
// deadline of its own around `execute`, so this is the only one that exists.
|
|
127
|
+
const SEARCH_TIMEOUT = envInt("BRAVE_TIMEOUT", 25_000)
|
|
123
128
|
|
|
124
129
|
const CACHE_LIMIT = 200
|
|
125
130
|
const cache = new Map<string, { at: number; results: SearchResult[] }>()
|
|
@@ -143,12 +148,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
143
148
|
}
|
|
144
149
|
}
|
|
145
150
|
|
|
151
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
152
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
153
|
+
// removes the deadline but never the cancellation, because
|
|
154
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
155
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
156
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
157
|
+
}
|
|
158
|
+
|
|
146
159
|
export const effect = (ctx: Plugin.Context) =>
|
|
147
160
|
Effect.gen(function* () {
|
|
148
161
|
// Make "Brave Search" show up in /connect with an API-key method.
|
|
149
162
|
yield* ctx.integration.transform((editor) => {
|
|
150
163
|
editor.update(ID, (integration) => {
|
|
151
|
-
integration.name = "Brave Search"
|
|
164
|
+
integration.name = "Brave Search (osp)"
|
|
152
165
|
})
|
|
153
166
|
editor.method.update({
|
|
154
167
|
integrationID: ID,
|
|
@@ -212,14 +225,20 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
212
225
|
if (FRESHNESS) url.searchParams.set("freshness", FRESHNESS)
|
|
213
226
|
|
|
214
227
|
const response = yield* Effect.tryPromise({
|
|
215
|
-
try: () =>
|
|
228
|
+
try: (signal) =>
|
|
216
229
|
fetch(url, {
|
|
217
230
|
headers: {
|
|
218
231
|
Accept: "application/json",
|
|
219
232
|
"X-Subscription-Token": key,
|
|
220
233
|
},
|
|
234
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
221
235
|
}),
|
|
222
|
-
|
|
236
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
237
|
+
// for a key problem that was never there.
|
|
238
|
+
catch: (error) =>
|
|
239
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
240
|
+
? new Error("Brave search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
241
|
+
: new Error("Brave request failed: " + String(error)),
|
|
223
242
|
})
|
|
224
243
|
|
|
225
244
|
if (!response.ok) {
|
|
@@ -112,8 +112,12 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
112
112
|
//
|
|
113
113
|
// DEEPSEEK_CACHE_TTL ms to reuse a response for the same query (0 disables)
|
|
114
114
|
// DEEPSEEK_COUNT results per search, clamped to 1-50
|
|
115
|
+
// DEEPSEEK_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
115
116
|
// DEEPSEEK_API_KEY required; from https://platform.deepseek.com
|
|
116
117
|
const CACHE_TTL = envInt("DEEPSEEK_CACHE_TTL", 600_000)
|
|
118
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
119
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
120
|
+
const SEARCH_TIMEOUT = envInt("DEEPSEEK_TIMEOUT", 25_000)
|
|
117
121
|
const COUNT = Math.min(Math.max(envInt("DEEPSEEK_COUNT", 10), 1), 50)
|
|
118
122
|
|
|
119
123
|
const CACHE_LIMIT = 200
|
|
@@ -138,12 +142,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
146
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
147
|
+
// removes the deadline but never the cancellation, because
|
|
148
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
149
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
150
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
export const effect = (ctx: Plugin.Context) =>
|
|
142
154
|
Effect.gen(function* () {
|
|
143
155
|
// Make "DeepSeek" show up in /connect with an API-key method.
|
|
144
156
|
yield* ctx.integration.transform((editor) => {
|
|
145
157
|
editor.update(ID, (integration) => {
|
|
146
|
-
integration.name = "DeepSeek"
|
|
158
|
+
integration.name = "DeepSeek (osp)"
|
|
147
159
|
})
|
|
148
160
|
editor.method.update({
|
|
149
161
|
integrationID: ID,
|
|
@@ -200,7 +212,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
const response = yield* Effect.tryPromise({
|
|
203
|
-
try: () =>
|
|
215
|
+
try: (signal) =>
|
|
204
216
|
fetch("https://api.deepseek.com/v1/tools/search", {
|
|
205
217
|
method: "POST",
|
|
206
218
|
headers: {
|
|
@@ -212,8 +224,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
212
224
|
query: queryKey,
|
|
213
225
|
count: COUNT,
|
|
214
226
|
}),
|
|
227
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
215
228
|
}),
|
|
216
|
-
|
|
229
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
230
|
+
// for a key problem that was never there.
|
|
231
|
+
catch: (error) =>
|
|
232
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
233
|
+
? new Error("DeepSeek search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
234
|
+
: new Error("DeepSeek request failed: " + String(error)),
|
|
217
235
|
})
|
|
218
236
|
|
|
219
237
|
if (!response.ok) {
|
package/plugins/jina-search.ts
CHANGED
|
@@ -77,6 +77,10 @@ function envStr(name: string): string {
|
|
|
77
77
|
// JINA_GL country code for results, e.g. "us", "cn"
|
|
78
78
|
// JINA_HL language code for results, e.g. "en", "zh-cn"
|
|
79
79
|
// JINA_LOCATION city-level origin to simulate, e.g. "Berlin"
|
|
80
|
+
// JINA_TIMEOUT ms to wait before failing (0 removes the deadline).
|
|
81
|
+
// Defaults to 25000, or 60000 when JINA_INCLUDE_CONTENT
|
|
82
|
+
// is on, because whole-page fetches legitimately take
|
|
83
|
+
// far longer than a snippet search
|
|
80
84
|
// JINA_API_KEY required; free key at https://jina.ai/?sui=apikey
|
|
81
85
|
const CACHE_TTL = envInt("JINA_CACHE_TTL", 600_000)
|
|
82
86
|
const INCLUDE_CONTENT = envStr("JINA_INCLUDE_CONTENT") === "true"
|
|
@@ -84,6 +88,13 @@ const SEARCH_SIZE = Math.min(Math.max(envInt("JINA_NUM", 0), 0), 100)
|
|
|
84
88
|
const GL = envStr("JINA_GL")
|
|
85
89
|
const HL = envStr("JINA_HL")
|
|
86
90
|
const LOCATION = envStr("JINA_LOCATION")
|
|
91
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
92
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
93
|
+
// Jina is the one provider whose default has to vary: asking for page content
|
|
94
|
+
// turns a snippet search into a crawl, and a shared budget would either cut
|
|
95
|
+
// that off or be padded so high that a genuine hang takes a minute to surface.
|
|
96
|
+
const DEFAULT_TIMEOUT = INCLUDE_CONTENT ? 60_000 : 25_000
|
|
97
|
+
const SEARCH_TIMEOUT = envInt("JINA_TIMEOUT", DEFAULT_TIMEOUT)
|
|
87
98
|
|
|
88
99
|
const CACHE_LIMIT = 200
|
|
89
100
|
const cache = new Map<string, { at: number; results: SearchResult[] }>()
|
|
@@ -107,12 +118,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
122
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
123
|
+
// removes the deadline but never the cancellation, because
|
|
124
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
125
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
126
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
127
|
+
}
|
|
128
|
+
|
|
110
129
|
export const effect = (ctx: Plugin.Context) =>
|
|
111
130
|
Effect.gen(function* () {
|
|
112
131
|
// Make "Jina" show up in /connect with an API-key method.
|
|
113
132
|
yield* ctx.integration.transform((editor) => {
|
|
114
133
|
editor.update(ID, (integration) => {
|
|
115
|
-
integration.name = "Jina"
|
|
134
|
+
integration.name = "Jina (osp)"
|
|
116
135
|
})
|
|
117
136
|
editor.method.update({
|
|
118
137
|
integrationID: ID,
|
|
@@ -169,7 +188,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
169
188
|
}
|
|
170
189
|
|
|
171
190
|
const response = yield* Effect.tryPromise({
|
|
172
|
-
try: () =>
|
|
191
|
+
try: (signal) =>
|
|
173
192
|
fetch("https://s.jina.ai/", {
|
|
174
193
|
method: "POST",
|
|
175
194
|
headers: {
|
|
@@ -188,8 +207,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
188
207
|
...(HL ? { hl: HL } : {}),
|
|
189
208
|
...(LOCATION ? { location: LOCATION } : {}),
|
|
190
209
|
}),
|
|
210
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
191
211
|
}),
|
|
192
|
-
|
|
212
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
213
|
+
// for a key problem that was never there.
|
|
214
|
+
catch: (error) =>
|
|
215
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
216
|
+
? new Error("Jina search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
217
|
+
: new Error("Jina request failed: " + String(error)),
|
|
193
218
|
})
|
|
194
219
|
|
|
195
220
|
if (!response.ok) {
|
|
@@ -122,8 +122,12 @@ function envStr(name: string): string {
|
|
|
122
122
|
// leave unset and the organization's default applies
|
|
123
123
|
// KEENABLE_TITLE app name sent as X-Keenable-Title, which the keyless
|
|
124
124
|
// endpoint requires to attribute traffic (max 256 chars)
|
|
125
|
+
// KEENABLE_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
125
126
|
// KEENABLE_API_KEY optional; keyless access works without it
|
|
126
127
|
const CACHE_TTL = envInt("KEENABLE_CACHE_TTL", 600_000)
|
|
128
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
129
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
130
|
+
const SEARCH_TIMEOUT = envInt("KEENABLE_TIMEOUT", 25_000)
|
|
127
131
|
const SEARCH_SIZE = Math.min(Math.max(envInt("KEENABLE_MAX_RESULTS", 10), 1), 50)
|
|
128
132
|
const MODE = envStr("KEENABLE_MODE")
|
|
129
133
|
const TITLE = (envStr("KEENABLE_TITLE") || "opencode-search-plugin").slice(0, 256)
|
|
@@ -153,6 +157,14 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
153
157
|
}
|
|
154
158
|
}
|
|
155
159
|
|
|
160
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
161
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
162
|
+
// removes the deadline but never the cancellation, because
|
|
163
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
164
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
165
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
166
|
+
}
|
|
167
|
+
|
|
156
168
|
// Unlike the key-requiring providers in this repo, Keenable never gates on
|
|
157
169
|
// `bound`: keyless access is a first-class mode, so the provider is always in
|
|
158
170
|
// the pool. The key is still read per call, which is why no credential-event
|
|
@@ -163,7 +175,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
163
175
|
// Make "Keenable" show up in /connect with an API-key method.
|
|
164
176
|
yield* ctx.integration.transform((editor) => {
|
|
165
177
|
editor.update(ID, (integration) => {
|
|
166
|
-
integration.name = "Keenable"
|
|
178
|
+
integration.name = "Keenable (osp)"
|
|
167
179
|
})
|
|
168
180
|
editor.method.update({
|
|
169
181
|
integrationID: ID,
|
|
@@ -213,7 +225,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
213
225
|
if (key) headers["X-API-Key"] = key
|
|
214
226
|
|
|
215
227
|
const response = yield* Effect.tryPromise({
|
|
216
|
-
try: () =>
|
|
228
|
+
try: (signal) =>
|
|
217
229
|
fetch(key ? SEARCH_URL : PUBLIC_URL, {
|
|
218
230
|
method: "POST",
|
|
219
231
|
headers,
|
|
@@ -222,8 +234,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
222
234
|
max_results: SEARCH_SIZE,
|
|
223
235
|
...(MODE ? { mode: MODE } : {}),
|
|
224
236
|
}),
|
|
237
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
225
238
|
}),
|
|
226
|
-
|
|
239
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
240
|
+
// for a key problem that was never there.
|
|
241
|
+
catch: (error) =>
|
|
242
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
243
|
+
? new Error("Keenable search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
244
|
+
: new Error("Keenable request failed: " + String(error)),
|
|
227
245
|
})
|
|
228
246
|
|
|
229
247
|
if (!response.ok) {
|
package/plugins/linkup-search.ts
CHANGED
|
@@ -54,8 +54,12 @@ function envStr(name: string): string {
|
|
|
54
54
|
// vendor's recommended default for agents), "standard"
|
|
55
55
|
// (single agentic pass) or "deep" (several iterations)
|
|
56
56
|
// LINKUP_MAX_RESULTS results per search, clamped to 1-100
|
|
57
|
+
// LINKUP_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
57
58
|
// LINKUP_API_KEY required; from https://app.linkup.so
|
|
58
59
|
const CACHE_TTL = envInt("LINKUP_CACHE_TTL", 600_000)
|
|
60
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
61
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
62
|
+
const SEARCH_TIMEOUT = envInt("LINKUP_TIMEOUT", 25_000)
|
|
59
63
|
const SEARCH_SIZE = Math.min(Math.max(envInt("LINKUP_MAX_RESULTS", 10), 1), 100)
|
|
60
64
|
const DEPTH = ["flash", "fast", "standard", "deep"].includes(envStr("LINKUP_DEPTH"))
|
|
61
65
|
? envStr("LINKUP_DEPTH")
|
|
@@ -83,12 +87,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
83
87
|
}
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
91
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
92
|
+
// removes the deadline but never the cancellation, because
|
|
93
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
94
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
95
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
96
|
+
}
|
|
97
|
+
|
|
86
98
|
export const effect = (ctx: Plugin.Context) =>
|
|
87
99
|
Effect.gen(function* () {
|
|
88
100
|
// Make "Linkup" show up in /connect with an API-key method.
|
|
89
101
|
yield* ctx.integration.transform((editor) => {
|
|
90
102
|
editor.update(ID, (integration) => {
|
|
91
|
-
integration.name = "Linkup"
|
|
103
|
+
integration.name = "Linkup (osp)"
|
|
92
104
|
})
|
|
93
105
|
editor.method.update({
|
|
94
106
|
integrationID: ID,
|
|
@@ -145,7 +157,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
145
157
|
}
|
|
146
158
|
|
|
147
159
|
const response = yield* Effect.tryPromise({
|
|
148
|
-
try: () =>
|
|
160
|
+
try: (signal) =>
|
|
149
161
|
fetch("https://api.linkup.so/v1/search", {
|
|
150
162
|
method: "POST",
|
|
151
163
|
headers: {
|
|
@@ -159,8 +171,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
159
171
|
outputType: "searchResults",
|
|
160
172
|
maxResults: SEARCH_SIZE,
|
|
161
173
|
}),
|
|
174
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
162
175
|
}),
|
|
163
|
-
|
|
176
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
177
|
+
// for a key problem that was never there.
|
|
178
|
+
catch: (error) =>
|
|
179
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
180
|
+
? new Error("Linkup search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
181
|
+
: new Error("Linkup request failed: " + String(error)),
|
|
164
182
|
})
|
|
165
183
|
|
|
166
184
|
if (!response.ok) {
|
package/plugins/metaso-search.ts
CHANGED
|
@@ -111,8 +111,12 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
111
111
|
// METASO_INCLUDE_RAW_CONTENT true returns the full source text of every hit: same
|
|
112
112
|
// search cost, far more context tokens. Only worth it
|
|
113
113
|
// when you would otherwise webfetch those hits anyway.
|
|
114
|
+
// METASO_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
114
115
|
// METASO_API_KEY required; from https://metaso.cn
|
|
115
116
|
const CACHE_TTL = envInt("METASO_CACHE_TTL", 600_000)
|
|
117
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
118
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
119
|
+
const SEARCH_TIMEOUT = envInt("METASO_TIMEOUT", 25_000)
|
|
116
120
|
const SEARCH_SIZE = Math.min(Math.max(envInt("METASO_SEARCH_SIZE", 20), 1), 100)
|
|
117
121
|
const INCLUDE_RAW_CONTENT = envStr("METASO_INCLUDE_RAW_CONTENT") === "true"
|
|
118
122
|
|
|
@@ -138,12 +142,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
138
142
|
}
|
|
139
143
|
}
|
|
140
144
|
|
|
145
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
146
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
147
|
+
// removes the deadline but never the cancellation, because
|
|
148
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
149
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
150
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
151
|
+
}
|
|
152
|
+
|
|
141
153
|
export const effect = (ctx: Plugin.Context) =>
|
|
142
154
|
Effect.gen(function* () {
|
|
143
155
|
// Make "Metaso" show up in /connect with an API-key method.
|
|
144
156
|
yield* ctx.integration.transform((editor) => {
|
|
145
157
|
editor.update(ID, (integration) => {
|
|
146
|
-
integration.name = "Metaso Search"
|
|
158
|
+
integration.name = "Metaso Search (osp)"
|
|
147
159
|
})
|
|
148
160
|
editor.method.update({
|
|
149
161
|
integrationID: ID,
|
|
@@ -200,7 +212,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
const response = yield* Effect.tryPromise({
|
|
203
|
-
try: () =>
|
|
215
|
+
try: (signal) =>
|
|
204
216
|
fetch("https://metaso.cn/api/v1/search", {
|
|
205
217
|
method: "POST",
|
|
206
218
|
headers: {
|
|
@@ -216,8 +228,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
216
228
|
includeRawContent: INCLUDE_RAW_CONTENT,
|
|
217
229
|
conciseSnippet: true,
|
|
218
230
|
}),
|
|
231
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
219
232
|
}),
|
|
220
|
-
|
|
233
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
234
|
+
// for a key problem that was never there.
|
|
235
|
+
catch: (error) =>
|
|
236
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
237
|
+
? new Error("Metaso search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
238
|
+
: new Error("Metaso request failed: " + String(error)),
|
|
221
239
|
})
|
|
222
240
|
|
|
223
241
|
if (!response.ok) {
|
package/plugins/querit-search.ts
CHANGED
|
@@ -122,8 +122,12 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
122
122
|
//
|
|
123
123
|
// QUERIT_CACHE_TTL ms to reuse a response for the same query (0 disables)
|
|
124
124
|
// QUERIT_COUNT results per search; Querit's own limit applies
|
|
125
|
+
// QUERIT_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
125
126
|
// QUERIT_API_KEY required; from https://querit.ai
|
|
126
127
|
const CACHE_TTL = envInt("QUERIT_CACHE_TTL", 600_000)
|
|
128
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
129
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
130
|
+
const SEARCH_TIMEOUT = envInt("QUERIT_TIMEOUT", 25_000)
|
|
127
131
|
const SEARCH_SIZE = envInt("QUERIT_COUNT", 8)
|
|
128
132
|
|
|
129
133
|
const CACHE_LIMIT = 200
|
|
@@ -148,12 +152,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
148
152
|
}
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
156
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
157
|
+
// removes the deadline but never the cancellation, because
|
|
158
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
159
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
160
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
export const effect = (ctx: Plugin.Context) =>
|
|
152
164
|
Effect.gen(function* () {
|
|
153
165
|
// Make "Querit" show up in /connect with an API-key method.
|
|
154
166
|
yield* ctx.integration.transform((editor) => {
|
|
155
167
|
editor.update(ID, (integration) => {
|
|
156
|
-
integration.name = "Querit"
|
|
168
|
+
integration.name = "Querit (osp)"
|
|
157
169
|
})
|
|
158
170
|
editor.method.update({
|
|
159
171
|
integrationID: ID,
|
|
@@ -210,7 +222,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
210
222
|
}
|
|
211
223
|
|
|
212
224
|
const response = yield* Effect.tryPromise({
|
|
213
|
-
try: () =>
|
|
225
|
+
try: (signal) =>
|
|
214
226
|
fetch("https://api.querit.ai/v1/search", {
|
|
215
227
|
method: "POST",
|
|
216
228
|
headers: {
|
|
@@ -219,8 +231,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
219
231
|
Authorization: "Bearer " + key,
|
|
220
232
|
},
|
|
221
233
|
body: JSON.stringify({ query: queryKey, count: SEARCH_SIZE }),
|
|
234
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
222
235
|
}),
|
|
223
|
-
|
|
236
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
237
|
+
// for a key problem that was never there.
|
|
238
|
+
catch: (error) =>
|
|
239
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
240
|
+
? new Error("Querit search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
241
|
+
: new Error("Querit request failed: " + String(error)),
|
|
224
242
|
})
|
|
225
243
|
|
|
226
244
|
if (!response.ok) {
|
|
@@ -111,11 +111,15 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
111
111
|
// SERPAPI_GL country code for results, e.g. "us", "cn"
|
|
112
112
|
// SERPAPI_HL language code for results, e.g. "en", "zh-cn"
|
|
113
113
|
// SERPAPI_TBS advanced filters, e.g. "qdr:d" (past day), "qdr:y"
|
|
114
|
+
// SERPAPI_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
114
115
|
// SERPAPI_API_KEY required; from https://serpapi.com
|
|
115
116
|
//
|
|
116
117
|
// There is deliberately no result-count knob: Google withdrew the `num`
|
|
117
118
|
// parameter, so SerpApi cannot honour one and a count would be a lie.
|
|
118
119
|
const CACHE_TTL = envInt("SERPAPI_CACHE_TTL", 600_000)
|
|
120
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
121
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
122
|
+
const SEARCH_TIMEOUT = envInt("SERPAPI_TIMEOUT", 25_000)
|
|
119
123
|
const GL = envStr("SERPAPI_GL")
|
|
120
124
|
const HL = envStr("SERPAPI_HL")
|
|
121
125
|
const TBS = envStr("SERPAPI_TBS")
|
|
@@ -142,12 +146,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
150
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
151
|
+
// removes the deadline but never the cancellation, because
|
|
152
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
153
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
154
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
155
|
+
}
|
|
156
|
+
|
|
145
157
|
export const effect = (ctx: Plugin.Context) =>
|
|
146
158
|
Effect.gen(function* () {
|
|
147
159
|
// Make "SerpApi" show up in /connect with an API-key method.
|
|
148
160
|
yield* ctx.integration.transform((editor) => {
|
|
149
161
|
editor.update(ID, (integration) => {
|
|
150
|
-
integration.name = "SerpApi"
|
|
162
|
+
integration.name = "SerpApi (osp)"
|
|
151
163
|
})
|
|
152
164
|
editor.method.update({
|
|
153
165
|
integrationID: ID,
|
|
@@ -212,13 +224,19 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
212
224
|
if (TBS) url.searchParams.set("tbs", TBS)
|
|
213
225
|
|
|
214
226
|
const response = yield* Effect.tryPromise({
|
|
215
|
-
try: () =>
|
|
227
|
+
try: (signal) =>
|
|
216
228
|
fetch(url, {
|
|
217
229
|
headers: {
|
|
218
230
|
Accept: "application/json",
|
|
219
231
|
},
|
|
232
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
220
233
|
}),
|
|
221
|
-
|
|
234
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
235
|
+
// for a key problem that was never there.
|
|
236
|
+
catch: (error) =>
|
|
237
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
238
|
+
? new Error("SerpApi search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
239
|
+
: new Error("SerpApi request failed: " + String(error)),
|
|
222
240
|
})
|
|
223
241
|
|
|
224
242
|
if (!response.ok) {
|
package/plugins/serper-search.ts
CHANGED
|
@@ -112,8 +112,12 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
112
112
|
// SERPER_HL language code for results, e.g. "en", "zh-cn"
|
|
113
113
|
// SERPER_TBS time filter: "qdr:h" (hour) "qdr:d" (day) "qdr:w" (week)
|
|
114
114
|
// "qdr:m" (month) "qdr:y" (year)
|
|
115
|
+
// SERPER_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
115
116
|
// SERPER_API_KEY required; from https://serper.dev
|
|
116
117
|
const CACHE_TTL = envInt("SERPER_CACHE_TTL", 600_000)
|
|
118
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
119
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
120
|
+
const SEARCH_TIMEOUT = envInt("SERPER_TIMEOUT", 25_000)
|
|
117
121
|
const SEARCH_SIZE = Math.min(Math.max(envInt("SERPER_NUM", 10), 1), 100)
|
|
118
122
|
const GL = envStr("SERPER_GL")
|
|
119
123
|
const HL = envStr("SERPER_HL")
|
|
@@ -141,12 +145,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
141
145
|
}
|
|
142
146
|
}
|
|
143
147
|
|
|
148
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
149
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
150
|
+
// removes the deadline but never the cancellation, because
|
|
151
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
152
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
153
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
154
|
+
}
|
|
155
|
+
|
|
144
156
|
export const effect = (ctx: Plugin.Context) =>
|
|
145
157
|
Effect.gen(function* () {
|
|
146
158
|
// Make "Serper" show up in /connect with an API-key method.
|
|
147
159
|
yield* ctx.integration.transform((editor) => {
|
|
148
160
|
editor.update(ID, (integration) => {
|
|
149
|
-
integration.name = "Serper"
|
|
161
|
+
integration.name = "Serper (osp)"
|
|
150
162
|
})
|
|
151
163
|
editor.method.update({
|
|
152
164
|
integrationID: ID,
|
|
@@ -203,7 +215,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
203
215
|
}
|
|
204
216
|
|
|
205
217
|
const response = yield* Effect.tryPromise({
|
|
206
|
-
try: () =>
|
|
218
|
+
try: (signal) =>
|
|
207
219
|
fetch("https://google.serper.dev/search", {
|
|
208
220
|
method: "POST",
|
|
209
221
|
headers: {
|
|
@@ -218,8 +230,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
218
230
|
...(HL ? { hl: HL } : {}),
|
|
219
231
|
...(TBS ? { tbs: TBS } : {}),
|
|
220
232
|
}),
|
|
233
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
221
234
|
}),
|
|
222
|
-
|
|
235
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
236
|
+
// for a key problem that was never there.
|
|
237
|
+
catch: (error) =>
|
|
238
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
239
|
+
? new Error("Serper search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
240
|
+
: new Error("Serper request failed: " + String(error)),
|
|
223
241
|
})
|
|
224
242
|
|
|
225
243
|
if (!response.ok) {
|
package/plugins/youcom-search.ts
CHANGED
|
@@ -140,8 +140,12 @@ type SearchResult = ReturnType<typeof toResult>
|
|
|
140
140
|
// YOUCOM_LANGUAGE result language, e.g. "EN", "ZH-HANS"
|
|
141
141
|
// YOUCOM_COUNTRY geographic focus, e.g. "US", "CN"
|
|
142
142
|
// YOUCOM_FRESHNESS "day" | "week" | "month" | "year" or "YYYY-MM-DDtoYYYY-MM-DD"
|
|
143
|
+
// YOUCOM_TIMEOUT ms to wait before failing (0 removes the deadline)
|
|
143
144
|
// YDC_API_KEY required; You.com's canonical env var, shared with its SDKs
|
|
144
145
|
const CACHE_TTL = envInt("YOUCOM_CACHE_TTL", 600_000)
|
|
146
|
+
// OpenCode's own providers each bounded themselves at 25s, and the host adds
|
|
147
|
+
// no deadline of its own around `execute`, so this is the only one that exists.
|
|
148
|
+
const SEARCH_TIMEOUT = envInt("YOUCOM_TIMEOUT", 25_000)
|
|
145
149
|
const SEARCH_SIZE = Math.min(Math.max(envInt("YOUCOM_COUNT", 10), 1), 100)
|
|
146
150
|
const EXTRACTION = ["none", "highlights", "full_page"].includes(envStr("YOUCOM_EXTRACTION"))
|
|
147
151
|
? envStr("YOUCOM_EXTRACTION")
|
|
@@ -172,12 +176,20 @@ function writeCache(queryKey: string, results: SearchResult[]): void {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
// A hung upstream must not hang the agent: the deadline bounds it, and the
|
|
180
|
+
// thunk's signal carries host cancellation into the socket. `timeout <= 0`
|
|
181
|
+
// removes the deadline but never the cancellation, because
|
|
182
|
+
// `AbortSignal.timeout(0)` aborts instantly rather than "never".
|
|
183
|
+
function timedSignal(signal: AbortSignal, timeout: number): AbortSignal {
|
|
184
|
+
return timeout > 0 ? AbortSignal.any([signal, AbortSignal.timeout(timeout)]) : signal
|
|
185
|
+
}
|
|
186
|
+
|
|
175
187
|
export const effect = (ctx: Plugin.Context) =>
|
|
176
188
|
Effect.gen(function* () {
|
|
177
189
|
// Make "You.com" show up in /connect with an API-key method.
|
|
178
190
|
yield* ctx.integration.transform((editor) => {
|
|
179
191
|
editor.update(ID, (integration) => {
|
|
180
|
-
integration.name = "You.com"
|
|
192
|
+
integration.name = "You.com (osp)"
|
|
181
193
|
})
|
|
182
194
|
editor.method.update({
|
|
183
195
|
integrationID: ID,
|
|
@@ -244,7 +256,7 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
244
256
|
: undefined
|
|
245
257
|
|
|
246
258
|
const response = yield* Effect.tryPromise({
|
|
247
|
-
try: () =>
|
|
259
|
+
try: (signal) =>
|
|
248
260
|
fetch("https://ydc-index.io/v1/search", {
|
|
249
261
|
method: "POST",
|
|
250
262
|
headers: {
|
|
@@ -260,8 +272,14 @@ export const effect = (ctx: Plugin.Context) =>
|
|
|
260
272
|
...(FRESHNESS ? { freshness: FRESHNESS } : {}),
|
|
261
273
|
...(extraction ? { extraction } : {}),
|
|
262
274
|
}),
|
|
275
|
+
signal: timedSignal(signal, SEARCH_TIMEOUT),
|
|
263
276
|
}),
|
|
264
|
-
|
|
277
|
+
// A timeout is its own failure: naming it stops the user hunting
|
|
278
|
+
// for a key problem that was never there.
|
|
279
|
+
catch: (error) =>
|
|
280
|
+
error instanceof Error && error.name === "TimeoutError"
|
|
281
|
+
? new Error("You.com search timed out after " + SEARCH_TIMEOUT + "ms")
|
|
282
|
+
: new Error("You.com request failed: " + String(error)),
|
|
265
283
|
})
|
|
266
284
|
|
|
267
285
|
if (!response.ok) {
|