@x1a0f3n9/dsh-web 0.1.5-rc.2

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
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.
@@ -0,0 +1,6 @@
1
+ # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
+ # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
+ # after editing either side, bring the other along and re-record with:
4
+ # pnpm run verify-translation-pairing --write packages/web/web/README.md
5
+ README.md: c8af47eb02c632a0b1142056b4148ce93ade817e
6
+ README.zh.md: 688ee3fa2dfcf223ff72e135dacead4e585411c7
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ ---
2
+ description: "The web access service (ctx.web): how deployments and plugin authors search the web and fetch URLs through interchangeable providers, with one selection policy and error vocabulary."
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ Use `dsh-web` to search the web or fetch a URL without tying callers to a specific vendor. It selects a usable backend for each operation and gives callers consistent cancellation, errors, and result limits. Choose it for plugins or tools that call `ctx.web.search()` or `ctx.web.fetch()`; the shipped `dsh-tool-web` tools load it for you. A search or fetch requires a configured, usable provider because this package does not make network requests on its own.
13
+
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
27
+
28
+ A composition that needs web access loads the `dsh-web` service and mounts at least one backend — a search provider and/or a fetch provider — and plugin or tool authors then call `ctx.web.search()` and `ctx.web.fetch()` directly. The service resolves the backend for each call, so callers never see provider ids unless they configured one.
29
+
30
+ ### When to choose it
31
+
32
+ Choose the service when a plugin or tool must search or fetch without hard-coding a vendor; a deployment that only uses the shipped `web_search`/`web_fetch` tools gets it for free through `dsh-tool-web`. You do not need it when the composition never reaches the web. The service adds no network access of its own: without at least one usable provider, every call fails with a structured `WebError`.
33
+
34
+ ### Minimal configuration
35
+
36
+ Load the service and let a single mounted backend auto-select, or pin a provider id with `searchProvider`/`fetchProvider`. The environment variables `$DSH_WEB_SEARCH_PROVIDER` and `$DSH_WEB_FETCH_PROVIDER` feed the same fields and are not a separate priority chain.
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-exa'
41
+ - name: '@x1a0f3n9/dsh-web-fetch-http'
42
+ ```
43
+
44
+ | Field | Default | Meaning |
45
+ |---|---|---|
46
+ | `searchProvider` | (unset) | Pinned search provider id; unset auto-selects when exactly one is usable |
47
+ | `searchProviderOrder` | (unset) | Exclusive search allowlist when no id is pinned; unusable ids are skipped, and an unlisted provider such as `deepseek-official` is not selected |
48
+ | `fetchProvider` | (unset) | Pinned fetch provider id; unset auto-selects when exactly one is usable |
49
+
50
+ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web) is the exhaustive source for every accepted field and its JSDoc.
51
+
52
+ ### Searching and fetching
53
+
54
+ `search()` runs one query and returns an optional provider answer plus a list of citeable sources; the service enforces `request.maxResults` by truncating `sources[]` and setting `truncated`. `fetch()` retrieves one URL and returns its final URL, status code, decoded body, and a truncation flag; a non-2xx response is a result, not an error.
55
+
56
+ ```text
57
+ // Search the web; sources[] is capped to maxResults:
58
+ const result = await ctx.web.search({ query: 'deepseek harness', maxResults: 8 })
59
+
60
+ // Fetch one URL; a non-2xx response is a result, not an error:
61
+ const page = await ctx.web.fetch({ url: 'https://example.com' })
62
+ ```
63
+
64
+ Both calls accept an optional `AbortSignal` that is forwarded to the provider for cancellation. The normalized request and result shapes are the contract callers build on; the vocabulary section of the [web subsystem](../../../docs/subsystems/web.md) reference describes them exhaustively.
65
+
66
+ ### Provider selection
67
+
68
+ Each call resolves its provider at execution time, and registration or load order never matters. A configured provider id wins when it is registered and usable; without a configured id, the service runs the single usable provider or fails clearly:
69
+
70
+ | Situation | Outcome |
71
+ |---|---|
72
+ | configured id registered and usable | runs that provider |
73
+ | configured id not registered | `WEB_PROVIDER_CONFIGURED_MISSING` |
74
+ | configured id registered but unavailable | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
75
+ | no id, first ordered provider usable | runs that provider |
76
+ | no id, order set, none of those usable | `WEB_PROVIDER_UNAVAILABLE` |
77
+ | no id, no order, exactly one registered usable provider | runs it |
78
+ | no id, no usable provider | `WEB_PROVIDER_UNAVAILABLE` |
79
+ | no id, no order, multiple usable providers | `WEB_PROVIDER_AMBIGUOUS` |
80
+
81
+ A provider's availability is a cheap local check — for example whether its API key is present — and never makes network calls, so selection stays fast and deterministic.
82
+
83
+ ### Failures and recovery
84
+
85
+ Failures throw `WebError` with a stable, machine-routable code; the message adds detail such as the missing provider id or the ambiguous candidate set. Callers route on the code and decide how to degrade. To change which backend a call uses, reconfigure the pinned id, mount or unmount providers, or fix the provider's configuration so its availability check passes.
86
+
87
+ -----
88
+
89
+ <a id="understand-the-implementation"></a>
90
+ ## Understand the implementation
91
+
92
+ <details>
93
+ <summary>Implementation internals — click to expand</summary>
94
+
95
+ This section explains the design decisions behind the service; the observable behavior is fully covered in [Use this package](#use-this-package).
96
+
97
+ ### Design philosophy
98
+
99
+ The package is built on one deliberate separation:
100
+
101
+ - **One seam, two independent operations.** Search and fetch share no request schema and no business logic, but they share one service so provider selection, cancellation, errors, and product configuration have a single owner. The parallel `Search`/`Fetch` method pairs are intentional.
102
+ - **Selection is never order-dependent.** A capability either pins a provider id or auto-selects when exactly one usable provider is registered; `search()`/`fetch()` resolve the provider at execution time.
103
+ - **The service owns the result bound.** `maxResults` is enforced by the seam after the provider returns, so an over-returning provider can never leak more sources than the caller asked for.
104
+
105
+ ### Source map
106
+
107
+ | File | Role |
108
+ |---|---|
109
+ | [`src/index.ts`](src/index.ts) | Plugin entry: the `WebRuntime` service, both provider registries, and execution-time selection |
110
+ | [`src/types.ts`](src/types.ts) | Vocabulary: request/result types, the closed `WebFetchBody` union, and the `WebError` taxonomy |
111
+ | — | No runtime invariant companion is published; provider maps are private and selection/result caps are enforced on each call; the seam publishes no independent registry or request/result observation stream. |
112
+
113
+ ### Data model
114
+
115
+ The request and result types define the normalized vocabulary callers build on — one `Search` pair and one `Fetch` pair — and the exhaustive fields and JSDoc live in [`src/types.ts`](src/types.ts) and the [web subsystem](../../../docs/subsystems/web.md) reference. Two deliberate choices shape them: `WebFetchBody` is a closed union (`html` | `text`) owned here, so adding a kind breaks compilation until every consumer handles it; `WebError` extends `HarnessError` with an open-string `code`, so consumers must tolerate provider-specific values. Source fields stay optional because not every provider returns all of them.
116
+
117
+ ### Selection flow
118
+
119
+ At call time the service resolves the provider — configured id first, then the unique usable provider — and throws the matching `WebError` when no clear winner exists. A search result then passes through `capSources`, which truncates `sources[]` to `maxResults` and flags `truncated`. Registration is effect-based: providers register with the calling fiber and unregister when it disposes, and a duplicate id within a capability kind is rejected at registration.
120
+
121
+ </details>
122
+
123
+ -----
124
+
125
+ <a id="further-exploration"></a>
126
+ ## Further Exploration
127
+
128
+ Read these pages when the package-level contract is not enough. They move from the shared vocabulary to the shipped backends, the model-facing tools, and the design rationale.
129
+
130
+ - [Web subsystem](../../../docs/subsystems/web.md) — the exhaustive search/fetch requests and results, provider availability, and error codes.
131
+ - [Web package map](../README.md) — the seven-package family and each role.
132
+ - [dsh-tool-web](../tool-web/README.md) — the model-facing `web_search` and `web_fetch` tools over this service.
133
+ - [dsh-web-fetch-http](../web-fetch-http/README.md) — the shipped anonymous HTTP(S) fetch backend.
134
+ - [Generated configuration catalog](../../../docs/config-catalog.md#deepseek-aidsh-web) — every accepted config field and its source declaration.
135
+ - [Web capability seam decision](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) — why search and fetch share one provider-selection service.
136
+
137
+ -----
138
+
139
+ <a id="model-experience"></a>
140
+ ## Model Experience
141
+
142
+ Indirectly, through `dsh-tool-web`, which renders the seam's normalized search results and fetch bodies to the model while this service contributes no prompt or schema.
143
+
144
+ #### KV Cache effect
145
+
146
+ No direct invalidation; the named consumer owns any request-prefix changes.
147
+
148
+ ## Known Limitations and Deferred Work
149
+
150
+ <a id="known-limitations-and-deferred-work"></a>
151
+
152
+
153
+ These limits define when the service is incomplete on its own. They are current package constraints.
154
+
155
+ - **No observation surface** — there is no provider-change event and no capability-status query; availability is observable only by running a search or fetch and routing the thrown code, and the no-provider failure is the generic `WEB_PROVIDER_UNAVAILABLE` with no per-provider reason enumeration ([Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)).
156
+ - **Search requests carry only `query` and `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until the backends can honor them ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)).
157
+ - **`WebFetchBody` has no `pdf` arm** — text-extractable PDF support is named deferred work; the closed union makes adding it a compile-enforced change across the web packages.
158
+ - **Provider-backed page extraction is out of scope of `fetch()`** — a Firecrawl/Tavily-style `web_extract` capability is deferred rather than widening the fetch operation.
159
+
160
+ <a id="dev-note"></a>
161
+ ### Dev Note
162
+
163
+ <details>
164
+ <summary>Working context for maintainers — click to expand</summary>
165
+
166
+ This Dev Note is working context for maintainers: open questions and undecided directions. It is explicitly non-authoritative — shipped behavior, limits, and rationale live in the sections above and the linked Agent Notes.
167
+
168
+ #### Future: observing provider state
169
+
170
+ No provider-change event or capability-status query exists; consumers observe availability only by executing a call and routing the thrown code. Restoring a small observation surface is possible if a consumer needs per-provider reasons, but the archived simplification note records why the earlier one was dropped.
171
+
172
+ </details>
package/README.zh.md ADDED
@@ -0,0 +1,172 @@
1
+ ---
2
+ description: "web 访问服务(ctx.web):部署方与插件作者如何通过可互换的提供方搜索 web 与抓取 URL,以及统一的选择策略与错误词汇。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # @x1a0f3n9/dsh-web
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ 使用 `dsh-web` 搜索 web 或抓取 URL,而无需让调用方依赖特定厂商。它为每项操作选择可用后端,并为调用方提供一致的取消、错误和结果上限。在调用 `ctx.web.search()` 或 `ctx.web.fetch()` 的插件或工具中选择它;已交付的 `dsh-tool-web` 工具会为你加载它。搜索或抓取需要已配置且可用的提供方,因为本包自身不发起网络请求。
13
+
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
24
+
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
27
+
28
+ 需要 web 访问的组合会加载 `dsh-web` 服务并挂载至少一个后端——搜索提供方和/或抓取提供方——插件或工具作者随后直接调用 `ctx.web.search()` 与 `ctx.web.fetch()`。服务会为每次调用解析后端,因此除非调用方配置了提供方 id,否则它们看不到提供方 id。
29
+
30
+ ### 何时选择
31
+
32
+ 当插件或工具必须搜索或抓取、又不希望硬编码厂商时选择本服务;只使用已交付的 `web_search`/`web_fetch` 工具的组合会通过 `dsh-tool-web` 自动加载本服务。当组合从不访问 web 时,你不需要它。服务本身不增加任何网络访问能力:没有至少一个可用提供方时,每次调用都会以结构化 `WebError` 失败。
33
+
34
+ ### 最小配置
35
+
36
+ 加载服务并让唯一挂载的后端自动选择,或用 `searchProvider`/`fetchProvider` 固定提供方 id。环境变量 `$DSH_WEB_SEARCH_PROVIDER` 与 `$DSH_WEB_FETCH_PROVIDER` 提供相同字段,不是另一条优先级链。
37
+
38
+ ```yaml
39
+ - name: '@x1a0f3n9/dsh-web'
40
+ - name: '@x1a0f3n9/dsh-web-search-exa'
41
+ - name: '@x1a0f3n9/dsh-web-fetch-http'
42
+ ```
43
+
44
+ | 字段 | 默认值 | 含义 |
45
+ |---|---|---|
46
+ | `searchProvider` | (未设置) | 固定的搜索提供方 id;未设置时仅在恰好一个可用时自动选择 |
47
+ | `searchProviderOrder` | (未设置) | 未固定 id 时的独占搜索允许列表;不可用的 id 会被跳过,不会选中未列出的提供方(例如 `deepseek-official`) |
48
+ | `fetchProvider` | (未设置) | 固定的抓取提供方 id;未设置时仅在恰好一个可用时自动选择 |
49
+
50
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web)是每个受支持字段及其 JSDoc 的穷尽式真源。
51
+
52
+ ### 搜索与抓取
53
+
54
+ `search()` 执行一次查询,返回可选的提供方答案与可引用的来源列表;服务强制执行 `request.maxResults`:截断 `sources[]` 并设置 `truncated`。`fetch()` 获取一个 URL,返回其最终 URL、状态码、解码后的正文与截断标志;非 2xx 响应是结果,不是错误。
55
+
56
+ ```text
57
+ // Search the web; sources[] is capped to maxResults:
58
+ const result = await ctx.web.search({ query: 'deepseek harness', maxResults: 8 })
59
+
60
+ // Fetch one URL; a non-2xx response is a result, not an error:
61
+ const page = await ctx.web.fetch({ url: 'https://example.com' })
62
+ ```
63
+
64
+ 两个调用都接受可选的 `AbortSignal`,用于把取消转发给提供方。规范化的请求与结果形状是调用方赖以构建的约定;[web 子系统](../../../docs/subsystems/web.zh.md) 参考中的词汇章节对其有穷尽式描述。
65
+
66
+ ### 提供方选择
67
+
68
+ 每次调用都在执行时解析提供方,注册或加载顺序从不影响结果。已配置的提供方 id 在已注册且可用时优先;没有配置 id 时,服务运行唯一可用的提供方,或在情况不明时明确失败:
69
+
70
+ | 情况 | 结果 |
71
+ |---|---|
72
+ | 已配置 id 已注册且可用 | 运行该提供方 |
73
+ | 已配置 id 未注册 | `WEB_PROVIDER_CONFIGURED_MISSING` |
74
+ | 已配置 id 已注册但不可用 | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
75
+ | 无 id,顺序中第一个可用提供方 | 运行该提供方 |
76
+ | 无 id,已设置顺序且其中没有可用提供方 | `WEB_PROVIDER_UNAVAILABLE` |
77
+ | 无 id,未设置顺序,恰好一个已注册的可用提供方 | 运行它 |
78
+ | 无 id,没有可用提供方 | `WEB_PROVIDER_UNAVAILABLE` |
79
+ | 无 id,未设置顺序,多个可用提供方 | `WEB_PROVIDER_AMBIGUOUS` |
80
+
81
+ 提供方的可用性是一项廉价的局部检查——例如其 API 密钥是否存在——并且从不发起网络调用,因此选择保持快速且确定。
82
+
83
+ ### 失败与恢复
84
+
85
+ 失败抛出 `WebError`,携带稳定、可按机器路由的 code;消息补充细节,例如缺失的提供方 id 或歧义候选集合。调用方按 code 路由并决定如何降级。要改变一次调用使用的后端,请重新配置固定的 id、挂载或卸载提供方,或修正提供方配置使其可用性检查通过。
86
+
87
+ -----
88
+
89
+ <a id="understand-the-implementation"></a>
90
+ ## 理解实现
91
+
92
+ <details>
93
+ <summary>实现细节——点击展开</summary>
94
+
95
+ 本节解释服务背后的设计决策;可观察行为已在[使用本包](#use-this-package)中完整说明。
96
+
97
+ ### 设计理念
98
+
99
+ 本包建立在一个刻意的分离之上:
100
+
101
+ - **一个 seam,两个独立操作。** 搜索与抓取没有共享请求 schema 或业务逻辑,但它们共用一个服务,使提供方选择、取消、错误与产品配置只有一个归属方。并行的 `Search`/`Fetch` 方法对是有意为之。
102
+ - **选择绝不依赖顺序。** 能力要么固定提供方 id,要么在恰好注册一个可用提供方时自动选择;`search()`/`fetch()` 在执行时解析提供方。
103
+ - **服务拥有结果上限。** `maxResults` 由 seam 在提供方返回后强制执行,因此超量返回的提供方绝不可能泄漏超出调用方要求的来源。
104
+
105
+ ### 源码地图
106
+
107
+ | 文件 | 职责 |
108
+ |---|---|
109
+ | [`src/index.ts`](src/index.ts) | 插件入口:`WebRuntime` 服务、两个提供方注册表与执行时选择 |
110
+ | [`src/types.ts`](src/types.ts) | 词汇:请求/结果类型、封闭的 `WebFetchBody` 联合与 `WebError` 分类体系 |
111
+ | — | 不发布运行时不变式配套项;提供方映射是私有数据,服务会在每次调用时执行提供方选择并强制执行结果上限;该 seam 不发布独立注册表,也不发布请求/结果观测流。 |
112
+
113
+ ### 数据模型
114
+
115
+ 请求与结果类型定义了调用方赖以构建的规范化词汇——`Search` 请求/结果对与 `Fetch` 请求/结果对各一组——穷尽式字段与 JSDoc 见 [`src/types.ts`](src/types.ts) 与 [web 子系统](../../../docs/subsystems/web.zh.md) 参考。两个刻意的选择塑造了它们:`WebFetchBody` 是这里拥有的封闭联合(`html` | `text`),因此新增类型会破坏编译,直到每个消费方都处理它;`WebError` 继承 `HarnessError`,携带开放的字符串 `code`,因此消费方必须容忍提供方专有的取值。来源字段保持可选,因为并非每个提供方都返回全部字段。
116
+
117
+ ### 选择流程
118
+
119
+ 执行时,服务先按配置 id、再按唯一可用提供方解析提供方,没有明确赢家时抛出对应的 `WebError`。搜索结果随后经过 `capSources`:把 `sources[]` 截断到 `maxResults` 并标记 `truncated`。注册基于 effect:提供方随调用 fiber 注册,fiber 释放时注销;同一能力类型下重复的 id 会在注册时被拒绝。
120
+
121
+ </details>
122
+
123
+ -----
124
+
125
+ <a id="further-exploration"></a>
126
+ ## 进一步探索
127
+
128
+ 当包级约定不够用时阅读以下页面。它们从共享词汇逐步进入已交付后端、面向模型的工具与设计依据。
129
+
130
+ - [web 子系统](../../../docs/subsystems/web.zh.md)——穷尽式的搜索/抓取请求与结果、提供方可用性与错误码。
131
+ - [web 包映射](../README.zh.md)——七包家族与各角色。
132
+ - [dsh-tool-web](../tool-web/README.zh.md)——构建于本服务之上的面向模型 `web_search` 与 `web_fetch` 工具。
133
+ - [dsh-web-fetch-http](../web-fetch-http/README.zh.md)——已交付的匿名 HTTP(S) 抓取后端。
134
+ - [生成配置目录](../../../docs/config-catalog.zh.md#deepseek-aidsh-web)——每个受支持配置字段及其源声明。
135
+ - [web 能力 seam 决策](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)——搜索与抓取为何共用一项提供方选择服务。
136
+
137
+ -----
138
+
139
+ <a id="model-experience"></a>
140
+ ## 模型体验
141
+
142
+ 间接地,通过 `dsh-tool-web`:该工具把 seam 规范化的搜索结果与抓取正文渲染给模型,而本服务不贡献任何提示词或 schema。
143
+
144
+ #### KV Cache 影响
145
+
146
+ 不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
147
+
148
+ ## 已知限制与延期工作
149
+
150
+ <a id="known-limitations-and-deferred-work"></a>
151
+
152
+
153
+ 这些限制说明本服务单独使用时在哪些方面不完整。它们是当前包约束。
154
+
155
+ - **没有观测接口**:没有提供方变更事件或能力状态查询;可用性只能通过执行搜索或抓取并按抛出的 code 路由来观测,无提供方失败是通用的 `WEB_PROVIDER_UNAVAILABLE`,不枚举逐提供方原因(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md))。
156
+ - **搜索请求只携带 `query` 与 `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至后端都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
157
+ - **`WebFetchBody` 没有 `pdf` 分支**:可提取文本的 PDF 支持属于明确的延期工作;封闭联合会使新增该分支成为跨 web 包、由编译强制执行的变更。
158
+ - **提供方支持的页面提取不属于 `fetch()` 范围**:Firecrawl/Tavily 风格的 `web_extract` 能力延期,而不会扩展抓取操作。
159
+
160
+ <a id="dev-note"></a>
161
+ ### 开发备注
162
+
163
+ <details>
164
+ <summary>维护者的工作上下文——点击展开</summary>
165
+
166
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和相关 Agent Note 为准。
167
+
168
+ #### 未来:观测提供方状态
169
+
170
+ 没有提供方变更事件或能力状态查询;消费方只能通过执行调用并按抛出的 code 路由来观测可用性。如果消费方需要逐提供方原因,恢复一个小的观测接口是可行的,但已归档的简化笔记记录了为何放弃此前的那个接口。
171
+
172
+ </details>
package/lib/index.js ADDED
@@ -0,0 +1,163 @@
1
+ import { Service } from "@deepseek-ai/cordis";
2
+ import z from "@deepseek-ai/schemastery";
3
+ import { HarnessError } from "@x1a0f3n9/dsh-llm";
4
+ //#region lib/types/types.js
5
+ /**
6
+ * Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
7
+ * seam so provider selection, cancellation, errors, and product configuration have one owner,
8
+ * while retaining separate request and result types.
9
+ * @module @x1a0f3n9/dsh-web/types
10
+ */
11
+ /**
12
+ * Typed web error with a machine-routable, open-string `code` and chained `cause`.
13
+ * Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
14
+ * missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
15
+ * the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
16
+ * size and timeout limits, and unsupported content types. Tool execution exposes the code in
17
+ * structured error metadata.
18
+ */
19
+ var WebError = class extends HarnessError {};
20
+ //#endregion
21
+ //#region lib/types/index.js
22
+ /**
23
+ * Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
24
+ * fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
25
+ * be usable; without one, exactly one usable provider is required, so selection never depends
26
+ * on registration order.
27
+ * @module @x1a0f3n9/dsh-web
28
+ */
29
+ /**
30
+ * The web access service. Registered as `ctx.web` (one instance per context).
31
+ *
32
+ * Selection semantics (resolved at execution time, never order-dependent):
33
+ * - A configured id that is registered and `available()` → that provider.
34
+ * - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
35
+ * - A configured id registered but unavailable →
36
+ * `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
37
+ * - No id configured, an ordered provider is registered and usable → the first such provider.
38
+ * - No id configured, an order is set and none of those providers are usable →
39
+ * `WEB_PROVIDER_UNAVAILABLE`.
40
+ * - No id configured, no order set, and exactly one usable provider → that provider.
41
+ * - No id configured, no order set, and multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
42
+ * - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
43
+ */
44
+ var WebRuntime = class WebRuntime extends Service {
45
+ /**
46
+ * Provider selection config. Operational env overrides feed the SAME fields:
47
+ * `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
48
+ * `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
49
+ */
50
+ static Config = z.object({
51
+ searchProvider: z.string(),
52
+ searchProviderOrder: z.array(z.string()),
53
+ fetchProvider: z.string()
54
+ });
55
+ searchProviders = /* @__PURE__ */ new Map();
56
+ fetchProviders = /* @__PURE__ */ new Map();
57
+ currentConfig;
58
+ constructor(ctx, config = {}) {
59
+ super(ctx, "web");
60
+ this.currentConfig = () => config;
61
+ ctx.inject(["settings"], (settingsCtx) => {
62
+ settingsCtx.settings.installSection(ctx, "web", WebRuntime.Config, config, {
63
+ setSource: (source) => {
64
+ this.currentConfig = source;
65
+ },
66
+ onChange: () => {}
67
+ });
68
+ });
69
+ }
70
+ /**
71
+ * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
72
+ * if its id is already registered for search. Returns a disposer; disposed
73
+ * with the calling fiber.
74
+ * @param provider - the provider; its `id` is the registry key.
75
+ * @returns the disposer that unregisters the provider.
76
+ */
77
+ registerSearchProvider(provider) {
78
+ return this.registerProvider(this.searchProviders, provider);
79
+ }
80
+ /**
81
+ * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
82
+ * if its id is already registered for fetch. Returns a disposer; disposed
83
+ * with the calling fiber.
84
+ * @param provider - the provider; its `id` is the registry key.
85
+ * @returns the disposer that unregisters the provider.
86
+ */
87
+ registerFetchProvider(provider) {
88
+ return this.registerProvider(this.fetchProviders, provider);
89
+ }
90
+ registerProvider(store, provider) {
91
+ if (store.has(provider.id)) throw new WebError(`a web provider with id "${provider.id}" is already registered`, "WEB_DUPLICATE_PROVIDER");
92
+ const dispose = this.ctx.effect(function* () {
93
+ store.set(provider.id, provider);
94
+ yield () => store.delete(provider.id);
95
+ }, "web.registerProvider()");
96
+ return () => void dispose();
97
+ }
98
+ /**
99
+ * Run one search through the selected provider. Resolves the provider at call
100
+ * time with the selection rules above; throws {@link WebError} when the
101
+ * capability cannot run. The seam enforces `request.maxResults` on the result:
102
+ * if the provider over-returns, `sources[]` is truncated and `truncated` set.
103
+ * @param request - the query and optional result limit.
104
+ * @param signal - optional cancellation signal forwarded to the provider.
105
+ * @returns the provider's results, capped to `request.maxResults`.
106
+ */
107
+ async search(request, signal) {
108
+ const config = this.currentConfig();
109
+ const selection = { providers: this.searchProviders };
110
+ const configuredId = config.searchProvider ?? process.env.DSH_WEB_SEARCH_PROVIDER;
111
+ if (configuredId !== void 0) selection.configuredId = configuredId;
112
+ if (config.searchProviderOrder !== void 0) selection.preferredIds = config.searchProviderOrder;
113
+ return capSources(await resolveProvider(selection).search(request, signal), request.maxResults);
114
+ }
115
+ /**
116
+ * Retrieve one URL through the selected provider. Resolves the provider at
117
+ * call time with the selection rules above; throws {@link WebError} when the
118
+ * capability cannot run. A non-2xx response is a result, not a throw.
119
+ * @param request - the URL plus retrieval options.
120
+ * @param signal - optional cancellation signal forwarded to the provider.
121
+ * @returns the retrieval outcome; non-2xx responses resolve descriptively.
122
+ */
123
+ async fetch(request, signal) {
124
+ const config = this.currentConfig();
125
+ const selection = { providers: this.fetchProviders };
126
+ const configuredId = config.fetchProvider ?? process.env.DSH_WEB_FETCH_PROVIDER;
127
+ if (configuredId !== void 0) selection.configuredId = configuredId;
128
+ return resolveProvider(selection).fetch(request, signal);
129
+ }
130
+ };
131
+ /** Resolve the selected provider or throw the matching {@link WebError}. */
132
+ function resolveProvider(selection) {
133
+ const { configuredId, preferredIds, providers } = selection;
134
+ if (configuredId !== void 0) {
135
+ const provider = providers.get(configuredId);
136
+ if (!provider) throw new WebError(`configured web provider "${configuredId}" is not registered`, "WEB_PROVIDER_CONFIGURED_MISSING");
137
+ if (!provider.available()) throw new WebError(`configured web provider "${configuredId}" is registered but unavailable`, "WEB_PROVIDER_CONFIGURED_UNAVAILABLE");
138
+ return provider;
139
+ }
140
+ if (preferredIds !== void 0 && preferredIds.length > 0) {
141
+ for (const id of preferredIds) {
142
+ const provider = providers.get(id);
143
+ if (provider?.available()) return provider;
144
+ }
145
+ throw new WebError(`none of the ordered web providers are usable (${preferredIds.join(", ")})`, "WEB_PROVIDER_UNAVAILABLE");
146
+ }
147
+ const usable = [...providers.values()].filter((provider) => provider.available());
148
+ const [single] = usable;
149
+ if (single === void 0) throw new WebError("no usable web provider is registered", "WEB_PROVIDER_UNAVAILABLE");
150
+ if (usable.length > 1) throw new WebError(`multiple usable web providers are registered (${usable.map((provider) => provider.id).join(", ")}); configure one explicitly`, "WEB_PROVIDER_AMBIGUOUS");
151
+ return single;
152
+ }
153
+ /** Enforce `maxResults` on a search result: truncate `sources[]` and flag it. */
154
+ function capSources(result, maxResults) {
155
+ if (maxResults === void 0 || result.sources.length <= maxResults) return result;
156
+ return {
157
+ ...result,
158
+ sources: result.sources.slice(0, maxResults),
159
+ truncated: true
160
+ };
161
+ }
162
+ //#endregion
163
+ export { WebError, WebRuntime, WebRuntime as default };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Service Definition for the web access capability seam (`ctx.web`): registries and provider-selecting execution for search and
3
+ * fetch. Duplicate ids are rejected. At execution time, a configured provider must exist and
4
+ * be usable; without one, exactly one usable provider is required, so selection never depends
5
+ * on registration order.
6
+ * @module @x1a0f3n9/dsh-web
7
+ */
8
+ import { Context, Service } from '@deepseek-ai/cordis';
9
+ import z from '@deepseek-ai/schemastery';
10
+ import type { WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult } from './types.ts';
11
+ export { WebError, } from './types.ts';
12
+ export type { WebFetchBody, WebFetchProvider, WebFetchRequest, WebFetchResult, WebSearchProvider, WebSearchRequest, WebSearchResult, WebSearchSource, } from './types.ts';
13
+ declare module '@deepseek-ai/cordis' {
14
+ interface Context {
15
+ web: WebRuntime;
16
+ }
17
+ }
18
+ /**
19
+ * Config for the web seam. `searchProvider` / `fetchProvider` pin which provider
20
+ * wins for each capability. `searchProviderOrder` is an exclusive search allowlist
21
+ * when search is not pinned; all fields are optional (a single registered usable
22
+ * provider auto-selects). Operational overrides such as environment variables
23
+ * must feed these same fields rather than introduce a hidden priority chain.
24
+ */
25
+ export interface WebRuntimeConfig {
26
+ /** Explicit search provider id. Omitted = auto-select when exactly one usable. */
27
+ readonly searchProvider?: string;
28
+ /**
29
+ * Ordered search-provider ids used when no explicit provider is pinned.
30
+ * Unusable ids are skipped; if none of these are usable, search fails instead
31
+ * of selecting an unlisted provider such as `deepseek-official`.
32
+ */
33
+ readonly searchProviderOrder?: string[];
34
+ /** Explicit fetch provider id. Omitted = auto-select when exactly one usable. */
35
+ readonly fetchProvider?: string;
36
+ }
37
+ /**
38
+ * The web access service. Registered as `ctx.web` (one instance per context).
39
+ *
40
+ * Selection semantics (resolved at execution time, never order-dependent):
41
+ * - A configured id that is registered and `available()` → that provider.
42
+ * - A configured id not registered → `WEB_PROVIDER_CONFIGURED_MISSING`.
43
+ * - A configured id registered but unavailable →
44
+ * `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`.
45
+ * - No id configured, an ordered provider is registered and usable → the first such provider.
46
+ * - No id configured, an order is set and none of those providers are usable →
47
+ * `WEB_PROVIDER_UNAVAILABLE`.
48
+ * - No id configured, no order set, and exactly one usable provider → that provider.
49
+ * - No id configured, no order set, and multiple usable providers → `WEB_PROVIDER_AMBIGUOUS`.
50
+ * - No id configured, no usable provider → `WEB_PROVIDER_UNAVAILABLE`.
51
+ */
52
+ export declare class WebRuntime extends Service {
53
+ /**
54
+ * Provider selection config. Operational env overrides feed the SAME fields:
55
+ * `$DSH_WEB_SEARCH_PROVIDER` / `$DSH_WEB_FETCH_PROVIDER` are equivalent to
56
+ * `searchProvider` / `fetchProvider` and are NOT a hidden priority chain.
57
+ */
58
+ static Config: z<WebRuntimeConfig>;
59
+ private searchProviders;
60
+ private fetchProviders;
61
+ private currentConfig;
62
+ constructor(ctx: Context, config?: WebRuntimeConfig);
63
+ /**
64
+ * Register a search provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
65
+ * if its id is already registered for search. Returns a disposer; disposed
66
+ * with the calling fiber.
67
+ * @param provider - the provider; its `id` is the registry key.
68
+ * @returns the disposer that unregisters the provider.
69
+ */
70
+ registerSearchProvider(provider: WebSearchProvider): () => void;
71
+ /**
72
+ * Register a fetch provider. Throws {@link WebError} `WEB_DUPLICATE_PROVIDER`
73
+ * if its id is already registered for fetch. Returns a disposer; disposed
74
+ * with the calling fiber.
75
+ * @param provider - the provider; its `id` is the registry key.
76
+ * @returns the disposer that unregisters the provider.
77
+ */
78
+ registerFetchProvider(provider: WebFetchProvider): () => void;
79
+ private registerProvider;
80
+ /**
81
+ * Run one search through the selected provider. Resolves the provider at call
82
+ * time with the selection rules above; throws {@link WebError} when the
83
+ * capability cannot run. The seam enforces `request.maxResults` on the result:
84
+ * if the provider over-returns, `sources[]` is truncated and `truncated` set.
85
+ * @param request - the query and optional result limit.
86
+ * @param signal - optional cancellation signal forwarded to the provider.
87
+ * @returns the provider's results, capped to `request.maxResults`.
88
+ */
89
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
90
+ /**
91
+ * Retrieve one URL through the selected provider. Resolves the provider at
92
+ * call time with the selection rules above; throws {@link WebError} when the
93
+ * capability cannot run. A non-2xx response is a result, not a throw.
94
+ * @param request - the URL plus retrieval options.
95
+ * @param signal - optional cancellation signal forwarded to the provider.
96
+ * @returns the retrieval outcome; non-2xx responses resolve descriptively.
97
+ */
98
+ fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
99
+ }
100
+ export default WebRuntime;
101
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Vocabulary for the web capability seam (`ctx.web`). Search and fetch deliberately share one
3
+ * seam so provider selection, cancellation, errors, and product configuration have one owner,
4
+ * while retaining separate request and result types.
5
+ * @module @x1a0f3n9/dsh-web/types
6
+ */
7
+ import { HarnessError } from '@x1a0f3n9/dsh-llm';
8
+ /**
9
+ * What one search-capable backend is asked to search. Each request carries one
10
+ * query; a consumer may issue several requests. `maxResults` is a
11
+ * `dsh-tool-web`-layer bound passed through unchanged and enforced on the way
12
+ * back by the seam (see {@link WebSearchResult}).
13
+ */
14
+ export interface WebSearchRequest {
15
+ readonly query: string;
16
+ /**
17
+ * Upper bound on returned sources; the seam truncates to it. Omitted = no
18
+ * bound. `dsh-tool-web` always sets it. A provider whose API supports a
19
+ * result-count control (Exa's `numResults`) should apply it at the request
20
+ * layer as a cost/latency optimization; the seam enforces the bound
21
+ * regardless.
22
+ */
23
+ readonly maxResults?: number;
24
+ }
25
+ /**
26
+ * Normalized search outcome. `content` is optional provider-generated answer
27
+ * text or summary (Exa and DeepSeek return none; Perplexity returns a
28
+ * generated answer).
29
+ * `sources[]` is the portable citation shape. `truncated` is set by the seam
30
+ * when it cut `sources[]` down to `maxResults`.
31
+ */
32
+ export interface WebSearchResult {
33
+ /** Optional provider-generated answer text, search context, or summary. */
34
+ readonly content?: string;
35
+ /** Citeable sources, already truncated to the request's `maxResults`. */
36
+ readonly sources: readonly WebSearchSource[];
37
+ /** True when the seam dropped sources to honor `maxResults`. */
38
+ readonly truncated: boolean;
39
+ }
40
+ /**
41
+ * One citeable source. A source always has a URL; `title`, `snippet`, and
42
+ * `publishedAt` are optional because not every provider returns them — forcing
43
+ * adapters to invent them would make the seam lie (Perplexity citations may be
44
+ * URL-only). `dsh-tool-web` renders `title ?? hostname(url)` for display.
45
+ */
46
+ export interface WebSearchSource {
47
+ readonly url: string;
48
+ readonly title?: string;
49
+ readonly snippet?: string;
50
+ /** Publication/crawl timestamp as a provider-supplied ISO-8601 string. */
51
+ readonly publishedAt?: string;
52
+ }
53
+ /**
54
+ * What one fetch-capable backend is asked to retrieve. The request deliberately
55
+ * omits timeout, format, prompt, and extraction controls: cancellation is a
56
+ * direct execution argument, while presentation and higher-level LLM concerns
57
+ * belong outside safe retrieval.
58
+ */
59
+ export interface WebFetchRequest {
60
+ readonly url: string;
61
+ }
62
+ /**
63
+ * Normalized fetch outcome. A successful network fetch of a non-2xx response is
64
+ * a result, not an error: the status code is part of the fetched resource
65
+ * state. {@link WebError} is reserved for failures to safely retrieve or
66
+ * represent the resource.
67
+ */
68
+ export interface WebFetchResult {
69
+ /** The final URL after allowed redirects (the request URL is in the request). */
70
+ readonly url: string;
71
+ /** HTTP status code of the fetched response. */
72
+ readonly statusCode: number;
73
+ /** Decoded body, classified by content kind. */
74
+ readonly body: WebFetchBody;
75
+ /** True when the provider capped the decoded body. */
76
+ readonly truncated: boolean;
77
+ }
78
+ /**
79
+ * The decoded body of a fetched resource. A CLOSED discriminated union owned by
80
+ * `dsh-web`: the provider decodes the kind and `dsh-tool-web` renders it, so a
81
+ * new kind is a coordinated change across known packages, not a plugin
82
+ * extension. Consumers `switch` on `kind` ending in `default: assertNever(...)`
83
+ * so adding a kind breaks compilation at every consumer until handled. Each arm
84
+ * stays its own object literal even where fields coincide, so an arm can gain
85
+ * fields the others lack.
86
+ */
87
+ export type WebFetchBody = {
88
+ readonly kind: 'html';
89
+ readonly content: string;
90
+ } | {
91
+ readonly kind: 'text';
92
+ readonly content: string;
93
+ };
94
+ /**
95
+ * A search-capable backend. Registered with `ctx.web.registerSearchProvider`.
96
+ * `id` is a stable string, unique within the search capability kind.
97
+ */
98
+ export interface WebSearchProvider {
99
+ readonly id: string;
100
+ /** Cheap local usability check; must not make network calls. */
101
+ available(): boolean;
102
+ /** Run one search; honor `signal` for cancellation. */
103
+ search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>;
104
+ }
105
+ /**
106
+ * A fetch-capable backend. Registered with `ctx.web.registerFetchProvider`.
107
+ * `id` is a stable string, unique within the fetch capability kind.
108
+ */
109
+ export interface WebFetchProvider {
110
+ readonly id: string;
111
+ /** Cheap local usability check; must not make network calls. */
112
+ available(): boolean;
113
+ /** Retrieve one URL; honor `signal` for cancellation. */
114
+ fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>;
115
+ }
116
+ /**
117
+ * Typed web error with a machine-routable, open-string `code` and chained `cause`.
118
+ * Consumers must tolerate provider-specific codes. Shared codes cover unavailable,
119
+ * missing, unusable, ambiguous, or duplicate providers, cancellation, and provider failure;
120
+ * the local fetch provider additionally distinguishes invalid or blocked URLs, redirects,
121
+ * size and timeout limits, and unsupported content types. Tool execution exposes the code in
122
+ * structured error metadata.
123
+ */
124
+ export declare class WebError extends HarnessError {
125
+ }
126
+ //# sourceMappingURL=types.d.ts.map
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@x1a0f3n9/dsh-web",
3
+ "description": "Abstract web access capability seam (ctx.web) for the DeepSeek Harness — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy",
4
+ "version": "0.1.5-rc.2",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
11
+ "directory": "packages/web/web"
12
+ },
13
+ "type": "module",
14
+ "main": "lib/index.js",
15
+ "types": "lib/types/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./lib/types/index.d.ts",
19
+ "default": "./lib/index.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "files": [
25
+ "lib/index.js",
26
+ "lib/types/**/*.d.ts"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "@x1a0f3n9/dsh-llm": "^0.1.5-rc.2",
31
+ "@deepseek-ai/cordis": "^4.0.2",
32
+ "@x1a0f3n9/dsh-settings": "^0.1.5-rc.2"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.2"
36
+ },
37
+ "devDependencies": {
38
+ "@x1a0f3n9/dsh-llm": "^0.1.5-rc.2",
39
+ "@deepseek-ai/cordis": "^4.0.2",
40
+ "@x1a0f3n9/dsh-settings": "^0.1.5-rc.2"
41
+ }
42
+ }