@xneog/dsh-web 0.1.0 → 0.1.3-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/web/web/README.md
5
- README.md: 8dfc7f032e25e40207bb3880777b814e67175a20
6
- README.zh.md: 3354037bfcd77ca2b1a07da710b565065baf543b
5
+ README.md: 305509b3f4666611e25abe582a5dc59f43852f03
6
+ README.zh.md: db7a1e32ba3d4c72e55254bd0278c05c90fb657b
package/README.md CHANGED
@@ -1,53 +1,142 @@
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
+
1
6
  # @xneog/dsh-web
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- The **`WebRuntime`** (`ctx.web`) defines WHAT web access the harness has — search the web, fetch a URL — over multiple providers, without binding the model contract to one vendor's API shape.
10
+ ## Summary
6
11
 
7
- This package owns the Service Definition role of the web capability. Unlike shell/fs it spans two operations (search and fetch) on one seam, with potentially multiple providers each:
12
+ Any plugin or tool can search the web or fetch a URL through `dsh-web` (`ctx.web`) without binding to any vendor's API. Search and fetch providers plug in as backends, and the service picks one usable provider per operation, so callers never track which vendor runs behind a call. Choose it when building web tooling or another backend; the shipped model-facing tools (`dsh-tool-web`) mount it automatically. The service itself makes no network calls and registers no model-facing tool: a provider must be mounted before search or fetch can run. Search and fetch share one selection policy, one cancellation and error vocabulary, and one configuration surface, so "how this harness reaches the web" has a single owner.
8
13
 
9
- | Package | Role |
10
- |---|---|
11
- | `@xneog/dsh-web` (this) | Service Definition: the service, provider registries, selection policy, request/result vocabulary, the `WebError` taxonomy |
12
- | `@xneog/dsh-web-search-exa` | Search provider: Exa |
13
- | `@xneog/dsh-web-search-perplexity` | Search provider: Perplexity |
14
- | `@xneog/dsh-web-fetch-http` | Fetch provider: anonymous public HTTP(S) |
15
- | `@xneog/dsh-tool-web` | Consumer: the model-facing `web_search` / `web_fetch` tool schemas over `ctx.web` |
14
+ ## Table of Contents
16
15
 
17
- Search and fetch share no request schema and no business logic, but they are deliberately one seam: `ctx.web` is a single web-access middle layer with one provider-selection policy owner, one abort/error vocabulary, and one product-facing "how this harness reaches the web" config surface. The `Search`/`Fetch` method pairs are deliberately parallel.
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)
18
22
 
19
- ## Service API (`ctx.web`)
23
+ -----
20
24
 
21
- | Member | Semantics |
22
- |---|---|
23
- | `registerSearchProvider(provider)` / `registerFetchProvider(provider)` | Register a backend. Throws `WebError` `WEB_DUPLICATE_PROVIDER` on a duplicate id within that capability kind. Returns a disposer. Disposed with the calling fiber. |
24
- | `search(request, signal?)` | Resolve the search provider and run one search. Enforces `request.maxResults` on the result (truncates `sources[]`, sets `truncated`). Throws `WebError` when the capability cannot run. |
25
- | `fetch(request, signal?)` | Resolve the fetch provider and retrieve one URL. A non-2xx response is a result, not a throw. Throws `WebError` for failures to safely retrieve or represent the resource. |
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: '@xneog/dsh-web'
40
+ - name: '@xneog/dsh-web-search-exa'
41
+ - name: '@xneog/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
+ | `fetchProvider` | (unset) | Pinned fetch provider id; unset auto-selects when exactly one is usable |
48
+
49
+ The generated [configuration catalog](../../../docs/config-catalog.md#xneogdsh-web) is the exhaustive source for every accepted field and its JSDoc.
26
50
 
27
- Providers register **capabilities**, not tools. `dsh-tool-web` is the only owner of model-facing names, descriptions, prompt guidance, JSON schemas, and presentation.
51
+ ### Searching and fetching
28
52
 
29
- ## Selection
53
+ `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.
30
54
 
31
- Selection never depends on registration, config, or HMR order. A capability has an explicit provider id (config `searchProvider`/`fetchProvider`, or env `$DSH_WEB_SEARCH_PROVIDER`/`$DSH_WEB_FETCH_PROVIDER` feeding the same fields), or auto-selects when exactly one usable provider is registered. `search()`/`fetch()` resolve the provider at execution time:
55
+ ```text
56
+ // Search the web; sources[] is capped to maxResults:
57
+ const result = await ctx.web.search({ query: 'deepseek harness', maxResults: 8 })
32
58
 
33
- | Situation | Execution |
59
+ // Fetch one URL; a non-2xx response is a result, not an error:
60
+ const page = await ctx.web.fetch({ url: 'https://example.com' })
61
+ ```
62
+
63
+ 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.
64
+
65
+ ### Provider selection
66
+
67
+ 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:
68
+
69
+ | Situation | Outcome |
34
70
  |---|---|
35
- | configured id registered and `available()` | runs that provider |
71
+ | configured id registered and usable | runs that provider |
36
72
  | configured id not registered | `WEB_PROVIDER_CONFIGURED_MISSING` |
37
73
  | configured id registered but unavailable | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
38
74
  | no id, exactly one registered usable provider | runs it |
39
75
  | no id, no usable provider | `WEB_PROVIDER_UNAVAILABLE` |
40
76
  | no id, multiple usable providers | `WEB_PROVIDER_AMBIGUOUS` |
41
77
 
42
- The failure branches throw `WebError`, whose structured code (plus message detail — the missing id, the ambiguous candidate set) is the direct callers route on. A provider's own `available()` is a cheap local check (credential presence, parseable config) that feeds this execution-time selection and **must not make network calls**; `dsh-tool-web` never calls it — the tool executes through `ctx.web.search()`/`fetch()` and routes on the thrown codes, so provider selection has one owner.
78
+ 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.
79
+
80
+ ### Failures and recovery
81
+
82
+ 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.
83
+
84
+ -----
85
+
86
+ <a id="understand-the-implementation"></a>
87
+ ## Understand the implementation
88
+
89
+ <details>
90
+ <summary>Implementation internals — click to expand</summary>
91
+
92
+ This section explains the design decisions behind the service; the observable behavior is fully covered in [Use this package](#use-this-package).
93
+
94
+ ### Design philosophy
43
95
 
44
- ## Vocabulary
96
+ The package is built on one deliberate separation:
45
97
 
46
- `WebSearchRequest` (`query`, `maxResults?`) `WebSearchResult` (`content?`, `sources[]`, `truncated`); each `WebSearchSource` has a required `url` and optional `title`/`snippet`/`publishedAt` (Perplexity citations may be URL-only). `WebFetchRequest` (`url`) `WebFetchResult` (final `url`, `statusCode`, `body`, `truncated`); cancellation is a direct optional `AbortSignal` argument to `search()`/`fetch()`. `WebFetchBody` is a CLOSED discriminated union (`html` | `text`) owned here — consumers `switch` to exhaustiveness so a new kind breaks their compilation until handled. See `src/types.ts` for the full contracts and the `WebError` code taxonomy.
98
+ - **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.
99
+ - **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.
100
+ - **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.
47
101
 
102
+ ### Source map
103
+
104
+ | File | Role |
105
+ |---|---|
106
+ | [`src/index.ts`](src/index.ts) | Plugin entry: the `WebRuntime` service, both provider registries, and execution-time selection |
107
+ | [`src/types.ts`](src/types.ts) | Vocabulary: request/result types, the closed `WebFetchBody` union, and the `WebError` taxonomy |
108
+ | — | 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. |
109
+
110
+ ### Data model
111
+
112
+ 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.
113
+
114
+ ### Selection flow
115
+
116
+ 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.
117
+
118
+ </details>
119
+
120
+ -----
121
+
122
+ <a id="further-exploration"></a>
123
+ ## Further Exploration
124
+
125
+ 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.
126
+
127
+ - [Web subsystem](../../../docs/subsystems/web.md) — the exhaustive search/fetch requests and results, provider availability, and error codes.
128
+ - [Web package map](../README.md) — the six-package family and each role.
129
+ - [dsh-tool-web](../tool-web/README.md) — the model-facing `web_search` and `web_fetch` tools over this service.
130
+ - [dsh-web-fetch-http](../web-fetch-http/README.md) — the shipped anonymous HTTP(S) fetch backend.
131
+ - [Generated configuration catalog](../../../docs/config-catalog.md#xneogdsh-web) — every accepted config field and its source declaration.
132
+ - [Web capability seam decision](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md) — why search and fetch share one provider-selection service.
133
+
134
+ -----
135
+
136
+ <a id="model-experience"></a>
48
137
  ## Model Experience
49
138
 
50
- Indirectly, through `dsh-tool-web`, which retains bounded normalized provider data or the exact configured-provider, unavailable-provider, no-provider, multiple-provider, and `Error: <message>` failures while this registry contributes no prompt or schema itself.
139
+ 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.
51
140
 
52
141
  #### KV Cache effect
53
142
 
@@ -55,7 +144,26 @@ No direct invalidation; the named consumer owns any request-prefix changes.
55
144
 
56
145
  ## Known Limitations and Deferred Work
57
146
 
58
- - **No observation surface** — no provider-change event and no capability-status query; availability is observed only by executing `search()`/`fetch()` and routing the thrown `WebError` codes, 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)).
59
- - **`WebSearchRequest` carries only `query` + `maxResults`** — provider-neutral controls (recency, domain filters, regional hints, search depth) are deferred until Exa and Perplexity can both honor them honestly ([seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md)).
60
- - **`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 three web packages.
147
+ <a id="known-limitations-and-deferred-work"></a>
148
+
149
+
150
+ These limits define when the service is incomplete on its own. They are current package constraints.
151
+
152
+ - **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)).
153
+ - **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)).
154
+ - **`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.
61
155
  - **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.
156
+
157
+ <a id="dev-note"></a>
158
+ ### Dev Note
159
+
160
+ <details>
161
+ <summary>Working context for maintainers — click to expand</summary>
162
+
163
+ 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.
164
+
165
+ #### Future: observing provider state
166
+
167
+ 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.
168
+
169
+ </details>
package/README.zh.md CHANGED
@@ -1,61 +1,169 @@
1
+ ---
2
+ description: "web 访问服务(ctx.web):部署方与插件作者如何通过可互换的提供方搜索 web 与抓取 URL,以及统一的选择策略与错误词汇。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @xneog/dsh-web
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- **`WebRuntime`**(`ctx.web`)定义 harness 具备哪些 web 访问能力(搜索 web、抓取 URL),并通过多个提供方实现,不把模型约定绑定到某个厂商的 API 形状。
10
+ ## 概述
6
11
 
7
- 本包承担 web 能力的 Service Definition 角色。与 shell/fs 不同,它在一个 seam 上跨越搜索与抓取两种操作,每种操作都可能有多个提供方:
12
+ 任何插件或工具都可以通过 `dsh-web`(`ctx.web`)搜索 web 或抓取 URL,而无需绑定任何厂商的 API。搜索与抓取提供方以后端形式接入,服务按操作挑选一个可用的提供方,调用方无需追踪每次调用背后是哪家厂商。在构建 web 工具或其他后端时选择它;已交付的面向模型工具(`dsh-tool-web`)会自动挂载它。服务本身不发起网络调用、不注册面向模型的工具:搜索或抓取执行前必须已挂载提供方。搜索与抓取共用同一套选择策略、取消与错误词汇以及配置接口,因此「这个 harness 如何访问 web」只有一个归属方。
8
13
 
9
- | 包 | 职责 |
10
- |---|---|
11
- | `@xneog/dsh-web`(本包) | Service Definition:服务、提供方注册表、选择策略、请求/结果词汇、`WebError` 分类体系 |
12
- | `@xneog/dsh-web-search-exa` | 搜索提供方:Exa |
13
- | `@xneog/dsh-web-search-perplexity` | 搜索提供方:Perplexity |
14
- | `@xneog/dsh-web-fetch-http` | 抓取提供方:匿名公共 HTTP(S) |
15
- | `@xneog/dsh-tool-web` | Consumer:面向模型的 `web_search`/`web_fetch` 工具 schema,构建于 `ctx.web` 之上 |
14
+ ## 目录
16
15
 
17
- 搜索与抓取没有共享请求 schema 或业务逻辑,但有意共用一个 seam:`ctx.web` 是单一 web 访问中间层,拥有一项提供方选择策略、一套中止/错误词汇和一个面向产品的「该 harness 如何访问 web」配置接口。成对的 `Search`/`Fetch` 方法保持并行是有意为之。
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
18
22
 
19
- ## 服务 API(`ctx.web`)
23
+ -----
20
24
 
21
- | 成员 | 语义 |
22
- |---|---|
23
- | `registerSearchProvider(provider)`/`registerFetchProvider(provider)` | 注册后端。同一能力类型下 id 重复时抛出 `WebError` `WEB_DUPLICATE_PROVIDER`。返回 disposer。随调用 fiber 一并 dispose(资源释放)。 |
24
- | `search(request, signal?)` | 解析搜索提供方并运行一次搜索。在结果上强制执行 `request.maxResults`(截断 `sources[]`,设置 `truncated`)。能力无法运行时抛出 `WebError`。 |
25
- | `fetch(request, signal?)` | 解析抓取提供方并获取一个 URL。非 2xx 响应是结果,不会抛出异常。无法安全获取或表示资源时抛出 `WebError`。 |
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: '@xneog/dsh-web'
40
+ - name: '@xneog/dsh-web-search-exa'
41
+ - name: '@xneog/dsh-web-fetch-http'
42
+ ```
43
+
44
+ | 字段 | 默认值 | 含义 |
45
+ |---|---|---|
46
+ | `searchProvider` | (未设置) | 固定的搜索提供方 id;未设置时仅在恰好一个可用时自动选择 |
47
+ | `fetchProvider` | (未设置) | 固定的抓取提供方 id;未设置时仅在恰好一个可用时自动选择 |
48
+
49
+ 生成的[配置目录](../../../docs/config-catalog.zh.md#xneogdsh-web)是每个受支持字段及其 JSDoc 的穷尽式真源。
26
50
 
27
- 提供方注册的是**能力**而非工具。`dsh-tool-web` 是面向模型的名称、描述、提示词指引、JSON Schema 和呈现的唯一归属方。
51
+ ### 搜索与抓取
28
52
 
29
- ## 选择
53
+ `search()` 执行一次查询,返回可选的提供方答案与可引用的来源列表;服务强制执行 `request.maxResults`:截断 `sources[]` 并设置 `truncated`。`fetch()` 获取一个 URL,返回其最终 URL、状态码、解码后的正文与截断标志;非 2xx 响应是结果,不是错误。
30
54
 
31
- 选择绝不依赖注册、配置或 HMR(热模块替换)顺序。能力要么具有显式提供方 id(配置 `searchProvider`/`fetchProvider`,或由环境变量 `$DSH_WEB_SEARCH_PROVIDER`/`$DSH_WEB_FETCH_PROVIDER` 提供相同字段),要么在恰好只注册一个可用提供方时自动选择。`search()`/`fetch()` 会在执行时解析提供方:
55
+ ```text
56
+ // Search the web; sources[] is capped to maxResults:
57
+ const result = await ctx.web.search({ query: 'deepseek harness', maxResults: 8 })
32
58
 
33
- | 情况 | 执行 |
59
+ // Fetch one URL; a non-2xx response is a result, not an error:
60
+ const page = await ctx.web.fetch({ url: 'https://example.com' })
61
+ ```
62
+
63
+ 两个调用都接受可选的 `AbortSignal`,用于把取消转发给提供方。规范化的请求与结果形状是调用方赖以构建的约定;[web 子系统](../../../docs/subsystems/web.zh.md) 参考中的词汇章节对其有穷尽式描述。
64
+
65
+ ### 提供方选择
66
+
67
+ 每次调用都在执行时解析提供方,注册或加载顺序从不影响结果。已配置的提供方 id 在已注册且可用时优先;没有配置 id 时,服务运行唯一可用的提供方,或在情况不明时明确失败:
68
+
69
+ | 情况 | 结果 |
34
70
  |---|---|
35
- | 已配置 id 已注册且 `available()` | 运行该提供方 |
71
+ | 已配置 id 已注册且可用 | 运行该提供方 |
36
72
  | 已配置 id 未注册 | `WEB_PROVIDER_CONFIGURED_MISSING` |
37
73
  | 已配置 id 已注册但不可用 | `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` |
38
- | 无 id,恰好一个已注册的可用提供方 | 运行该提供方 |
74
+ | 无 id,恰好一个已注册的可用提供方 | 运行它 |
39
75
  | 无 id,没有可用提供方 | `WEB_PROVIDER_UNAVAILABLE` |
40
76
  | 无 id,多个可用提供方 | `WEB_PROVIDER_AMBIGUOUS` |
41
77
 
42
- 失败分支会抛出 `WebError`;调用方按其结构化 code(加消息细节:缺失 id、歧义候选集合)路由。提供方自身的 `available()` 是便宜的局部检查(凭据是否存在、配置是否可解析),供执行时选择使用,且**禁止发起网络调用**;`dsh-tool-web` 永远不会调用它。工具通过 `ctx.web.search()`/`fetch()` 执行,并按抛出的 code 路由,因此提供方选择只有一个归属方。
78
+ 提供方的可用性是一项廉价的局部检查——例如其 API 密钥是否存在——并且从不发起网络调用,因此选择保持快速且确定。
79
+
80
+ ### 失败与恢复
81
+
82
+ 失败抛出 `WebError`,携带稳定、可按机器路由的 code;消息补充细节,例如缺失的提供方 id 或歧义候选集合。调用方按 code 路由并决定如何降级。要改变一次调用使用的后端,请重新配置固定的 id、挂载或卸载提供方,或修正提供方配置使其可用性检查通过。
83
+
84
+ -----
85
+
86
+ <a id="understand-the-implementation"></a>
87
+ ## 理解实现
88
+
89
+ <details>
90
+ <summary>实现细节——点击展开</summary>
91
+
92
+ 本节解释服务背后的设计决策;可观察行为已在[使用本包](#use-this-package)中完整说明。
93
+
94
+ ### 设计理念
43
95
 
44
- ## 词汇
96
+ 本包建立在一个刻意的分离之上:
45
97
 
46
- `WebSearchRequest`(`query`、`maxResults?`)→ `WebSearchResult`(`content?`、`sources[]`、`truncated`);每个 `WebSearchSource` 都有必填 `url` 与可选 `title`/`snippet`/`publishedAt`(Perplexity 引用可能只含 URL)。`WebFetchRequest`(`url`)→ `WebFetchResult`(最终 `url`、`statusCode`、`body`、`truncated`);取消作为可选的直接 `AbortSignal` 参数传给 `search()`/`fetch()`。`WebFetchBody` 是这里拥有的封闭判别联合(`html` | `text`);消费方使用 `switch` 实现穷尽检查,因此新增类型会导致编译失败,直到处理完毕。完整约定见 `src/types.ts`,其中也包含 `WebError` code 分类体系。
98
+ - **一个 seam,两个独立操作。** 搜索与抓取没有共享请求 schema 或业务逻辑,但它们共用一个服务,使提供方选择、取消、错误与产品配置只有一个归属方。并行的 `Search`/`Fetch` 方法对是有意为之。
99
+ - **选择绝不依赖顺序。** 能力要么固定提供方 id,要么在恰好注册一个可用提供方时自动选择;`search()`/`fetch()` 在执行时解析提供方。
100
+ - **服务拥有结果上限。** `maxResults` 由 seam 在提供方返回后强制执行,因此超量返回的提供方绝不可能泄漏超出调用方要求的来源。
47
101
 
102
+ ### 源码地图
103
+
104
+ | 文件 | 职责 |
105
+ |---|---|
106
+ | [`src/index.ts`](src/index.ts) | 插件入口:`WebRuntime` 服务、两个提供方注册表与执行时选择 |
107
+ | [`src/types.ts`](src/types.ts) | 词汇:请求/结果类型、封闭的 `WebFetchBody` 联合与 `WebError` 分类体系 |
108
+ | — | 不发布运行时不变式伴生入口;约定在服务处强制执行。 |
109
+
110
+ ### 数据模型
111
+
112
+ 请求与结果类型定义了调用方赖以构建的规范化词汇——一组 `Search` 对与一组 `Fetch` 对——穷尽式字段与 JSDoc 见 [`src/types.ts`](src/types.ts) 与 [web 子系统](../../../docs/subsystems/web.zh.md) 参考。两个刻意的选择塑造了它们:`WebFetchBody` 是这里拥有的封闭联合(`html` | `text`),因此新增类型会破坏编译,直到每个消费方都处理它;`WebError` 继承 `HarnessError`,携带开放的字符串 `code`,因此消费方必须容忍提供方专有的取值。来源字段保持可选,因为并非每个提供方都返回全部字段。
113
+
114
+ ### 选择流程
115
+
116
+ 执行时,服务先按配置 id、再按唯一可用提供方解析提供方,没有明确赢家时抛出对应的 `WebError`。搜索结果随后经过 `capSources`:把 `sources[]` 截断到 `maxResults` 并标记 `truncated`。注册基于 effect:提供方随调用 fiber 注册,fiber 释放时注销;同一能力类型下重复的 id 会在注册时被拒绝。
117
+
118
+ </details>
119
+
120
+ -----
121
+
122
+ <a id="further-exploration"></a>
123
+ ## 进一步探索
124
+
125
+ 当包级约定不够用时阅读以下页面。它们从共享词汇逐步进入已交付后端、面向模型的工具与设计依据。
126
+
127
+ - [web 子系统](../../../docs/subsystems/web.zh.md)——穷尽式的搜索/抓取请求与结果、提供方可用性与错误码。
128
+ - [web 包映射](../README.zh.md)——六包家族与各角色。
129
+ - [dsh-tool-web](../tool-web/README.zh.md)——构建于本服务之上的面向模型 `web_search` 与 `web_fetch` 工具。
130
+ - [dsh-web-fetch-http](../web-fetch-http/README.zh.md)——已交付的匿名 HTTP(S) 抓取后端。
131
+ - [生成配置目录](../../../docs/config-catalog.zh.md#xneogdsh-web)——每个受支持配置字段及其源声明。
132
+ - [web 能力 seam 决策](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md)——搜索与抓取为何共用一项提供方选择服务。
133
+
134
+ -----
135
+
136
+ <a id="model-experience"></a>
48
137
  ## 模型体验
49
138
 
50
- 通过 `dsh-tool-web` 间接影响;该工具会保留有界的规范化提供方数据,或者原样保留以下失败:已配置的提供方缺失、提供方不可用、无提供方、存在多个提供方以及 `Error: <message>`;本注册表自身不贡献提示词或 schema。
139
+ 间接地,通过 `dsh-tool-web`:该工具把 seam 规范化的搜索结果与抓取正文渲染给模型,而本服务不贡献任何提示词或 schema。
51
140
 
52
141
  #### KV Cache 影响
53
142
 
54
143
  不会直接导致 KV Cache 失效;请求前缀变更由上述消费方负责。
55
144
 
56
- ## 已知限制与暂缓事项
145
+ ## 已知限制与延期工作
146
+
147
+ <a id="known-limitations-and-deferred-work"></a>
148
+
149
+
150
+ 这些限制说明本服务单独使用时在哪些方面不完整。它们是当前包约束。
151
+
152
+ - **没有观测接口**:没有提供方变更事件或能力状态查询;可用性只能通过执行搜索或抓取并按抛出的 code 路由来观测,无提供方失败是通用的 `WEB_PROVIDER_UNAVAILABLE`,不枚举逐提供方原因(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md))。
153
+ - **搜索请求只携带 `query` 与 `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至后端都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.zh.md))。
154
+ - **`WebFetchBody` 没有 `pdf` 分支**:可提取文本的 PDF 支持属于明确的延期工作;封闭联合会使新增该分支成为跨 web 包、由编译强制执行的变更。
155
+ - **提供方支持的页面提取不属于 `fetch()` 范围**:Firecrawl/Tavily 风格的 `web_extract` 能力延期,而不会扩展抓取操作。
156
+
157
+ <a id="dev-note"></a>
158
+ ### 开发备注
159
+
160
+ <details>
161
+ <summary>维护者的工作上下文——点击展开</summary>
162
+
163
+ 本开发备注是维护者的工作上下文:开放问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文和相关 Agent Note 为准。
164
+
165
+ #### 未来:观测提供方状态
166
+
167
+ 没有提供方变更事件或能力状态查询;消费方只能通过执行调用并按抛出的 code 路由来观测可用性。如果消费方需要逐提供方原因,恢复一个小的观测接口是可行的,但已归档的简化笔记记录了为何放弃此前的那个接口。
57
168
 
58
- - **没有观测接口**:没有提供方变更事件或能力状态查询;可用性只能通过执行 `search()`/`fetch()` 并按抛出的 `WebError` code 路由来观测,无提供方失败是通用的 `WEB_PROVIDER_UNAVAILABLE`,不会枚举逐提供方原因(见 [Agent Note](../../../.agents/notes/archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md))。
59
- - **`WebSearchRequest` 只携带 `query` + `maxResults`**:提供方无关的控制项(新近程度、域名过滤条件、区域提示、搜索深度)暂缓至 Exa 与 Perplexity 都能诚实支持时(见 [seam Agent Note](../../../.agents/notes/implemented/architecture/2026-06-24-web-capability-seam.md))。
60
- - **`WebFetchBody` 没有 `pdf` 分支**:可提取文本的 PDF 支持属于明确的暂缓工作;封闭联合会使新增该分支成为三个 web 包中由编译强制执行的变更。
61
- - **提供方支持的页面提取不属于 `fetch()` 范围**:Firecrawl/Tavily 风格的 `web_extract` 能力暂缓,而不会扩展抓取操作。
169
+ </details>
@@ -6,9 +6,10 @@
6
6
  */
7
7
  import { HarnessError } from '@xneog/dsh-llm';
8
8
  /**
9
- * What one search-capable backend can return. The model-facing argument is just
10
- * a query; `maxResults` is a `dsh-tool-web`-layer bound passed through unchanged
11
- * and enforced on the way back by the seam (see {@link WebSearchResult}).
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}).
12
13
  */
13
14
  export interface WebSearchRequest {
14
15
  readonly query: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@xneog/dsh-web",
3
3
  "description": "Abstract web access capability seam (ctx.web) for the xneog — search/fetch provider registry, registration-order-independent selection, request/result vocabulary, and the WebError taxonomy",
4
- "version": "0.1.0",
4
+ "version": "0.1.3-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -18,30 +18,23 @@
18
18
  "types": "./lib/types/index.d.ts",
19
19
  "default": "./lib/index.js"
20
20
  },
21
- "./invariant": {
22
- "types": "./lib/types/invariant.d.ts",
23
- "default": "./lib/invariant.js"
24
- },
25
21
  "./src/*": "./src/*",
26
22
  "./package.json": "./package.json"
27
23
  },
28
24
  "files": [
29
25
  "lib/index.js",
30
- "lib/invariant.js",
31
26
  "lib/types/**/*.d.ts"
32
27
  ],
33
28
  "license": "MIT",
34
29
  "peerDependencies": {
35
- "@xneog/dsh-invariants": "0.1.0",
36
- "@xneog/dsh-llm": "0.1.0",
37
- "@xneog/cordis": "0.1.0"
30
+ "@xneog/dsh-llm": "^0.1.3-alpha.1",
31
+ "@xneog/cordis": "^4.0.2"
38
32
  },
39
33
  "dependencies": {
40
- "@xneog/schemastery": "0.1.0"
34
+ "@xneog/schemastery": "^3.18.2"
41
35
  },
42
36
  "devDependencies": {
43
- "@xneog/dsh-invariants": "0.1.0",
44
- "@xneog/dsh-llm": "0.1.0",
45
- "@xneog/cordis": "0.1.0"
37
+ "@xneog/dsh-llm": "^0.1.3-alpha.1",
38
+ "@xneog/cordis": "^4.0.2"
46
39
  }
47
40
  }
package/lib/invariant.js DELETED
@@ -1,23 +0,0 @@
1
- //#region lib/types/invariant.js
2
- /**
3
- * Package-owned invariant companion for `@xneog/dsh-web`.
4
- * @module @xneog/dsh-web/invariant
5
- */
6
- const PACKAGE_NAME = "@xneog/dsh-web";
7
- /** Cordis companion plugin name. */
8
- const name = "web-invariant";
9
- /** Service required before the companion can reserve package ownership. */
10
- const inject = ["invariants"];
11
- /**
12
- * No runtime invariant: provider maps are private and selection/result caps are enforced on each
13
- * call; the seam publishes no independent registry or request/result observation stream.
14
- */
15
- const install = () => {};
16
- /**
17
- * Register this package's invariant companion.
18
- * @param ctx - Cordis context carrying the invariant service.
19
- * @returns the installed registration's disposer after setup succeeds.
20
- */
21
- const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
22
- //#endregion
23
- export { apply, inject, name };
@@ -1,16 +0,0 @@
1
- /**
2
- * Package-owned invariant companion for `@xneog/dsh-web`.
3
- * @module @xneog/dsh-web/invariant
4
- */
5
- import type { Context } from '@xneog/cordis';
6
- /** Cordis companion plugin name. */
7
- export declare const name = "web-invariant";
8
- /** Service required before the companion can reserve package ownership. */
9
- export declare const inject: string[];
10
- /**
11
- * Register this package's invariant companion.
12
- * @param ctx - Cordis context carrying the invariant service.
13
- * @returns the installed registration's disposer after setup succeeds.
14
- */
15
- export declare const apply: (ctx: Context) => Promise<() => void>;
16
- //# sourceMappingURL=invariant.d.ts.map