mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-11-09 20:07:43 +08:00
Merge origin/master into worktree/continue-generation-after-failure
This commit is contained in:
@@ -6,23 +6,23 @@
|
||||
en: ca120635fc40431c
|
||||
zh: 6be7c5ae3c86cdcc
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/problem:
|
||||
en: c8b932b02a4e1c80
|
||||
zh: 96c466be1a521f3f
|
||||
en: df1e82a74c42fa1d
|
||||
zh: 8d5452360a0dec8a
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/decision:
|
||||
en: 2e08d801099cbe18
|
||||
zh: 8c5ab162ada9cc41
|
||||
en: 7a4921d82cd11749
|
||||
zh: 6ab506109afc1351
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/package-topology:
|
||||
en: 5ea3ddfae82df181
|
||||
zh: 2d05d95e51081696
|
||||
en: 3394139c17d28ba8
|
||||
zh: 66857733ba51d98c
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/ctx-web-contract:
|
||||
en: b432f80a424c3e49
|
||||
zh: faa26925fdd98387
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/provider-availability-and-selection:
|
||||
en: 4e4ec068c2a22383
|
||||
zh: b7030d0f00bfdb74
|
||||
en: afc540d986e00d68
|
||||
zh: 84795770d00228c3
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/search-request-and-result-schema:
|
||||
en: ec3703b7e7c6b23d
|
||||
zh: 325755e6e90092da
|
||||
en: ccf6404ae7f3172e
|
||||
zh: 3253035ea49e2548
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/fetch-request-and-result-schema:
|
||||
en: 6dba3b9a51c28b6d
|
||||
zh: 58a93f6ba2d269ea
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 3440f2b34b93f4a1
|
||||
zh: 57a8f784c7e26eff
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/testing:
|
||||
en: 7f73699ac4459432
|
||||
zh: c16be49ac50c34c8
|
||||
en: 03da2f3422183384
|
||||
zh: 10fede51a415ce11
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered:
|
||||
en: 6119e2aff3b6be5a
|
||||
zh: 76136ef5249c23eb
|
||||
@@ -42,8 +42,8 @@
|
||||
en: e0d84a9b1b9c2123
|
||||
zh: 9e116a7b96751563
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/put-provider-dispatch-directly-in-dsh-tool-web:
|
||||
en: dc01d014d03b49d2
|
||||
zh: b956b02f41c62682
|
||||
en: 68ef45e8e5a36de9
|
||||
zh: 0013786ed77e03a8
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/alternatives-considered/split-search-and-fetch-into-two-seams-dsh-search-dsh-fetch:
|
||||
en: 3fb72ba3e554f20a
|
||||
zh: aff5f0add5773388
|
||||
@@ -63,11 +63,11 @@
|
||||
en: 3ea94ffb142d144a
|
||||
zh: 89645f68412f6769
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/consequences:
|
||||
en: 9774448fd6b21c41
|
||||
zh: 8411c590ad70d5c4
|
||||
en: 8e7523b2858efaf3
|
||||
zh: aff8fc74eb439ed6
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/deferred-work:
|
||||
en: 48b7b8e7310cd56b
|
||||
zh: d406cd6a8a649e58
|
||||
en: ddb599fd2ac1647c
|
||||
zh: c0b8f446cf483b3c
|
||||
/agent-note-web-capability-seam-stable-tools-over-multiple-providers/open-questions:
|
||||
en: 63848c6ef9a6ab44
|
||||
zh: 041b340802091e60
|
||||
|
||||
@@ -6,7 +6,7 @@ English | [中文](2026-06-24-web-capability-seam.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The harness needs model-facing web tools without binding the model contract to one vendor's API shape. Search is the immediate pressure point: supporting both Exa search and Perplexity search from the start — two deliberately different provider shapes (Exa returns a flat `results[]` of `{title, url, highlights, publishedDate}`; Perplexity returns a generated answer plus citations) — is what proves the normalized web contract does not just mirror one vendor. Fetch is a separate operation: an anonymous public HTTP(S) fetch backend has transport, security, redirect, decoding, and size-limit concerns that are not the same as provider-backed search.
|
||||
The harness needs model-facing web tools without binding the model contract to one vendor's API shape. Search is the immediate pressure point: search APIs return deliberately different formats (Exa returns a flat `results[]` of `{title, url, highlights, publishedDate}`, Perplexity returns a generated answer plus citations, and the DeepSeek Search API returns a flat `results[]` with snippets and summaries), and the normalized web contract must accept each of them without mirroring one vendor. The seam was designed against the Exa and Perplexity formats; neither provider ships, and a plugin can register one. Fetch is a separate operation: an anonymous public HTTP(S) fetch backend has transport, security, redirect, decoding, and size-limit concerns that are not the same as provider-backed search.
|
||||
|
||||
The model-facing API must stay stable while backends change. A search provider swap should not change how the model asks for a query, and a fetch implementation swap should not change how the model asks for a URL. Conversely, a provider package should not expose its own model-facing tool schema just because it has extra provider-specific knobs.
|
||||
|
||||
@@ -19,7 +19,7 @@ There is also a provider-selection question. Existing `tool-bash` and `tool-fs`
|
||||
Web access is a first-class capability seam following [the capability-seam Agent Note](2026-06-13-capability-seams.md):
|
||||
|
||||
1. `@deepseek-ai/dsh-web` (`packages/web/web`) owns `ctx.web`, provider registration, provider selection, shared request/result vocabulary, and web-specific errors.
|
||||
2. Provider packages implement concrete backends and register capabilities with `ctx.web`, for example `@deepseek-ai/dsh-web-search-exa`, `@deepseek-ai/dsh-web-search-perplexity`, `@deepseek-ai/dsh-web-search-deepseek`, and `@deepseek-ai/dsh-web-fetch-http`.
|
||||
2. Provider packages implement concrete backends and register capabilities with `ctx.web`, for example `@deepseek-ai/dsh-web-search-deepseek`, `@deepseek-ai/dsh-web-deepseek-api`, and `@deepseek-ai/dsh-web-fetch-http`.
|
||||
3. `@deepseek-ai/dsh-tool-web` (`packages/web/tool-web`) owns the model-facing `web_search` and `web_fetch` tool schemas, prompt sections, argument validation, result formatting, and tool-owned presentation over `ctx.web`.
|
||||
|
||||
Providers do not register tools. Providers register capabilities. `dsh-tool-web` is the only owner of model-facing names, descriptions, prompt guidance, JSON schemas, and presentation.
|
||||
@@ -32,9 +32,9 @@ Search and fetch are separate tools but one web-access seam. `ctx.web` owns prov
|
||||
- A tool is never unregistered merely because its selected provider is missing, misconfigured, missing credentials, ambiguous, or temporarily unavailable.
|
||||
- The provider is resolved at execution time, and a structured `WebError` is returned when the selected capability cannot run.
|
||||
|
||||
This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. `dsh-tool-web` narrows this per model request: it [withholds `web_search` while the selected provider reports that the request's search cannot authenticate](2026-09-27-web-search-offered-by-provider-readiness.md). If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
|
||||
This keeps the model schema stable without making plugin load order, credential state, or HMR timing part of the model-facing contract. If web search is enabled but no usable search provider exists, `web_search` remains visible and execution fails with a structured `WebError` such as `WEB_PROVIDER_UNAVAILABLE` or `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`. `dsh-tool-web` narrows this per model request: it [withholds `web_search` or `web_fetch` while the selected provider reports that the request's operation cannot authenticate](2026-09-27-web-search-offered-by-provider-readiness.md). If a provider appears after `dsh-tool-web`, the next execution can use it without changing the schema. If a provider disappears mid-call, execution fails with a structured `WebError` instead of silently choosing another provider or falling through to `UNKNOWN_TOOL`.
|
||||
|
||||
The seam deliberately exposes no observation surface — no registry-change event and no aggregated capability-status query. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes.
|
||||
The seam deliberately exposes no availability observation — no aggregated capability-status query and no per-provider availability report. Unavailability is a fact a caller observes by executing: `search()`/`fetch()` resolve the provider at call time and throw the structured `WebError` that names what failed. [The observation-surface Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) records that judgment: derived-on-call selection and enablement-based registration leave no consumer that needs a change signal or an availability probe distinct from executing and routing the error, and a future provider-status panel reintroduces the smallest signal or query it actually consumes. Provider pickers consume registration, not availability: `listProviders()` and the Remote method `web/listProviders` list every registered provider with its optional `title` and `description` (including unavailable providers), and the payload-free `web/providers-changed` event fires after each registry mutation. The [web provider selection decision](2026-10-29-web-provider-selection.md) records why registration is observable while availability is not.
|
||||
|
||||
## Package topology
|
||||
|
||||
@@ -43,11 +43,9 @@ The three-package Service Definition / Service Provider / Consumer split follows
|
||||
The dependency direction mirrors bash and filesystem:
|
||||
|
||||
```text
|
||||
@deepseek-ai/dsh-tool-web --depends on--> @deepseek-ai/dsh-web <--depends on-- @deepseek-ai/dsh-web-search-exa
|
||||
@deepseek-ai/dsh-tool-web --depends on--> @deepseek-ai/dsh-web <--depends on-- @deepseek-ai/dsh-web-search-deepseek
|
||||
consumer interface implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-search-perplexity
|
||||
implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-search-deepseek
|
||||
<--depends on-- @deepseek-ai/dsh-web-deepseek-api
|
||||
implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-fetch-http
|
||||
implementation
|
||||
@@ -57,9 +55,7 @@ At runtime, provider packages register capabilities with `ctx.web`; `tool-web` r
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
|
||||
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
|
||||
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
|
||||
@@ -67,9 +63,9 @@ flowchart LR
|
||||
toolWeb -->|ctx.tools.register| webFetch["tool: web_fetch"]
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-web` depends only on Cordis and low-level harness support. It declares `ctx.web`, provider interfaces, request/result types, the provider availability contract, and error codes. It does not import tool, agent, session, LLM, or provider packages.
|
||||
`@deepseek-ai/dsh-web` depends only on Cordis and low-level harness support, including the Typert protocol for its Remote method. It declares `ctx.web`, provider interfaces, request/result types, the provider availability contract, and error codes. It does not import tool, agent, session, LLM, or provider packages.
|
||||
|
||||
Provider packages depend only on `dsh-web` and Cordis. They own credentials, endpoints, wire mapping, parsing, and `WebError` translation, using platform `fetch`. Each provider injects the shared service and registers a backend; only `dsh-web` owns the `ctx.web` key. Provider-private protocol shapes do not create dependencies on `ctx.llm` or a Cordis HTTP service.
|
||||
Provider packages depend on `dsh-web`, Cordis, and their credential/configuration helpers. They own credentials, endpoints, wire mapping, parsing, and `WebError` translation, using platform `fetch`. Each provider injects the shared service and registers a backend; only `dsh-web` owns the `ctx.web` key. Provider-private protocol shapes do not create dependencies on `ctx.llm` or a Cordis HTTP service.
|
||||
|
||||
`@deepseek-ai/dsh-tool-web` depends on `@deepseek-ai/dsh-web`, `@deepseek-ai/dsh-tools`, `@deepseek-ai/dsh-system-prompt`, and Cordis. It never imports concrete provider packages.
|
||||
|
||||
@@ -78,16 +74,22 @@ Provider packages depend only on `dsh-web` and Cordis. They own credentials, end
|
||||
`ctx.web` is a provider registry plus a provider-selecting execution API. The registry half stays close to `LlmRuntime`: a `Map<id, provider>` per capability kind, `registerSearchProvider` / `registerFetchProvider` methods that return disposers, duplicate ids that throw `WebError`, and execution-time resolution that throws when the selected provider is absent or unusable. The authoritative signatures live in `packages/web/web/src/types.ts`; the seam's shape:
|
||||
|
||||
```ts
|
||||
import type { WebFetchRequest, WebFetchResult, WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web'
|
||||
import type {
|
||||
WebFetchRequest, WebFetchResult, WebProviderInfo, WebProviderList, WebSearchRequest, WebSearchResult,
|
||||
} from '@deepseek-ai/dsh-web'
|
||||
|
||||
interface WebSearchProvider {
|
||||
readonly id: string
|
||||
readonly title?: string
|
||||
readonly description?: WebProviderInfo['description']
|
||||
available(): boolean
|
||||
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
|
||||
}
|
||||
|
||||
interface WebFetchProvider {
|
||||
readonly id: string
|
||||
readonly title?: string
|
||||
readonly description?: WebProviderInfo['description']
|
||||
available(): boolean
|
||||
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
|
||||
}
|
||||
@@ -95,6 +97,7 @@ interface WebFetchProvider {
|
||||
interface WebRuntime {
|
||||
registerSearchProvider(provider: WebSearchProvider): () => void
|
||||
registerFetchProvider(provider: WebFetchProvider): () => void
|
||||
listProviders(): WebProviderList
|
||||
|
||||
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
|
||||
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
|
||||
@@ -131,18 +134,15 @@ The "single provider auto-selects" rule is for tests, demos, and simple deployme
|
||||
- id: web
|
||||
name: '@deepseek-ai/dsh-web'
|
||||
config:
|
||||
searchProvider: exa
|
||||
searchProvider: deepseek-search-api
|
||||
fetchProvider: http
|
||||
|
||||
- id: web-search-exa
|
||||
name: '@deepseek-ai/dsh-web-search-exa'
|
||||
|
||||
- id: web-search-perplexity
|
||||
name: '@deepseek-ai/dsh-web-search-perplexity'
|
||||
|
||||
- id: web-search-deepseek
|
||||
name: '@deepseek-ai/dsh-web-search-deepseek'
|
||||
|
||||
- id: web-deepseek-api
|
||||
name: '@deepseek-ai/dsh-web-deepseek-api'
|
||||
|
||||
- id: web-fetch-http
|
||||
name: '@deepseek-ai/dsh-web-fetch-http'
|
||||
|
||||
@@ -150,9 +150,9 @@ The "single provider auto-selects" rule is for tests, demos, and simple deployme
|
||||
name: '@deepseek-ai/dsh-tool-web'
|
||||
```
|
||||
|
||||
Operational overrides feed the same explicit selection path: `DSH_WEB_SEARCH_PROVIDER=perplexity` is equivalent to config `searchProvider: perplexity`, not a hidden priority chain inside `dsh-tool-web`.
|
||||
Operational overrides feed the same explicit selection path: `DSH_WEB_SEARCH_PROVIDER=deepseek-search-api` is equivalent to config `searchProvider: deepseek-search-api`, not a hidden priority chain inside `dsh-tool-web`.
|
||||
|
||||
Each search or fetch reads the configured id when it runs, so a saved settings change selects another provider for the next operation without remounting `dsh-web`. The Web search settings switch relies on this to move both capabilities to the [DeepSeek Search and Fetch APIs](../feature/2026-10-15-deepseek-search-fetch-api.md) at once.
|
||||
Each search or fetch reads the configured id when it runs, so a saved settings change selects another provider for the next operation without remounting `dsh-web`. The Search and Fetch menus on the Web search and fetch settings page rely on this to switch each capability's provider independently, including the [DeepSeek Search and Fetch APIs](../feature/2026-10-15-deepseek-search-fetch-api.md); the [web provider selection decision](2026-10-29-web-provider-selection.md) owns the shipped defaults.
|
||||
|
||||
`ctx.web.search()` and `ctx.web.fetch()` resolve the provider at execution time using the selection rules above. If the selected capability is unavailable, they throw `WebError` with a structured code such as `WEB_PROVIDER_UNAVAILABLE`, `WEB_PROVIDER_CONFIGURED_MISSING`, `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`, or `WEB_PROVIDER_AMBIGUOUS`. If no provider is explicitly configured and no usable provider exists, the execution error is the generic `WEB_PROVIDER_UNAVAILABLE` case; there is deliberately no diagnostic summary of every unavailable provider.
|
||||
|
||||
@@ -168,8 +168,8 @@ The `web_search` model-facing tool is small. The only model-facing argument is:
|
||||
|
||||
- `dsh-tool-web` owns the value and puts it on `WebSearchRequest.maxResults`.
|
||||
- `ctx.web` passes the request through to the selected provider unchanged.
|
||||
- A provider applies `maxResults` at the request layer when its API supports it (Exa's `numResults`), as a cost/latency optimization.
|
||||
- `ctx.web` enforces the bound on the result: if a provider returns more than `maxResults` sources — because its API has no result-count control (Perplexity) or ignored the hint — the seam truncates `sources[]` to `maxResults` and sets `WebSearchResult.truncated` to `true` before returning. This makes the bound a single cross-provider guarantee the model-facing layer can rely on, rather than something each provider must remember to honor.
|
||||
- A provider applies `maxResults` at the request layer when its API supports it (the DeepSeek Search API's `count`), as a cost/latency optimization.
|
||||
- `ctx.web` enforces the bound on the result: if a provider returns more than `maxResults` sources — because its API has no result-count control or ignored the hint — the seam truncates `sources[]` to `maxResults` and sets `WebSearchResult.truncated` to `true` before returning. This makes the bound a single cross-provider guarantee the model-facing layer can rely on, rather than something each provider must remember to honor.
|
||||
|
||||
The seam request carries no provider-specific controls — no Perplexity model selection, search recency, domain filters, Exa `livecrawl`, Exa `type`, regional hints, generated-answer budgets, or search depth. Such a field is added only when it has provider-neutral semantics that both the tool schema and selected providers can honor honestly.
|
||||
|
||||
@@ -190,13 +190,14 @@ interface WebSearchSource {
|
||||
readonly url: string
|
||||
readonly title?: string
|
||||
readonly snippet?: string
|
||||
readonly summary?: string
|
||||
readonly publishedAt?: string
|
||||
}
|
||||
```
|
||||
|
||||
`content` is optional provider-generated answer text, search context, or summary. `sources[]` is the portable citation shape. A source always has a URL; title, snippet, and `publishedAt` are optional because not every provider returns them. `title` is not required: Perplexity-style citations may provide only URLs, and forcing adapters to invent titles would make the seam lie. `dsh-tool-web` renders a `title ?? hostname(url)`-style fallback label for display. `publishedAt` is an optional publication/crawl timestamp as an ISO-8601 string — Exa returns it as `publishedDate` on each result and Perplexity returns a `date` on search results, so it is real provider data, not derived; the seam carries it as a string and leaves date parsing to the consumer.
|
||||
`content` is optional provider-generated answer text, search context, or summary. `sources[]` is the portable citation shape. A source always has a URL; title, snippet, summary, and `publishedAt` are optional because not every provider returns them. `snippet` is a short excerpt for display, and `summary` is a longer excerpt for model reading whose length the consumer bounds. `title` is not required: Perplexity-style citations may provide only URLs, and forcing adapters to invent titles would make the seam lie. `dsh-tool-web` renders a `title ?? hostname(url)`-style fallback label for display. `publishedAt` is an optional publication/crawl timestamp as an ISO-8601 string — the DeepSeek Search API returns it as a nullable `date` on each result, Exa as `publishedDate`, and Perplexity as `date`, so it is real provider data, not derived; the seam carries it as a string and leaves date parsing to the consumer.
|
||||
|
||||
Exa search maps each entry of the provider's flat `results[]` into a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← the first `highlights[]` entry (an entry with no highlight has no portable snippet and is dropped), `publishedAt` ← `publishedDate`. Exa returns no provider-generated answer, so `content` is omitted. Perplexity search maps `choices[0].message.content` to `content` and prefers the structured top-level `search_results[]` for `sources[]` — `url` ← `url`, `title` ← `title`, `snippet` ← `snippet` (often empty), `publishedAt` ← `date` — falling back to the URL-only `citations[]` array only when `search_results` is absent (those sources carry just a `url`). If a provider returns fewer structured fields than the seam supports, the adapter omits those optional fields.
|
||||
The DeepSeek Search API provider maps each entry of the response's `results[]` into a `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← `snippet`, `summary` ← `summary`, `publishedAt` ← `date`, omitting empty values. It returns no provider-generated answer, so `content` is omitted. If a provider returns fewer structured fields than the seam supports, the adapter omits those optional fields.
|
||||
|
||||
Full page retrieval remains the job of `web_fetch(url)`. Search snippets are discovery context, not fetched page bodies.
|
||||
|
||||
@@ -283,7 +284,7 @@ Tool execution lets these errors flow through `ToolRuntime.execute()`, which alr
|
||||
|
||||
## Testing
|
||||
|
||||
Each layer is pinned at its own boundary: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures (Perplexity fixtures include URL-only citations so the optional source fields stay honest) plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-http`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`.
|
||||
Each layer is pinned at its own boundary: the registry/selection/truncation/abort contract and the `WebError` codes in `dsh-web`; per-provider request/response mapping over recorded fixtures plus a self-skipping with-key smoke per real provider; real local-HTTP behavior in `web-fetch-http`; and enablement-driven registration, structured execution errors, and result formatting through the real tool registry in `dsh-tool-web`. A real-Loader smoke guards the two export shapes ([postmortem 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.md)): `dsh-web` is a default-exported service, while the providers and `tool-web` are namespace plugins where a stray `export default` would drop `inject`.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -293,7 +294,7 @@ This matches the most flexible provider-plugin systems: every provider can expos
|
||||
|
||||
### Put provider dispatch directly in `dsh-tool-web`
|
||||
|
||||
This resembles OpenCode's local web search: one stable `websearch` tool dispatches to Exa or Parallel internally. It is acceptable for a small product path but wrong as a harness foundation. The tool package would own provider selection, credentials, request mapping, transport, response parsing, and presentation, making it hard to add Exa and Perplexity without baking their differences into the tool schema.
|
||||
This resembles OpenCode's local web search: one stable `websearch` tool dispatches to Exa or Parallel internally. It is acceptable for a small product path but wrong as a harness foundation. The tool package would own provider selection, credentials, request mapping, transport, response parsing, and presentation, making it hard to add providers with different formats without baking their differences into the tool schema.
|
||||
|
||||
### Split search and fetch into two seams (`dsh-search`, `dsh-fetch`)
|
||||
|
||||
@@ -321,9 +322,9 @@ Rejected for the shipped presets. Per-call confirmation would interrupt ordinary
|
||||
|
||||
## Consequences
|
||||
|
||||
**The search schema is deliberately thin.** Exa and Perplexity both expose useful provider-specific controls; a control is added only once it can be defined provider-neutrally and enforced honestly by both tool registration and provider execution.
|
||||
**The search schema is deliberately thin.** Search APIs expose useful provider-specific controls, such as Exa's `livecrawl`, Perplexity's search recency, and the DeepSeek Search API's `localization`; a control is added only once it can be defined provider-neutrally and enforced honestly by both tool registration and provider execution.
|
||||
|
||||
**Perplexity citations can be sparse.** A citation may be only a URL. Making `title` and `snippet` optional keeps the seam truthful but means `tool-web` renders fallback labels.
|
||||
**Citations can be sparse.** A Perplexity-style citation may be only a URL. Making `title` and `snippet` optional keeps the seam truthful but means `tool-web` renders fallback labels.
|
||||
|
||||
**Stable tool registration defers misconfiguration to execution.** Keeping the tool visible is correct when the product enabled web access, but product apps that expect web search should surface the structured `WEB_PROVIDER_CONFIGURED_MISSING` / `WEB_PROVIDER_CONFIGURED_UNAVAILABLE` / `WEB_PROVIDER_AMBIGUOUS` failures loudly so users do not discover setup problems only after the model calls the tool.
|
||||
|
||||
@@ -337,7 +338,7 @@ Rejected for the shipped presets. Per-call confirmation would interrupt ordinary
|
||||
|
||||
- A `pdf` `WebFetchBody` kind: the `http` provider decodes text-extractable PDFs (best-effort, capped, `truncated`) into a `{ kind: 'pdf'; content; pageCount? }` arm, and `tool-web` renders it. This is fetch, not `web_extract` — PDF retrieval is a concrete HTTP 200 plus deterministic local decoding, not provider-side extraction of a non-HTTP resource. Adding it is a coordinated change across `dsh-web` (declare the arm), the provider (decode + narrow "binary rejection" to "reject binary except text-extractable PDF"; scanned/image PDFs needing OCR stay out of scope), and `tool-web` (render). The closed `WebFetchBody` union makes the consumer side fail to compile until the new arm is handled.
|
||||
- Provider-backed extraction as a separate `web_extract` capability, rather than widening `web_fetch` silently.
|
||||
- Provider-neutral search controls beyond `query` and `maxResults`, once Exa and Perplexity can both honor them honestly.
|
||||
- Provider-neutral search controls beyond `query` and `maxResults`, once the registered providers can honor them honestly.
|
||||
|
||||
## Open questions
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Status: implemented
|
||||
|
||||
## 问题
|
||||
|
||||
harness 需要面向模型的 web 工具,但不能将模型约定绑定到某一家厂商的 API 形状上。搜索是当前的压力点:从一开始就同时支持 Exa 搜索和 Perplexity 搜索——两种刻意不同的提供方形状(Exa 返回扁平的 `results[]`,每项包含 `{title, url, highlights, publishedDate}`;Perplexity 返回一段生成式回答加引用列表)——正是用来证明归一化的 web 约定并非只是镜像某一家厂商。Fetch 是另一项独立操作:匿名公开 HTTP(S) fetch 后端涉及传输、安全、重定向、解码和大小限制等关注点,与提供方支撑的搜索并不相同。
|
||||
harness 需要面向模型的 web 工具,但不能将模型约定绑定到某一家厂商的 API 形状上。搜索是当前的压力点:各搜索 API 返回刻意不同的格式(Exa 返回扁平的 `results[]`,每项包含 `{title, url, highlights, publishedDate}`;Perplexity 返回一段生成式回答加引用列表;DeepSeek Search API 返回带 snippet 与摘要的扁平 `results[]`),归一化的 web 约定必须能接纳每一种格式,而不是镜像某一家厂商。seam 是以 Exa 和 Perplexity 的格式为参照设计的;这两个提供方都不随附,插件可以注册它们。Fetch 是另一项独立操作:匿名公开 HTTP(S) fetch 后端涉及传输、安全、重定向、解码和大小限制等关注点,与提供方支撑的搜索并不相同。
|
||||
|
||||
面向模型的 API 必须保持稳定,而后端可以更换。更换搜索提供方不应改变模型发起查询的方式;更换 fetch 实现不应改变模型请求 URL 的方式。反过来,提供方包也不应仅仅因为自己有额外的提供方特有旋钮就暴露自己的面向模型工具 schema。
|
||||
|
||||
@@ -19,7 +19,7 @@ harness 需要面向模型的 web 工具,但不能将模型约定绑定到某
|
||||
Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13-capability-seams.zh.md):
|
||||
|
||||
1. `@deepseek-ai/dsh-web`(`packages/web/web`)拥有 `ctx.web`、提供方注册、提供方选择、共享的请求/结果词汇,以及 web 特有的错误。
|
||||
2. 提供方包实现具体后端并向 `ctx.web` 注册能力,例如 `@deepseek-ai/dsh-web-search-exa`、`@deepseek-ai/dsh-web-search-perplexity`、`@deepseek-ai/dsh-web-search-deepseek` 和 `@deepseek-ai/dsh-web-fetch-http`。
|
||||
2. 提供方包实现具体后端并向 `ctx.web` 注册能力,例如 `@deepseek-ai/dsh-web-search-deepseek`、`@deepseek-ai/dsh-web-deepseek-api` 和 `@deepseek-ai/dsh-web-fetch-http`。
|
||||
3. `@deepseek-ai/dsh-tool-web`(`packages/web/tool-web`)拥有面向模型的 `web_search` 和 `web_fetch` 工具 schema、提示词段落、参数校验、结果格式化,以及通过 `ctx.web` 实现的工具展示。
|
||||
|
||||
提供方不注册工具。提供方注册能力。`dsh-tool-web` 是面向模型的名称、描述、提示词引导、JSON Schema、展示的唯一所有者。
|
||||
@@ -32,9 +32,9 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
|
||||
- 工具绝不会仅仅因为其选定的提供方缺失、配置错误、缺少凭证、存在歧义或暂时不可用就被注销。
|
||||
- 提供方在执行时解析,当选定的能力无法运行时返回结构化的 `WebError`。
|
||||
|
||||
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。发行版的 `dsh-tool-web` 按模型请求收窄了这一点:选中的提供方报告该请求的搜索无法鉴权时,它会[撤下 `web_search`](2026-09-27-web-search-offered-by-provider-readiness.zh.md)。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
|
||||
这使模型 schema 保持稳定,而不将插件加载顺序、凭证状态或 HMR(热模块替换)时序纳入面向模型的约定。如果 web 搜索已启用但不存在可用的搜索提供方,`web_search` 仍然可见,执行时以结构化的 `WebError`(如 `WEB_PROVIDER_UNAVAILABLE` 或 `WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)失败。`dsh-tool-web` 按模型请求收窄了这一点:选中的提供方报告该请求的操作无法鉴权时,它会[撤下 `web_search` 或 `web_fetch`](2026-09-27-web-search-offered-by-provider-readiness.zh.md)。如果某个提供方在 `dsh-tool-web` 之后出现,下一次执行即可使用它而无需更改 schema。如果某个提供方在调用过程中消失,执行以结构化的 `WebError` 失败,而不是静默选择另一个提供方或回退到 `UNKNOWN_TOOL`。
|
||||
|
||||
该 seam 刻意不暴露任何观察面——没有注册表变更事件,也没有聚合的能力状态查询。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。
|
||||
该 seam 刻意不暴露可用性观察——没有聚合的能力状态查询,也没有逐个提供方的可用性报告。不可用性是调用方通过执行观察到的事实:`search()`/`fetch()` 在调用时解析提供方,并抛出命名了失败原因的结构化 `WebError`。[观察面 Agent Note](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) 记录了这一判断:基于调用的派生选择与基于启用的注册使得没有消费方需要变更信号或独立于执行和错误路由的可用性探测;未来的提供方状态面板会重新引入它实际消费的最小信号或查询。提供方选择器消费的是注册情况而非可用性:`listProviders()` 与 Remote 方法 `web/listProviders` 列出每个已注册提供方及其可选的 `title` 与 `description`(包括不可用的提供方),且每次注册表变更后都会触发无载荷的 `web/providers-changed` 事件。[Web 提供方选择决策](2026-10-29-web-provider-selection.zh.md)记录了为何注册情况可观察而可用性不可观察。
|
||||
|
||||
## 包拓扑
|
||||
|
||||
@@ -43,11 +43,9 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
|
||||
依赖方向与 bash 和 filesystem 一致:
|
||||
|
||||
```text
|
||||
@deepseek-ai/dsh-tool-web --depends on--> @deepseek-ai/dsh-web <--depends on-- @deepseek-ai/dsh-web-search-exa
|
||||
@deepseek-ai/dsh-tool-web --depends on--> @deepseek-ai/dsh-web <--depends on-- @deepseek-ai/dsh-web-search-deepseek
|
||||
consumer interface implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-search-perplexity
|
||||
implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-search-deepseek
|
||||
<--depends on-- @deepseek-ai/dsh-web-deepseek-api
|
||||
implementation
|
||||
<--depends on-- @deepseek-ai/dsh-web-fetch-http
|
||||
implementation
|
||||
@@ -57,9 +55,7 @@ Web 访问是一个一等能力 seam,遵循[能力 seam Agent Note](2026-06-13
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
exa["@deepseek-ai/dsh-web-search-exa"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
perplexity["@deepseek-ai/dsh-web-search-perplexity"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web
|
||||
deepseek["@deepseek-ai/dsh-web-search-deepseek"] -->|registerSearchProvider| web["@deepseek-ai/dsh-web / ctx.web"]
|
||||
deepseekApi["@deepseek-ai/dsh-web-deepseek-api"] -->|registerSearchProvider / registerFetchProvider| web
|
||||
fetchLocal["@deepseek-ai/dsh-web-fetch-http"] -->|registerFetchProvider| web
|
||||
toolWeb["@deepseek-ai/dsh-tool-web"] -->|search/fetch| web
|
||||
@@ -67,9 +63,9 @@ flowchart LR
|
||||
toolWeb -->|ctx.tools.register| webFetch["tool: web_fetch"]
|
||||
```
|
||||
|
||||
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性约定和错误码。它不导入工具、agent(智能体)、会话、LLM 或提供方包。
|
||||
`@deepseek-ai/dsh-web` 仅依赖 Cordis 和底层 harness 支持,其中包括其 Remote 方法所用的 Typert 协议。它声明 `ctx.web`、提供方接口、请求/结果类型、提供方可用性约定和错误码。它不导入工具、agent(智能体)、会话、LLM 或提供方包。
|
||||
|
||||
提供方包仅依赖 `dsh-web` 和 Cordis。它们拥有凭证、端点、协议格式映射、解析和 `WebError` 转换,使用平台 `fetch`。每个提供方注入共享服务并注册后端;只有 `dsh-web` 拥有 `ctx.web` 键。提供方私有的协议形状不会产生对 `ctx.llm` 或 Cordis HTTP 服务的依赖。
|
||||
提供方包依赖 `dsh-web`、Cordis 及其凭据和配置函数。它们拥有凭证、端点、协议格式映射、解析和 `WebError` 转换,使用平台 `fetch`。每个提供方注入共享服务并注册后端;只有 `dsh-web` 拥有 `ctx.web` 键。提供方私有的协议形状不会产生对 `ctx.llm` 或 Cordis HTTP 服务的依赖。
|
||||
|
||||
`@deepseek-ai/dsh-tool-web` 依赖 `@deepseek-ai/dsh-web`、`@deepseek-ai/dsh-tools`、`@deepseek-ai/dsh-system-prompt` 和 Cordis。它从不导入具体的提供方包。
|
||||
|
||||
@@ -78,16 +74,22 @@ flowchart LR
|
||||
`ctx.web` 是一个提供方注册表加上一个带提供方选择的执行 API。注册表部分与 `LlmRuntime` 保持接近:每种能力类别一个 `Map<id, provider>`,`registerSearchProvider`/`registerFetchProvider` 方法返回 disposer,重复 id 抛出 `WebError`,执行时解析在选定提供方缺失或不可用时抛出异常。权威签名见 `packages/web/web/src/types.ts`;seam 的形状:
|
||||
|
||||
```ts
|
||||
import type { WebFetchRequest, WebFetchResult, WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web'
|
||||
import type {
|
||||
WebFetchRequest, WebFetchResult, WebProviderInfo, WebProviderList, WebSearchRequest, WebSearchResult,
|
||||
} from '@deepseek-ai/dsh-web'
|
||||
|
||||
interface WebSearchProvider {
|
||||
readonly id: string
|
||||
readonly title?: string
|
||||
readonly description?: WebProviderInfo['description']
|
||||
available(): boolean
|
||||
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
|
||||
}
|
||||
|
||||
interface WebFetchProvider {
|
||||
readonly id: string
|
||||
readonly title?: string
|
||||
readonly description?: WebProviderInfo['description']
|
||||
available(): boolean
|
||||
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
|
||||
}
|
||||
@@ -95,6 +97,7 @@ interface WebFetchProvider {
|
||||
interface WebRuntime {
|
||||
registerSearchProvider(provider: WebSearchProvider): () => void
|
||||
registerFetchProvider(provider: WebFetchProvider): () => void
|
||||
listProviders(): WebProviderList
|
||||
|
||||
search(request: WebSearchRequest, signal?: AbortSignal): Promise<WebSearchResult>
|
||||
fetch(request: WebFetchRequest, signal?: AbortSignal): Promise<WebFetchResult>
|
||||
@@ -131,18 +134,15 @@ interface WebRuntime {
|
||||
- id: web
|
||||
name: '@deepseek-ai/dsh-web'
|
||||
config:
|
||||
searchProvider: exa
|
||||
searchProvider: deepseek-search-api
|
||||
fetchProvider: http
|
||||
|
||||
- id: web-search-exa
|
||||
name: '@deepseek-ai/dsh-web-search-exa'
|
||||
|
||||
- id: web-search-perplexity
|
||||
name: '@deepseek-ai/dsh-web-search-perplexity'
|
||||
|
||||
- id: web-search-deepseek
|
||||
name: '@deepseek-ai/dsh-web-search-deepseek'
|
||||
|
||||
- id: web-deepseek-api
|
||||
name: '@deepseek-ai/dsh-web-deepseek-api'
|
||||
|
||||
- id: web-fetch-http
|
||||
name: '@deepseek-ai/dsh-web-fetch-http'
|
||||
|
||||
@@ -150,9 +150,9 @@ interface WebRuntime {
|
||||
name: '@deepseek-ai/dsh-tool-web'
|
||||
```
|
||||
|
||||
运维覆盖走同一条显式选择路径:`DSH_WEB_SEARCH_PROVIDER=perplexity` 等同于配置 `searchProvider: perplexity`,而非 `dsh-tool-web` 内部的隐式优先级链。
|
||||
运维覆盖走同一条显式选择路径:`DSH_WEB_SEARCH_PROVIDER=deepseek-search-api` 等同于配置 `searchProvider: deepseek-search-api`,而非 `dsh-tool-web` 内部的隐式优先级链。
|
||||
|
||||
每次搜索或抓取在执行时读取配置的 id,因此保存的设置变更无需重新挂载 `dsh-web` 即对下一次操作选择另一个提供方。网页搜索设置页的开关借此把两种能力同时切换到 [DeepSeek Search 与 Fetch API](../feature/2026-10-15-deepseek-search-fetch-api.zh.md)。
|
||||
每次搜索或抓取在执行时读取配置的 id,因此保存的设置变更无需重新挂载 `dsh-web` 即可让下一次操作选用新提供方。「网页搜索与获取」设置页的 Search 和 Fetch 下拉框借此分别切换各能力的提供方(包括 [DeepSeek Search 与 Fetch API](../feature/2026-10-15-deepseek-search-fetch-api.zh.md));[Web 提供方选择决策](2026-10-29-web-provider-selection.zh.md)负责内置默认值。
|
||||
|
||||
`ctx.web.search()` 和 `ctx.web.fetch()` 在执行时按上述选择规则解析提供方。如果选定的能力不可用,它们抛出带有结构化代码的 `WebError`,如 `WEB_PROVIDER_UNAVAILABLE`、`WEB_PROVIDER_CONFIGURED_MISSING`、`WEB_PROVIDER_CONFIGURED_UNAVAILABLE` 或 `WEB_PROVIDER_AMBIGUOUS`。如果未显式配置提供方且不存在可用提供方,执行错误是通用的 `WEB_PROVIDER_UNAVAILABLE` 情况;刻意不提供对每个不可用提供方的诊断汇总。
|
||||
|
||||
@@ -168,8 +168,8 @@ interface WebRuntime {
|
||||
|
||||
- `dsh-tool-web` 拥有该值并将其放在 `WebSearchRequest.maxResults` 上。
|
||||
- `ctx.web` 将请求原样传递给选定的提供方。
|
||||
- 当提供方的 API 支持结果数量控制时(Exa 的 `numResults`),提供方在请求层应用 `maxResults`,作为成本/延迟优化。
|
||||
- `ctx.web` 在结果上强制执行上限:如果提供方返回的 source 数量超过 `maxResults`——因为其 API 没有结果数量控制(Perplexity)或忽略了提示——seam 将 `sources[]` 截断到 `maxResults` 并在返回前将 `WebSearchResult.truncated` 设为 `true`。这使上限成为面向模型层可以依赖的单一跨提供方保证,而非每个提供方都必须记得遵守的东西。
|
||||
- 当提供方的 API 支持结果数量控制时(DeepSeek Search API 的 `count`),提供方在请求层应用 `maxResults`,作为成本/延迟优化。
|
||||
- `ctx.web` 在结果上强制执行上限:如果提供方返回的 source 数量超过 `maxResults`——因为其 API 没有结果数量控制或忽略了提示——seam 将 `sources[]` 截断到 `maxResults` 并在返回前将 `WebSearchResult.truncated` 设为 `true`。这使上限成为面向模型层可以依赖的单一跨提供方保证,而非每个提供方都必须记得遵守的东西。
|
||||
|
||||
seam 请求不携带提供方特有的控制——没有 Perplexity 模型选择、搜索时效性、域名过滤器、Exa `livecrawl`、Exa `type`、区域提示、生成式回答预算或搜索深度。只有当某个字段具有提供方无关的语义,且工具 schema 和选定的提供方都能诚实地遵守时,才会添加。
|
||||
|
||||
@@ -190,13 +190,14 @@ interface WebSearchSource {
|
||||
readonly url: string
|
||||
readonly title?: string
|
||||
readonly snippet?: string
|
||||
readonly summary?: string
|
||||
readonly publishedAt?: string
|
||||
}
|
||||
```
|
||||
|
||||
`content` 是可选的提供方生成的回答文本、搜索上下文或摘要。`sources[]` 是可移植的引用结构。source 必有 URL;title、snippet 和 `publishedAt` 可选,因为并非每个提供方都返回它们。`title` 不是必填:Perplexity 风格的引用可能只提供 URL,强制适配器编造标题会让 seam 说谎。`dsh-tool-web` 渲染 `title ?? hostname(url)` 风格的回退标签用于展示。`publishedAt` 是可选的发布/抓取时间戳,为 ISO-8601 字符串——Exa 在每条结果上以 `publishedDate` 返回它,Perplexity 在搜索结果上返回 `date`,因此它是真实的提供方数据而非派生值;seam 以字符串形式传递,日期解析留给消费方。
|
||||
`content` 是可选的提供方生成的回答文本、搜索上下文或摘要。`sources[]` 是可移植的引用结构。source 必有 URL;title、snippet、summary 和 `publishedAt` 可选,因为并非每个提供方都返回它们。`snippet` 是用于展示的短摘录,`summary` 是供模型阅读的较长摘录,其长度由消费方限制。`title` 不是必填:Perplexity 风格的引用可能只提供 URL,强制适配器编造标题会让 seam 说谎。`dsh-tool-web` 渲染 `title ?? hostname(url)` 风格的回退标签用于展示。`publishedAt` 是可选的发布/抓取时间戳,为 ISO-8601 字符串——DeepSeek Search API 在每条结果上以可为 null 的 `date` 返回它,Exa 以 `publishedDate` 返回,Perplexity 以 `date` 返回,因此它是真实的提供方数据而非派生值;seam 以字符串形式传递,日期解析留给消费方。
|
||||
|
||||
Exa 搜索将提供方扁平 `results[]` 的每一项映射为 `WebSearchSource`:`url` ← `url`、`title` ← `title`、`snippet` ← 第一个 `highlights[]` 条目(没有 highlight 的条目没有可移植的 snippet,被丢弃)、`publishedAt` ← `publishedDate`。Exa 不返回提供方生成的回答,因此 `content` 省略。Perplexity 搜索将 `choices[0].message.content` 映射为 `content`,并优先使用结构化的顶层 `search_results[]` 作为 `sources[]`——`url` ← `url`、`title` ← `title`、`snippet` ← `snippet`(常为空)、`publishedAt` ← `date`——仅在 `search_results` 缺失时回退到纯 URL 的 `citations[]` 数组(这些 source 只有 `url`)。如果提供方返回的结构化字段少于 seam 支持的,适配器省略那些可选字段。
|
||||
DeepSeek Search API 提供方将响应 `results[]` 的每一项映射为 `WebSearchSource`:`url` ← `url`、`title` ← `title`、`snippet` ← `snippet`、`summary` ← `summary`、`publishedAt` ← `date`,空值省略。它不返回提供方生成的回答,因此 `content` 省略。如果提供方返回的结构化字段少于 seam 支持的,适配器省略那些可选字段。
|
||||
|
||||
完整页面获取仍是 `web_fetch(url)` 的职责。搜索 snippet 是发现上下文,不是获取到的页面正文。
|
||||
|
||||
@@ -283,7 +284,7 @@ fetch 提供方的资源控制:
|
||||
|
||||
## 测试
|
||||
|
||||
每一层在自己的边界处固定:`dsh-web` 中的注册/选择/截断/abort 约定与 `WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射(Perplexity fixture 包含纯 URL 引用,以保持可选 source 字段的诚实性),加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-http` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem) 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`。
|
||||
每一层在自己的边界处固定:`dsh-web` 中的注册/选择/截断/abort 约定与 `WebError` 码;每个提供方基于录制的 fixture(测试前置数据)的请求/响应映射,加上每个真实提供方的自跳过带密钥冒烟测试;`web-fetch-http` 中的真实本地 HTTP 行为;`dsh-tool-web` 中通过真实工具注册表的启用驱动注册、结构化执行错误和结果格式化。一个真实 Loader 冒烟测试守护两种导出形状([事故复盘(postmortem) 0001](../../../../docs/postmortem/0001-acp-default-export-drops-inject.zh.md)):`dsh-web` 是默认导出的服务,而提供方和 `tool-web` 是命名空间插件,误加 `export default` 会丢失 `inject`。
|
||||
|
||||
## 曾考虑的替代方案
|
||||
|
||||
@@ -293,7 +294,7 @@ fetch 提供方的资源控制:
|
||||
|
||||
### 将提供方调度直接放在 `dsh-tool-web` 中
|
||||
|
||||
这类似 OpenCode 的本地 web 搜索:一个稳定的 `websearch` 工具在内部调度到 Exa 或 Parallel。对于小型产品路径可以接受,但作为 harness 基础是错误的。工具包将拥有提供方选择、凭证、请求映射、传输、响应解析和展示,使得在不将 Exa 和 Perplexity 的差异烘焙进工具 schema 的情况下难以添加它们。
|
||||
这类似 OpenCode 的本地 web 搜索:一个稳定的 `websearch` 工具在内部调度到 Exa 或 Parallel。对于小型产品路径可以接受,但作为 harness 基础是错误的。工具包将拥有提供方选择、凭证、请求映射、传输、响应解析和展示,使得在不将各提供方格式差异烘焙进工具 schema 的情况下难以添加格式不同的提供方。
|
||||
|
||||
### 将搜索和 fetch 拆为两个 seam(`dsh-search`、`dsh-fetch`)
|
||||
|
||||
@@ -322,9 +323,9 @@ fetch 提供方的资源控制:
|
||||
|
||||
## 后果
|
||||
|
||||
**搜索 schema 刻意精简。** Exa 和 Perplexity 都暴露了有用的提供方特有控制;只有当某个控制能以提供方无关的方式定义、且工具注册和提供方执行都能诚实遵守时,才会添加。
|
||||
**搜索 schema 刻意精简。** 各搜索 API 暴露了有用的提供方特有控制,例如 Exa 的 `livecrawl`、Perplexity 的搜索时效性和 DeepSeek Search API 的 `localization`;只有当某个控制能以提供方无关的方式定义、且工具注册和提供方执行都能诚实遵守时,才会添加。
|
||||
|
||||
**Perplexity 引用可能稀疏。** 一条引用可能只有 URL。将 `title` 和 `snippet` 设为可选使 seam 保持诚实,但意味着 `tool-web` 需要渲染回退标签。
|
||||
**引用可能稀疏。** 一条 Perplexity 风格的引用可能只有 URL。将 `title` 和 `snippet` 设为可选使 seam 保持诚实,但意味着 `tool-web` 需要渲染回退标签。
|
||||
|
||||
**稳定的工具注册将配置错误推迟到执行时。** 当产品启用了 web 访问时,保持工具可见是正确的;但期望 web 搜索可用的产品应用应当明确暴露结构化的 `WEB_PROVIDER_CONFIGURED_MISSING`/`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`/`WEB_PROVIDER_AMBIGUOUS` 失败,避免用户直到模型调用工具后才发现配置问题。
|
||||
|
||||
@@ -340,7 +341,7 @@ fetch 提供方的资源控制:
|
||||
|
||||
- `pdf` `WebFetchBody` 类别:`http` 提供方将可文本提取的 PDF 解码(尽力而为、有上限、`truncated`)为 `{ kind: 'pdf'; content; pageCount? }` 分支,`tool-web` 渲染它。这是 fetch 而非 `web_extract`——PDF 获取是具体的 HTTP 200 加确定性的本地解码,不是提供方侧对非 HTTP 资源的提取。添加它是跨 `dsh-web`(声明分支)、提供方(解码 + 将「二进制拒绝」收窄为「拒绝二进制,但可文本提取的 PDF 除外」;需要 OCR 的扫描/图片 PDF 不在范围内)和 `tool-web`(渲染)的协调变更。封闭的 `WebFetchBody` 联合类型使消费方在新分支被处理之前编译失败。
|
||||
- 提供方支撑的提取作为独立的 `web_extract` 能力,而非静默扩展 `web_fetch`。
|
||||
- `query` 和 `maxResults` 之外的提供方无关搜索控制,待 Exa 和 Perplexity 都能诚实遵守时再添加。
|
||||
- `query` 和 `maxResults` 之外的提供方无关搜索控制,待已注册的提供方都能诚实遵守时再添加。
|
||||
|
||||
## 开放问题
|
||||
|
||||
|
||||
+4
-4
@@ -21,8 +21,8 @@
|
||||
en: 0a4240d88cca240a
|
||||
zh: 8f5f1527caf571cd
|
||||
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/the-loading-flow-end-to-end:
|
||||
en: 15fb56befd328339
|
||||
zh: 0ffb10ada7e52805
|
||||
en: dd05aa1ee608c414
|
||||
zh: 2863ced0086e3122
|
||||
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/decision/live-graph-reconciliation:
|
||||
en: 37e2fa1fe5cb2b95
|
||||
zh: 80e513db87709cb4
|
||||
@@ -33,8 +33,8 @@
|
||||
en: 94ca0d09e6fd1e3a
|
||||
zh: d5949a2bdec795ba
|
||||
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/consequences:
|
||||
en: 16e1f2467c559315
|
||||
zh: a714ec6ff2722cf1
|
||||
en: 624efdd136029771
|
||||
zh: 7d60207147e631fe
|
||||
/agent-note-client-plugin-loading-lazy-factories-cordis-lifecycle-and-hot-reload/alternatives-considered:
|
||||
en: b8f036f50cff7cd8
|
||||
zh: 6d623c4a1f4314b1
|
||||
|
||||
@@ -67,8 +67,8 @@ Why is the roster yml rows and not a scan? Because which plugins compose into a
|
||||
1. The kernel mounts the vendored Loader and injects the module system as `internal` before any entry exists. Ordering matters: `tree.import`'s bare-import fallback must never run in a browser.
|
||||
2. It creates every graph row uniformly. Importing the modules row returns the memoized bootstrap exports, whose `apply()` reads that tree's `Loader.internal` and provides the same instance as `ctx.modules`; rows that require that service remain PENDING until then, so the modules row needs no special creation position. Render assembly is an ordinary host-graph row provided by `dsh-client-ui-renderer`; the kernel appends no assembly pseudo-entry.
|
||||
3. Graph order governs synchronous factory availability; Cordis activation remains independent and proceeds through service waiting.
|
||||
4. `settled` = every entry created + `loader.await()` quiescent + an all-ACTIVE sweep. The sweep lists each import-failed, FAILED, or PENDING fiber with its missing services. It exists because cordis inject waits have no timeout — the sweep is the fail-loud floor.
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the sweep, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
||||
4. `settled` = every entry created + `loader.await()` quiescent + the boot audit. The audit lists each import-failed, FAILED, or PENDING fiber with its activation error or missing services. It exists because cordis inject waits have no timeout. An inactive entry on the browser required list rejects startup; other inactive entries produce one console warning ([startup policy](2026-09-09-consumer-owned-startup-strictness.md)).
|
||||
5. The framework-free loading page projects real fiber states via `internal/status`. After the audit accepts the tree, the kernel calls `ctx.uiRenderer.mount(container)` and replaces the page with the real UI in one pass.
|
||||
|
||||
### Live graph reconciliation
|
||||
|
||||
@@ -104,7 +104,7 @@ The current package inventory and build forms live in the [client shell layering
|
||||
|
||||
One governance implementation runs on both sides of the wire; the browser-specific layer is one module system plus one reload plugin. Dynamic packages have one artifact form, so the purity check covers them all. Cordis dependencies, module requests, and the boot tier live with their owners — the manifests — while the composing app holds only the roster. Host graph validation and recursive request arrival keep synchronous factory dependencies explicit. Browser-native script loading preserves the standard mapping among plugin network resources, generated bundles, and TypeScript/TSX sources, while the module system keeps two replaceable carrier hooks: `loadBundle` for bundle bytes and `resolveAsset` for package-asset references.
|
||||
|
||||
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the settled sweep; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains bundle snapshots and lazy response plans for the current graph and one prior startup generation. Script and map bodies are cached after their first `GET`, so memory grows with bundle snapshots and requested response bodies. Each materialized response stays fixed at its URL; a prior-generation map first requested after a rebuild reads the current map file.
|
||||
Costs accepted: the vendored Loader carries idle machinery in the browser (EntryTree persistence is a no-op, groups/isolation unused); every plugin edit in dev pays a bundle rebuild plus fiber remount; graph `inject` rows guide factory arrival but service availability remains the activation authority, so a mismatch appears at the boot audit; the static UI libraries keep direct value exports; every bundle gains a source-map artifact; and external-script failures provide only coarse URL diagnostics instead of the HTTP status available to an explicit fetch. The Host retains bundle snapshots and lazy response plans for the current graph and one prior startup generation. Script and map bodies are cached after their first `GET`, so memory grows with bundle snapshots and requested response bodies. Each materialized response stays fixed at its URL; a prior-generation map first requested after a rebuild reads the current map file.
|
||||
|
||||
Roster: it lives in the web bundle's config tree (`packages/bundle/web-app/cordis.patch.yml`); `mountWebPlugins` and the `CLIENT_PACKAGES` constant are gone, and recomposing a deployment means swapping the yml/overlay. The graph composer lives in the `dsh-client-modules` node half, while the parser-preloaded client face bootstraps the browser module table. The webserver remains a plain route-registration plugin; `/api/*` binding, browser authentication, RPC envelopes, and exact Fetch routes belong to the Connection node half, while Remote dispatch belongs to API Gateway and the dev bundle watch plus SSE channel belongs to the hmr node half.
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ Host 会快照每个已构建插件入口 bundle,并把每个调度阶段的
|
||||
1. 内核挂载 vendored Loader,在任何 entry 存在之前就把模块系统注入为 `internal`。顺序有讲究:`tree.import` 的裸 import 兜底分支在浏览器里绝不能跑到。
|
||||
2. 它统一创建每个 graph row。Import modules row 会返回记忆化的 bootstrap exports,其 `apply()` 读取该树的 `Loader.internal`,把同一个实例提供为 `ctx.modules`;需要该 service 的 row 会保持 PENDING 直至此时,因此 modules row 无需特殊创建位置。渲染组装是由 `dsh-client-ui-renderer` 提供的普通 host graph row;内核不追加组装伪 entry。
|
||||
3. Graph 顺序治理同步 factory 可用性;Cordis 激活与之独立,仍经服务等待推进。
|
||||
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 一次全 ACTIVE 扫描。扫描列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其缺失的服务。它存在的理由:cordis 的 inject 等待没有超时——这次扫描就是大声失败的兜底线。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。检查完成后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
|
||||
4. `settled` = 每个 entry 已创建 + `loader.await()` 完全停稳 + 启动审计。审计列出每个 import 失败、FAILED 或 PENDING 的 fiber 及其激活错误或缺失的服务。它存在的理由:cordis 的 inject 等待没有超时。浏览器必需名单中的 entry 未激活会拒绝启动;其他未激活 entry 输出一条控制台警告([启动策略](2026-09-09-consumer-owned-startup-strictness.zh.md))。
|
||||
5. 不依赖框架的 loading 页经 `internal/status` 投影真实 fiber 状态。审计接受插件树后,内核调用 `ctx.uiRenderer.mount(container)`,一次切换到真实 UI。
|
||||
|
||||
### 动态图对账
|
||||
|
||||
@@ -104,7 +104,7 @@ Bootstrap 替换会在失效或卸载前被拒绝:模块系统保留其初始
|
||||
|
||||
Wire 两侧运行同一份治理实现;浏览器特有层只包含一套模块系统和一个重载插件。动态包只有一种产物形态,因此纯度检查覆盖全部动态包。Cordis 依赖、模块请求与启动档位都与其所有者——manifest——同住,负责组合的 app 只握名册。Host graph 校验与递归请求到达使同步 factory 依赖保持显式。浏览器原生 script 装载保留插件网络资源、生成 bundle 与 TypeScript/TSX 源码之间的标准映射,模块系统只保留两个可替换的载体钩子:负责 bundle 字节的 `loadBundle` 和负责包资源引用的 `resolveAsset`。
|
||||
|
||||
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在 settled 扫描时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
|
||||
接受的代价:vendored Loader 在浏览器里背着闲置机件(EntryTree 持久化是 no-op,分组/隔离未用);开发期每次修改插件都要付一次 bundle 重建加 fiber 重挂;graph `inject` row 指导 factory 到达,但服务可用性仍是激活权威,因此不匹配会在启动审计时浮出;静态 UI 库保留直接实体导出;每个 bundle 多出一份 sourcemap 产物,外部 script 失败也只能给出粗粒度 URL 诊断,不能像显式 fetch 那样报告 HTTP 状态。Host 为当前图及上一代启动图保留 bundle 快照与惰性响应计划。脚本和 map body 在首次 `GET` 后缓存,因此内存随 bundle 快照和已请求的响应 body 增长。每份已物化响应在其 URL 下保持固定;若上一代 map 在重建后才首次被请求,则会读取当前 map 文件。
|
||||
|
||||
名册位于 web 组合包的配置树(`packages/bundle/web-app/cordis.patch.yml`);`mountWebPlugins` 与 `CLIENT_PACKAGES` 常量已消失,重组一次部署等于替换 yml/overlay。Graph 组合器位于 `dsh-client-modules` node 半,由 parser 预载的 Client face 则自举浏览器模块表。Webserver 继续作为朴素路由注册插件;`/api/*` 绑定、浏览器认证、RPC envelope 与精确 Fetch 路由属于 Connection node 半,Remote 分发属于 API Gateway,开发期 bundle 监视与 SSE 通道属于 HMR node 半。
|
||||
|
||||
|
||||
+2
-2
@@ -12,8 +12,8 @@
|
||||
en: aea80ece13f2905b
|
||||
zh: a880bc1a7839cf12
|
||||
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/consequences:
|
||||
en: 640b7ee4e8c57ed0
|
||||
zh: 1c17b3fba16861a8
|
||||
en: 41ebe22077560090
|
||||
zh: da3475e4c196c3f9
|
||||
/agent-note-one-ordering-for-configuration-sources-and-what-a-discovered-file-may-not-decide/alternatives-considered:
|
||||
en: ac43499cfe7b8495
|
||||
zh: e5ab599e7d3ed753
|
||||
|
||||
@@ -58,7 +58,6 @@ The line is that these take effect with no user action, before any turn, outside
|
||||
- A `.env` holding `DSH_*`, `PATH`, `BROWSER`, or — in the invoking directory — a proxy variable fails the launch instead of being applied. Developers keeping switches in a repository `.env` move them to their shell — a deliberate, loud break.
|
||||
- Composition is no longer overridable by a stale shell endpoint. It is still overridable by a user's stored `settings.yaml`, which is the settings seam's layering and not something this note changes; the product CLI offers no flag above it, so a deployment that must win against stored settings owns its own bin or loader tree.
|
||||
- Not solved: the layers are still materialized into `process.env`, so ordinary project variables continue to reach child processes under the subprocess scrub. Bootstrap variables cannot come from a file at all; the environment package records the remaining subprocess reach as a limitation.
|
||||
- Exa and Perplexity still capture their key at load time rather than through the credential seam. They no longer read raw `process.env` — they resolve through the trusted layers — but converting them to per-request credential resolution is separate work.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ inherited process environment (read-only, wins)
|
||||
- 含 `DSH_*`、`PATH`、`BROWSER` 或(在调用目录中)proxy 变量的 `.env` 会导致启动失败而不是被应用。把开关放在仓库 `.env` 里的开发者需要改放到 shell——这是一次刻意且响亮的破坏。
|
||||
- composition 不再会被陈旧的 shell endpoint 覆盖。但它仍然会被用户已存的 `settings.yaml` 覆盖,这是 settings seam 的分层方式,本 Note 不改变它;产品 CLI 没有高于它的标志,因此需要压过已存 settings 的部署方要自带 bin 或 loader 配置树。
|
||||
- 未解决的:各层仍然会被物化进 `process.env`,因此普通项目变量继续按子进程清洗规则抵达子进程。bootstrap 变量完全不能来自文件;环境包将其余变量仍可抵达子进程这一点记录为一项限制。
|
||||
- Exa 与 Perplexity 仍在加载时捕获密钥,而不是经凭据 seam。它们不再读裸 `process.env`——改为经受信层解析——但把它们改造成按请求解析凭据是另一件事。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
|
||||
+4
-4
@@ -9,8 +9,8 @@
|
||||
en: 4548e7fbdbee5b5a
|
||||
zh: 0078762ea92fa180
|
||||
/agent-note-consumer-owned-startup-strictness/decision:
|
||||
en: 1f11e04162e19ad1
|
||||
zh: d102e300eba9a2ea
|
||||
en: 62e93742af5bed66
|
||||
zh: 3944d685b6503a2c
|
||||
/agent-note-consumer-owned-startup-strictness/alternatives-considered:
|
||||
en: 1b67e062213184cd
|
||||
zh: 91cc8a015de2bfe4
|
||||
@@ -18,5 +18,5 @@
|
||||
en: 283dec1b336c540d
|
||||
zh: 2ed358079072cfff
|
||||
/agent-note-consumer-owned-startup-strictness/testing:
|
||||
en: c38221069ce98fd3
|
||||
zh: 9a2f91f539eba4d4
|
||||
en: cee14c22297e5471
|
||||
zh: a68748cc36f4a574
|
||||
|
||||
@@ -20,6 +20,8 @@ The audit runs only during initial application boot. Later config HMR remains be
|
||||
|
||||
This policy governs [Web host boot](../../../../packages/boot/app-boot/README.md), including its [client plugin roster](2026-07-23-client-plugin-loading-model.md). [Per-session presets](2026-09-18-declarative-agent-presets.md) own a separate strict subtree audit.
|
||||
|
||||
The browser boot audit applies the same policy to the client plugin tree with its own global list in [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts): the module system, Session control, the renderer, the application frame, the Session view, and its sidebar. Browser entries are named by package, so the list holds package names; a name absent from the boot graph has no effect, and neither list is derived from the other. The list is the minimum fatal set: an entry that a listed entry waits on blocks startup too. Feature plugins, including credential masking, stay optional. A failed entry reports its activation error. Optional failures appear in one console warning, and in Settings → Plugins → Plugin list only after a later graph synchronization reconciles them. Renaming a listed package requires updating the list and its tests.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Add transactional and best-effort modes to vendored Loader.** Rejected because strictness belongs to the application or resource owner, while a Loader group contains unrelated plugins. A mode would also expand the vendor patch and leave callers to select a policy at every group.
|
||||
@@ -36,6 +38,8 @@ The compact terminal report keeps the failing plugins visible; a separate file r
|
||||
|
||||
App-boot unit tests cover absent and disabled required ids, optional import failure, config evaluation failure, synchronous and asynchronous `apply()` failure, pending dependencies, and required failure teardown. Unit expectations pin diagnostic grouping, preservation of original error objects and import logs, exporter cleanup, complete diagnostic values, private file creation, concurrent report names, and failed-write fallback. The built Web-profile acceptance asserts a single port-conflict stack without Node wrapper output, verifies the saved diagnostic file and its stderr fallback, serves the full UI with optional failures and exits nonzero without readiness when the required HTTP port is occupied or `modules` or `connection` cannot activate.
|
||||
|
||||
Browser audit unit tests (`boot-client.client.spec.ts`, `boot.client.spec.ts`) cover required and optional import, creation, activation, and pending failures, carrier presentation, and required names absent from the boot graph. Test carriers are stricter than the product: `TestClient` requires every roster entry to activate unless a case passes `allowInactiveEntries`, and the browser e2e lanes treat a `web boot: warning` as a failure with no opt-out.
|
||||
|
||||
The [Web process matrix](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts) independently exercises optional and required failures at startup and after native patch-file edits. Authenticated HTTP requests and plugin lifecycle files distinguish a usable application from a surviving process. These keyless process checks complement the [controlled-delivery unit tests](../../../../packages/boot/app-boot/tests/user-patches.spec.ts): unit tests isolate reconciliation failures, while the process tests also require the shipped launcher, native watcher, and bounded shutdown to work together.
|
||||
|
||||
The matrix enables Chokidar's `awaitWriteFinish` to acknowledge stable file contents before each reload; otherwise its short change-event suppression window can discard the next test edit. Native events remain required, and assertions wait for observed activation or failure rather than a fixed settling sleep. This is explicit test configuration, not evidence for the default watcher timing.
|
||||
|
||||
+4
@@ -20,6 +20,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
|
||||
|
||||
该策略适用于 [Web host 启动](../../../../packages/boot/app-boot/README.zh.md),包括其 [client 插件名册](2026-07-23-client-plugin-loading-model.zh.md)。[按会话的 preset](2026-09-18-declarative-agent-presets.zh.md)持有独立的严格子树审计。
|
||||
|
||||
浏览器启动审计以同一策略审计客户端插件树,使用 [`boot-client.ts`](../../../../packages/client/web/src/boot-client.ts) 中独立的全局名单:模块系统、Session 控制、渲染器、应用框架、Session 视图及其侧栏。浏览器 entry 以包名命名,因此名单写包名;启动图中不存在的名字不产生影响,两份名单互不推导。名单是最小致命集合:已列出 entry 所等待的 entry 同样会阻止启动。功能插件(包括凭据遮挡)保持可选。失败 entry 报告其激活错误。可选失败出现在一条控制台警告中;只有后续图同步调和之后,才会出现在「设置 → 插件 → 插件列表」。重命名名单中的包需要同步更新名单及其测试。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **给 vendored Loader 增加 transactional 与 best-effort mode。** 拒绝,因为严格语义属于应用或资源 owner,而一个 Loader group 包含互不相关的 plugin。Mode 还会扩大 vendor patch,并要求 caller 为每个 group 选择 policy。
|
||||
@@ -36,6 +38,8 @@ Required id 为 `agent-loop`、`webserver`、`modules`、`connection`、`headles
|
||||
|
||||
App-boot 单元测试覆盖缺失和禁用的 required id、optional import failure、config evaluation failure、同步和异步 `apply()` failure、pending dependency,以及 required failure teardown。单元预期输出固定诊断分组、原始错误对象与导入日志的保留、exporter 清理、完整诊断值、私有文件创建、并发报告命名以及写入失败回退行为。构建后的 Web-profile acceptance 断言端口冲突堆栈只输出一次且不包含 Node 包装输出,验证已保存的诊断文件及其 stderr 回退,并会在 optional failure 存在时继续提供完整 UI,并在 required HTTP port 被占用或 `modules`、`connection` 无法激活时以非零码退出,且不报告就绪。
|
||||
|
||||
浏览器审计单元测试(`boot-client.client.spec.ts`、`boot.client.spec.ts`)覆盖必需与可选 entry 的导入、创建、激活与 pending 失败、carrier 呈现,以及启动图中不存在的必需名称。测试载体比产品更严格:除非用例传入 `allowInactiveEntries`,`TestClient` 要求每个 roster entry 都激活;浏览器 e2e 把 `web boot: warning` 视为失败,且不提供放宽。
|
||||
|
||||
[Web 进程矩阵](../../../../apps/cli/tests/profiles/web/tests/web-failure-matrix.expected.e2e.ts)分别验证启动时和原生补丁文件修改后的 optional 与 required 失败。经过认证的 HTTP 请求和插件生命周期文件区分可用应用与仅存活的进程。这些无需密钥的进程检查与[受控事件投递单元测试](../../../../packages/boot/app-boot/tests/user-patches.spec.ts)互补:单元测试隔离配置协调失败,进程测试还要求随附启动器、原生监听器和有界关闭流程协同工作。
|
||||
|
||||
矩阵启用 Chokidar 的 `awaitWriteFinish`,在每次重载前确认文件内容已稳定;否则它的短暂 change 事件抑制窗口可能丢弃下一次测试编辑。测试仍然依赖原生事件,并等待观察到激活或失败,而不是固定时长的休眠。这是显式测试配置,不能证明默认监听器的时序行为。
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 2abacff687661d28
|
||||
zh: 2d7ba3ae27dba888
|
||||
/agent-note-keep-live-configuration-in-the-owning-profile/decision:
|
||||
en: 66c3884425cfb90a
|
||||
zh: 0fe6a1f1a576470b
|
||||
en: f9b1dd3d158457a9
|
||||
zh: 06ca76fcbbc76cda
|
||||
/agent-note-keep-live-configuration-in-the-owning-profile/alternatives-considered:
|
||||
en: 4089464ad1fb60bd
|
||||
zh: 3168e9e4a205145d
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ A plugin declares every configurable value in its Cordis Config. Fields that can
|
||||
|
||||
Form writes target the active profile. Home patches and command-line overlays remain higher-priority deployment inputs; an edit they would shadow fails before persistence. Two plugin instances are addressed by their distinct profile entry ids. Nested Includes retain independent ownership and are not writable through the profile form.
|
||||
|
||||
Cordis config patches replace complete entry configs. An edit preserves ordinary fields and unedited secrets, including raw configuration expressions. A field reset restores the currently inherited value; resetting the complete entry removes its config overrides so later bundle changes are inherited. Other edits store the entry's complete config in the profile row: the ordinary fields as composed at write time plus every volatile field. Later bundle changes to those fields do not reach that profile until the entry's config override is removed. In the shipped bundles this pins `permission.presets` after a default-preset change, `agent-presets` after a preset choice, `agent-loop.agents` after a parallelism edit, and `web-search-deepseek.apiKeyEnv` after a Web Search edit, and the client marks every volatile field of such a row as overridden, not only the edited one. Narrowing a write to the edited fields needs merge semantics for patch `config`, which Include does not provide.
|
||||
Cordis config patches replace complete entry configs. An edit preserves ordinary fields and unedited secrets, including raw configuration expressions. A field reset restores the currently inherited value; resetting the complete entry removes its config overrides so later bundle changes are inherited. Other edits store the entry's complete config in the profile row: the ordinary fields as composed at write time plus every volatile field. Later bundle changes to those fields do not reach that profile until the entry's config override is removed. In the shipped bundles this pins `permission.presets` after a default-preset change, `agent-presets` after a preset choice, `agent-loop.agents` after a parallelism edit, and `web-search-deepseek.apiKeyEnv` after a legacy Messages search edit on the Web search and fetch page, and the client marks every volatile field of such a row as overridden, not only the edited one. Narrowing a write to the edited fields needs merge semantics for patch `config`, which Include does not provide.
|
||||
|
||||
The removed `$DSH_HOME/settings.yaml` is imported once into the active profile when the Loader has settled every entry after Settings starts: section ids are entry ids, with `ui-developer-tools` → `ui-settings`, `ui-onboarding` → `ui-settings-general`, and `shell` → the platform's shell executor entry. The file is renamed to `settings.yaml.imported` before the first write, so the import never repeats; a rejected section is logged and stays in the renamed file.
|
||||
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Status: implemented
|
||||
|
||||
表单写入当前 profile。home patch 和命令行 overlay 仍是优先级更高的部署输入;如果它们会遮蔽某次修改,编辑器会在持久化前拒绝写入。两个插件实例通过各自不同的 profile entry id 寻址。嵌套 Include 保留独立所有权,不能通过该 profile 表单写入。
|
||||
|
||||
Cordis config patch 替换完整 entry config。编辑会保留普通字段、未编辑的秘密值和原始配置表达式。字段重置恢复当前继承值;整个 entry 重置后会移除其 config override,从而继承后续 bundle 变化。其他编辑会把该 entry 的完整 config 存入 profile 行:写入时组合出的普通字段加上全部 volatile 字段。此后 bundle 对这些字段的修改在该 entry 的 config override 被移除之前不会到达该 profile。在已发布的 bundle 中,修改默认权限预设会钉住 `permission.presets`,选择预设会钉住 `agent-presets`,编辑并行度会钉住 `agent-loop.agents`,编辑网页搜索会钉住 `web-search-deepseek.apiKeyEnv`;客户端会把这类行的每个 volatile 字段都标为已覆盖,而不只是被编辑的那个。要把写入收窄到被编辑字段需要 patch `config` 的合并语义,Include 不提供。
|
||||
Cordis config patch 替换完整 entry config。编辑会保留普通字段、未编辑的秘密值和原始配置表达式。字段重置恢复当前继承值;整个 entry 重置后会移除其 config override,从而继承后续 bundle 变化。其他编辑会把该 entry 的完整 config 存入 profile 行:写入时组合出的普通字段加上全部 volatile 字段。此后 bundle 对这些字段的修改在该 entry 的 config override 被移除之前不会到达该 profile。在已发布的 bundle 中,修改默认权限预设会钉住 `permission.presets`,选择预设会钉住 `agent-presets`,编辑并行度会钉住 `agent-loop.agents`,在「网页搜索与获取」页编辑旧版 Messages 搜索会钉住 `web-search-deepseek.apiKeyEnv`;客户端会把这类行的每个 volatile 字段都标为已覆盖,而不只是被编辑的那个。要把写入收窄到被编辑字段需要 patch `config` 的合并语义,Include 不提供。
|
||||
|
||||
被移除的 `$DSH_HOME/settings.yaml` 会在 Settings 启动后、Loader 完成所有条目加载时一次性导入当前 profile:section id 即 entry id,其中 `ui-developer-tools` → `ui-settings`、`ui-onboarding` → `ui-settings-general`、`shell` → 当前平台的 shell 执行器条目。文件在第一次写入前改名为 `settings.yaml.imported`,因此导入不会重复;被拒绝的 section 会记录日志并保留在改名后的文件中。
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
en: 97fe58a13135b4a1
|
||||
zh: 7490373a8d941da5
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/decision:
|
||||
en: 0c40a9283650c643
|
||||
zh: 91a18d752d09a9ce
|
||||
en: 24138152d511bf12
|
||||
zh: 95516d7566b6db7c
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/alternatives-considered:
|
||||
en: b78785f2a603eba8
|
||||
zh: b8c268228da7e2e7
|
||||
en: 311db1f781e386e7
|
||||
zh: 7e3312544154a158
|
||||
/agent-note-active-tool-headers-and-stateful-request-projection/consequences:
|
||||
en: 60d7bf28fa5a8fd1
|
||||
zh: a17b222e7aca01e2
|
||||
en: 8a64dba42c01f647
|
||||
zh: 9e7ba7a5bd855031
|
||||
|
||||
@@ -14,9 +14,9 @@ Plugin and MCP tools change during conversations. Providers differ in whether th
|
||||
|
||||
`Session.toolHistory()` caches a capability-independent fold of committed headers and developer messages. First access restores inherited events; later reads consume only unseen events. Immutable snapshots carry initial declarations and ordered update identities with additions resolved through their historical headers. Explicit series starts and changed retained definitions rebuild declarations. Restoring a retained name with an unchanged definition continues the series: the projection keeps the declaration unchanged and lets the recorded addition re-offer the tool after its earlier removal, so a removal never disables a declared tool and a loading mode never rewrites an earlier declaration.
|
||||
|
||||
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. Only current-series update identities survive. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates. Explicit deferred loading remains available on capable routes.
|
||||
Loop and compaction requests carry this snapshot as `GenerateOptions.toolHistory`. At adapter dispatch, `projectToolUpdates` uses the prepared route's `toolUpdate`. Unsupported routes receive active definitions without `deferLoading` or developer messages. Supported routes defer historical additions; `in-history` retains removed definitions and removal blocks, while `addition-only` omits both. On addition-only routes, removing any tool rebuilds the route's baseline after that update: the request declarations change even when the removed tool was activated later, so retaining superseded activation blocks cannot preserve the full conversation prefix. Adding tools when no declarations exist also rebuilds the baseline. Deferred declarations count as existing tools. The first native tool changes the empty declaration prefix; deferring it provides no cache advantage over declaring it immediately. Previously activated tools become immediate; explicitly deferred declarations with no activation remain deferred. Subsequent additions remain deferred until their recorded activation. Superseded tool blocks are omitted without discarding other content in those messages. The Session fold and durable events remain capability-independent. Missing history or a request prefix omitting recorded updates uses current declarations without developer updates.
|
||||
|
||||
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai declares no mode. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
|
||||
DeepSeek translates projected updates into system-role `tool_addition` and `tool_removal` blocks and `defer_loading` declarations, sending the tool-changes beta header when update blocks occur. The default `deepseek-flash` entry declares `addition-only`; pi-ai selects `in-history` for compatible Anthropic routes and `addition-only` for compatible Chat Completions and Responses routes. Chat and Trajectory display developer messages as context nodes, using localized tool-change notices: single changes show the tool name; multiple changes show counts and expandable name lists.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,6 +26,8 @@ DeepSeek translates projected updates into system-role `tool_addition` and `tool
|
||||
|
||||
**Scan the complete log on every request.** The cached fold consumes new events and supplies stable snapshots to ordinary and auxiliary calls without adding arbitrary synchronous history readers.
|
||||
|
||||
**Rebuild the baseline on every removal.** Deferred tools also have request declarations; deleting a tool changes those declarations regardless of its activation position. Addition-only routes cannot express that deletion in history. The rebuild therefore also covers removal-only updates and removals of later-activated tools. A past reset cannot force every future addition to become immediate: each reset establishes a new baseline from which deferred additions resume.
|
||||
|
||||
**Reject same-name schema changes during tool registration.** This restriction is not enforced. Immediate failure for a plugin registering an incompatible tool requires comparing the new registration with historical definitions of tools called on each affected current surface, while excluding calls removed by compaction. That registration-time historical and scope-aware check is deferred because of its implementation complexity. Same-name schema changes remain allowed; they rebuild declarations and invalidate prefix reuse even on `in-history` routes.
|
||||
|
||||
## Consequences
|
||||
@@ -34,4 +36,6 @@ Tool lists and logical update events are capability-independent; only outgoing d
|
||||
|
||||
The authored [headless prompt/tool scenario](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml) pins an appended prompt beside a deferred tool addition and verifies that the first request's messages stay unchanged. It shares the stable-prompt scenario's tool lifecycle and schema expectations.
|
||||
|
||||
The [DeepSeek replacement scenario](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml) checks the first additions to an empty set, a rebuilt baseline, a subsequent deferred addition, and removal of that later-activated tool through Messages serialization. Explicit deferred-only requests remain unchanged under the [DeepSeek failure policy](../simplification/2026-11-02-deepseek-deferred-tool-failures.md). These checks establish request construction, not measured cache savings from rebuilding.
|
||||
|
||||
The [real DeepSeek SDK test](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) measures the first request after a native tool addition with both unchanged and simultaneously updated instructions. Unique user history prevents a shared system-prompt cache hit from satisfying the preceding-input assertion. Removal checks tool availability without requiring cache retention; each case disables retries.
|
||||
|
||||
@@ -14,9 +14,9 @@ Status: implemented
|
||||
|
||||
`Session.toolHistory()` 缓存对已提交请求头和 developer 消息的归约,不依赖模型能力。首次访问恢复继承事件;后续读取仅处理新增事件。不可变快照携带初始声明和有序更新标识,添加定义通过历史请求头解析。显式新序列和保留定义变化会重建声明。以未变化的定义恢复保留名称时序列继续:投影保持声明不变,由记录的添加在先前移除之后重新提供该工具,因此移除不会禁用已声明的工具,加载模式也不会改写先前声明。
|
||||
|
||||
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。仅保留当前序列的更新标识。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。显式延迟加载在支持的路由上仍然可用。
|
||||
循环和压缩请求通过 `GenerateOptions.toolHistory` 携带此快照。适配器分发时,`projectToolUpdates` 使用已准备路由的 `toolUpdate`。不支持的路由接收有效定义,不携带 `deferLoading` 或 developer 消息。支持的路由延迟历史添加;`in-history` 保留已移除定义和移除块,`addition-only` 则省略两者。在 addition-only 路由移除任何工具时,使用该次更新后的状态重建路由基线:即使被移除工具是后来激活的,请求声明也已发生变化,保留已被取代的激活块无法保留完整对话前缀。不存在任何声明时添加工具,也会重建基线。延迟声明也计入已有工具。首个原生工具会改变空声明前缀;将其延迟加载并不比直接声明具有缓存优势。此前已激活工具变为立即加载;没有激活记录的显式延迟声明仍保持延迟。后续添加在记录的激活位置前保持延迟加载。已被取代的工具块会被省略,但不会丢弃这些消息中的其他内容。Session 归约和持久事件仍不依赖模型能力。缺少历史或请求前缀遗漏已记录更新时,使用当前声明且不发送 developer 更新。
|
||||
|
||||
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 不声明模式。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
|
||||
DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `tool_removal` 块以及 `defer_loading` 声明,在存在更新块时发送工具变更 beta 请求头。默认 `deepseek-flash` 条目声明 `addition-only`;pi-ai 为兼容的 Anthropic 路由选择 `in-history`,为兼容的 Chat Completions 和 Responses 路由选择 `addition-only`。Chat 和 Trajectory 将 developer 消息显示为上下文节点,工具变更使用本地化通知:单项变更显示工具名,多项变更显示数量及可展开的名称列表。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
@@ -26,6 +26,8 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
|
||||
|
||||
**每次请求扫描完整日志。** 缓存归约处理新事件,为普通和辅助调用提供稳定快照,不增加任意同步历史读取接口。
|
||||
|
||||
**每次移除都重建基线。** 延迟工具也具有请求声明;删除工具会改变这些声明,与其激活位置无关。addition-only 路由无法通过历史表达删除。因此,仅移除的更新和移除后来激活的工具也会重建基线。一次历史重置不能让所有后续添加都变为立即加载:每次重置确立新基线,后续添加继续采用延迟加载。
|
||||
|
||||
**在工具注册时拒绝同名 schema 变化。** 当前不执行此限制。要让注册不兼容工具的插件立即收到异常,需要将新注册与各受影响当前 surface 中已调用工具的历史定义比较,同时排除已被压缩移除的调用。由于实现复杂,注册时结合历史和作用域的检查留待后续实现。同名 schema 变化仍被允许;它们会重建声明,即使在 `in-history` 路由上也会使前缀复用失效。
|
||||
|
||||
## Consequences
|
||||
@@ -34,4 +36,6 @@ DeepSeek 把投影后的更新转换为 system 角色的 `tool_addition` 和 `to
|
||||
|
||||
手写的[无界面提示词/工具场景](../../../../snapshots/session/dynamic-tool-prompt-updates/snapshot.yml)钉住追加提示词与延迟工具添加同时发生的行为,并验证首次请求的消息保持不变。它复用稳定提示词场景的工具生命周期和 schema 期望值。
|
||||
|
||||
[DeepSeek 替换场景](../../../../snapshots/session/deepseek-deferred-tools/snapshot.yml)通过 Messages 序列化验证向空工具集首次添加工具、重建后的基线、后续延迟添加,以及移除该后来激活的工具。根据 [DeepSeek 失败策略](../simplification/2026-11-02-deepseek-deferred-tool-failures.zh.md),显式全延迟请求保持原样。这些检查验证请求构造,不代表已经测量重建带来的缓存收益。
|
||||
|
||||
[真实 DeepSeek SDK 测试](../../../../apps/cli/tests/profiles/sdk/dynamic-tool-cache.e2e.ts) 分别在指令不变和指令同时更新时测量原生工具新增后的首次请求。独有的用户历史确保仅命中共享系统提示词缓存无法满足前序输入断言。移除场景检查工具可用性,不要求保留缓存;每个用例均禁用重试。
|
||||
|
||||
+8
-8
@@ -6,14 +6,14 @@
|
||||
en: 9163af91ea0e030d
|
||||
zh: 92e44043b999d32f
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/problem:
|
||||
en: 6fac3c73a4415c83
|
||||
zh: f65b61cc1705348b
|
||||
en: 32ba2b2b42e8deb5
|
||||
zh: 6a6532561f81b8e2
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/decision:
|
||||
en: 458416ed7e47f644
|
||||
zh: 8b4dd820228e9dfd
|
||||
en: 08a54b37e157da29
|
||||
zh: 65f633055d9df242
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/alternatives-considered:
|
||||
en: b731254983be656d
|
||||
zh: 5dee2b09f7faa537
|
||||
en: 6da221f6b821ea44
|
||||
zh: b15774011abed7ed
|
||||
/agent-note-offer-web-search-and-web-fetch-only-while-the-selected-provider-is-ready/consequences:
|
||||
en: 798faeaa0b406eac
|
||||
zh: 97bbfdaebc84b1d4
|
||||
en: c90a122c1f33bb83
|
||||
zh: 0175ca0f599d086b
|
||||
|
||||
+8
-8
@@ -6,17 +6,17 @@ English | [中文](2026-09-27-web-search-offered-by-provider-readiness.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-web` registers `web_search` whenever search is enabled, and the [web capability seam decision](2026-06-24-web-capability-seam.md) keeps it visible while the selected provider cannot run. The shipped search provider, `dsh-web-search-deepseek`, authenticates a search with the DeepSeek account token only for a model request on the `deepseek-account` route; every other route needs a configured API key. A Session on an API-key route without a search key was therefore offered a tool whose every call failed with `WEB_PROVIDER_CREDENTIAL_MISSING`. Sessions in one Host can use different routes, a Session can switch routes between requests, and the Web app registers `web_search` once per agent preset, so no registration decision matches every request. The Search and Fetch APIs of `dsh-web-deepseek-api`, which the Web search settings switch selects together, need that credential for both operations, so the same Session was also offered a `web_fetch` whose every call failed.
|
||||
`dsh-tool-web` registers `web_search` whenever search is enabled, and the [web capability seam decision](2026-06-24-web-capability-seam.md) keeps it visible while the selected provider cannot run. The DeepSeek web providers authenticate an operation with the DeepSeek account token only for a model request on the `deepseek-account` route; every other route needs a configured API key. Without a readiness check, a Session on an API-key route without a key is offered a tool whose every call fails with `WEB_PROVIDER_CREDENTIAL_MISSING`. Sessions in one Host can use different routes, a Session can switch routes between requests, and the Web app registers `web_search` once per agent preset, so no registration decision matches every request. The Fetch API of `dsh-web-deepseek-api`, which the user selects separately from the search provider ([provider selection](2026-10-29-web-provider-selection.md)), needs the same credential, so such a Session with it selected would also be offered a `web_fetch` whose every call fails.
|
||||
|
||||
## Decision
|
||||
|
||||
Each role of the web capability seam owns one part of a per-step check:
|
||||
|
||||
- **Service Definition.** `WebSearchProvider` and `WebFetchProvider` have an optional `ready(llmProvider)` that answers whether an operation made for a model request on that chat route has a credential to send. It checks configuration only and makes no network calls. `ctx.web.searchReady(llmProvider?)` and `ctx.web.fetchReady(llmProvider?)` resolve the provider with the same rules as `search()` and `fetch()` and return its answer. A provider without `ready()` counts as ready, and a selection failure answers true because the operation reports that failure as a structured `WebError`.
|
||||
- **Service Provider.** `dsh-web-search-deepseek` answers true when the route is `deepseek-account` and `ctx.deepseekAccount.resolveToken()` returns a token for the configured search endpoint, when the section carries a literal `apiKey`, or when `ctx.credentials.describe()` reports the `apiKeyEnv` reference configured (without that service, when the launching environment carries it). It checks credentials in the order `search()` resolves them, and a failed account-token or credential-store read answers false, because `search()` fails on that read instead of trying the next credential. The Search and Fetch API providers of `dsh-web-deepseek-api` share one credential and answer the same way for their API base; their transport repeats these checks, so neither package depends on the other. The providers name no tool or prompt section.
|
||||
- **Service Provider.** Each DeepSeek provider checks its credentials in the order its operation resolves them. `dsh-web-search-deepseek` checks, on the `deepseek-account` route, whether `ctx.deepseekAccount.resolveToken()` returns a token for its Messages endpoint, and then its literal `apiKey` or its `apiKeyEnv` reference. The Search and Fetch API providers of `dsh-web-deepseek-api` resolve the same plan as the operation ([credential resolution](2026-10-29-web-provider-selection.md)): a capability with its own YAML fields checks only the key under its `*ApiKeyEnv`; otherwise, on the account route with the account model route mounted, a token for that route's `/v1/tools` base counts, and then the key of the `deepseek-official` route, or `DEEPSEEK_API_KEY` without that route. A key counts when `ctx.credentials.describe()` reports its reference configured (without that service, when the launching environment carries it); the [shared credential helpers](../../../../packages/llm/llm-deepseek/src/credential.ts) perform these presence checks. A failed account-token or credential-store read answers false, because the operation fails on that read instead of trying the next credential. The providers name no tool or prompt section.
|
||||
- **Consumer.** `dsh-tool-web` registers `web_search` and `web_fetch` with [step activation conditions](../../../../packages/core/tools/README.md#conditional-tools) that ask `ctx.web.searchReady()` and `ctx.web.fetchReady()` about the route of the step's model selection. The agent loop resolves conditions before it assembles the step, so the declared tools, the `tool:web_search` and `tool:web_fetch` prompt sections, PTC program bindings, and dispatch all read the same admitted set.
|
||||
|
||||
The route decides only whether the account token is checked before the API key, which happens on the `deepseek-account` route; an API key counts on every route, so a conversation on another provider's model is offered the DeepSeek web tools whenever a DeepSeek key is configured. The providers do not compare the chat route's endpoint with their own: each operation sends the configured key, `DEEPSEEK_API_KEY` by default, to the configured endpoint, even when the official route chats through a gateway.
|
||||
The route decides only whether the account token is checked before the API key, which happens on the `deepseek-account` route; an API key counts on every route, so a conversation on another provider's model is offered the DeepSeek web tools whenever a DeepSeek key is configured. A Search or Fetch API capability with its own YAML fields ignores the route and is ready exactly when its own key is configured.
|
||||
|
||||
Only presence decides: no API key value is read and no endpoint is contacted, so a wrong key or an endpoint that does not serve the native `web_search_20250305` server tool keeps the tool admitted. A step that does not admit a web tool also refuses calls to it as `UNKNOWN_TOOL`, so a model that repeats an earlier call reaches no provider; an operation made outside a model step resolves its credential when it runs and fails with `WEB_PROVIDER_CREDENTIAL_MISSING` when there is none.
|
||||
|
||||
@@ -24,7 +24,7 @@ The request header records the assembled tools, so the agent loop announces each
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Let the DeepSeek provider remove `web_search` in its own assemble listener.** The seam stays unchanged, but the provider then names another package's tool, prompt section, and PTC transport, and it removes the tool even when the deployment selects another search provider, such as Exa, whose searches would succeed.
|
||||
**Let the DeepSeek provider remove `web_search` in its own assemble listener.** The seam stays unchanged, but the provider then names another package's tool, prompt section, and PTC transport, and it removes the tool even when the deployment selects another search provider, such as one a plugin registers, whose searches would succeed.
|
||||
|
||||
**Extend `available()`.** It is synchronous and takes no request, while this check reads an asynchronous credential store and depends on the request's route. A route-dependent `available()` would also make provider selection depend on the conversation.
|
||||
|
||||
@@ -36,9 +36,9 @@ The request header records the assembled tools, so the agent loop announces each
|
||||
|
||||
**Offer search only on the DeepSeek account route.** Account tokens are issued only for the official endpoint, so a gateway key could never reach DeepSeek. But every shipped profile defaults to the `deepseek-official` route, so search would be off by default, including the official API-key case.
|
||||
|
||||
**Withhold the tools from other providers' conversations unless search has a key of its own.** A search from another model's conversation bills the DeepSeek key, but DeepSeek search is one of the services that key pays for, and a user who configured it expects search to work whatever model chats. The Web search settings page saves its key under `apiKeyEnv`, `DEEPSEEK_API_KEY` by default, which the chat route also reads, so a key entered there could never serve another model; a `routes` setting that opens them would be one more thing a user has to find.
|
||||
**Withhold the tools from other providers' conversations unless search has a key of its own.** A search from another model's conversation bills the DeepSeek key, but DeepSeek search is one of the services that key pays for, and a user who configured it expects search to work whatever model chats. The Search and Fetch APIs send the chat route's own key, and the settings page saves the legacy Messages search key under `apiKeyEnv`, `DEEPSEEK_API_KEY` by default, which the chat route also reads, so withholding them would leave a configured key unable to serve another model; a `routes` setting that opens them would be one more thing a user has to find.
|
||||
|
||||
**Compare the official chat route's endpoint with the search endpoint.** Withholding the tools or the key when the two endpoints differ in origin keeps a gateway's key away from DeepSeek, but the providers would have to find the chat route's configuration themselves. Taking the first active `dsh-llm-deepseek-api-key` Loader row answers for an unrelated instance when a composition isolates several, which disables a correct route or lets a gateway key through; only the route's owner knows which endpoint serves a request, and an endpoint field in `dsh-llm` would reach the browser through the Remote `listProviders()`. The check would also withhold search from users who chat through a proxy or development endpoint with a key the official search endpoint accepts. Without it, a gateway user's search fails at the configured endpoint.
|
||||
**Compare the official chat route's endpoint with the web endpoint and withhold the tools when they differ.** It would keep a gateway's key away from DeepSeek, but it would also withhold web tools from users who chat through a proxy or development endpoint with a key the web endpoint accepts. The Search and Fetch APIs instead derive their endpoint from the origin of the route whose key they send, so the key and endpoint come from one route; legacy Messages search keeps its own endpoint, where a gateway user's search fails.
|
||||
|
||||
**Keep the tool offered and fail at execution.** The model spends a call on a search that cannot authenticate, and the failure repeats on every attempt.
|
||||
|
||||
@@ -46,6 +46,6 @@ The request header records the assembled tools, so the agent loop announces each
|
||||
|
||||
## Consequences
|
||||
|
||||
`dsh-web` gains public members: the optional `ready()` of both provider interfaces, `WebRuntime.searchReady()`, and `WebRuntime.fetchReady()`. Each readiness method is the smallest query its one consumer needs, which is how the [observation-surface decision](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) lets a status query return; neither reports availability or selection failures. The Exa, Perplexity, and HTTP fetch providers implement no `ready()`, so their behavior is unchanged, and `web_fetch` stays admitted whenever the switch leaves HTTP fetch selected. A step that does not admit a web tool leaves it out of PTC program bindings too. An endpoint that cannot serve the native server tool keeps the tool admitted and fails each call. Removing a tool on an unchanged route changes the declared tools and misses the prompt cache once; a model switch already invalidates it. Searches and fetches from other providers' conversations bill the configured DeepSeek key, and a deployment whose official route chats through a gateway sends the gateway's key to the configured endpoint, the official one by default, where each operation fails unless that endpoint accepts the key. The DeepSeek web packages read no Loader rows and depend on no LLM package.
|
||||
`dsh-web` gains public members: the optional `ready()` of both provider interfaces, `WebRuntime.searchReady()`, and `WebRuntime.fetchReady()`. Each readiness method is the smallest query its one consumer needs, which is how the [observation-surface decision](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) lets a status query return; neither reports availability or selection failures. The HTTP fetch provider implements no `ready()`, so `web_fetch` stays admitted whenever HTTP fetch is selected. A step that does not admit a web tool leaves it out of PTC program bindings too. An endpoint that cannot serve the native server tool keeps the tool admitted and fails each call. Removing a tool on an unchanged route changes the declared tools and misses the prompt cache once; a model switch already invalidates it. Searches and fetches from other providers' conversations bill the configured DeepSeek key. A deployment whose official route chats through a gateway sends the gateway's key to that gateway's origin with `/v1/tools` for the Search and Fetch APIs, and to the configured Messages endpoint, the official one by default, for legacy Messages search, where each search fails unless that endpoint accepts the key. The Search and Fetch APIs read the DeepSeek model routes' Loader entries through the model provider directory.
|
||||
|
||||
Unit tests pin each role: `packages/web/web/tests/web.spec.ts` covers which provider `searchReady()` and `fetchReady()` ask and their answer on selection failure, `packages/web/web-search-deepseek/tests/deepseek.spec.ts` and `packages/web/web-deepseek-api/tests/readiness.spec.ts` cover each credential case of `ready()` on the account route and on other routes, and `packages/web/tool-web/tests/tool-web.spec.ts` resolves step admission of both tools through the real prompt, tool, and executor services, including the `UNKNOWN_TOOL` refusal of a call in a step that withheld the tool. In real compositions, the shipped headless keyless smoke test and the `web-search-unconfigured` recorded-session snapshot, whose search key references are unset, pin the withheld search tool. Snapshot replays supply a placeholder DeepSeek key, so their tool lists do not depend on the runner's environment; with it, the `pi-ai-conversation-updates` conversation on a pi-ai route keeps `web_search`.
|
||||
Unit tests pin each role: [web.spec.ts](../../../../packages/web/web/tests/web.spec.ts) covers which provider `searchReady()` and `fetchReady()` ask and their answer on selection failure, [deepseek.spec.ts](../../../../packages/web/web-search-deepseek/tests/deepseek.spec.ts) and [credential-resolution.spec.ts](../../../../packages/web/web-deepseek-api/tests/credential-resolution.spec.ts) cover each credential case of `ready()` on the account route and on other routes, including readiness that matches each operation, and [tool-web.spec.ts](../../../../packages/web/tool-web/tests/tool-web.spec.ts) resolves step admission of both tools through the real prompt, tool, and executor services, including the `UNKNOWN_TOOL` refusal of a call in a step that withheld the tool. In real compositions, the shipped headless keyless smoke test and the `web-search-unconfigured` recorded-session snapshot, whose search key references are unset, pin the withheld search tool. Snapshot replays supply a placeholder DeepSeek key, so their tool lists do not depend on the runner's environment; with it, the `pi-ai-conversation-updates` conversation on a pi-ai route keeps `web_search`.
|
||||
|
||||
+8
-8
@@ -6,17 +6,17 @@ Status: implemented
|
||||
|
||||
## Problem
|
||||
|
||||
`dsh-tool-web` 在启用搜索时总会注册 `web_search`,而 [web 能力 seam 决策](2026-06-24-web-capability-seam.zh.md)让它在选中的提供方无法运行时仍然可见。发行版的搜索提供方 `dsh-web-search-deepseek` 只对 `deepseek-account` 路由上的模型请求使用 DeepSeek 账号 token 鉴权;其他路由都需要已配置的 API 密钥。因此,API 密钥路由上没有搜索密钥的会话会获得一个每次调用都以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败的工具。同一 Host 中的会话可以使用不同路由,一个会话可以在请求之间切换路由,而 Web 应用按 agent preset 只注册一次 `web_search`,因此任何注册层面的决定都无法与每个请求相符。Web 搜索设置开关会同时选中 `dsh-web-deepseek-api` 的 Search 与 Fetch API,两个操作都需要这份凭据,因此同一会话还会获得一个每次调用都失败的 `web_fetch`。
|
||||
`dsh-tool-web` 在启用搜索时总会注册 `web_search`,而 [web 能力 seam 决策](2026-06-24-web-capability-seam.zh.md)让它在选中的提供方无法运行时仍然可见。DeepSeek Web 提供方只对 `deepseek-account` 路由上的模型请求使用 DeepSeek 账号 token 为操作鉴权;其他路由都需要已配置的 API 密钥。没有就绪检查时,API 密钥路由上没有密钥的会话会获得一个每次调用都以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败的工具。同一 Host 中的会话可以使用不同路由,一个会话可以在请求之间切换路由,而 Web 应用按 agent preset 只注册一次 `web_search`,因此任何注册层面的决定都无法与每个请求相符。`dsh-web-deepseek-api` 的 Fetch API 由用户与搜索提供方分别选择([提供方选择](2026-10-29-web-provider-selection.zh.md)),它需要同一份凭据,因此这样的会话选中它时,还会获得一个每次调用都失败的 `web_fetch`。
|
||||
|
||||
## Decision
|
||||
|
||||
web 能力 seam 的每个角色各负责逐步骤检查的一部分:
|
||||
|
||||
- **Service Definition。** `WebSearchProvider` 与 `WebFetchProvider` 有一个可选的 `ready(llmProvider)`,回答为该对话路由上的模型请求发起的操作是否有可发送的凭据。它只检查配置,不发起网络调用。`ctx.web.searchReady(llmProvider?)` 与 `ctx.web.fetchReady(llmProvider?)` 按与 `search()`、`fetch()` 相同的规则解析提供方并返回其回答。没有 `ready()` 的提供方视为就绪;选择失败时回答 true,因为对应操作会把该失败报告为结构化 `WebError`。
|
||||
- **Service Provider。** `dsh-web-search-deepseek` 在以下任一情况下回答 true:路由为 `deepseek-account` 且 `ctx.deepseekAccount.resolveToken()` 为配置的搜索端点返回 token;配置段带有字面 `apiKey`;或 `ctx.credentials.describe()` 报告 `apiKeyEnv` 引用已配置(没有该服务时,启动环境中存在该变量)。它按 `search()` 解析凭据的顺序检查凭据,读取账号 token 或凭据存储失败时回答 false,因为 `search()` 会在该次读取上失败,而不会尝试下一种凭据。`dsh-web-deepseek-api` 的 Search 与 Fetch API 提供方共用一份凭据,并对其 API 基础地址以同样方式回答;它们的传输层重复了这些检查,因此两个包互不依赖。提供方不点名任何工具或提示词段落。
|
||||
- **Service Provider。** 每个 DeepSeek 提供方都按其操作解析凭据的顺序检查凭据。`dsh-web-search-deepseek` 在 `deepseek-account` 路由上检查 `ctx.deepseekAccount.resolveToken()` 是否为其 Messages 端点返回 token,然后检查其字面 `apiKey` 或 `apiKeyEnv` 引用。`dsh-web-deepseek-api` 的 Search 与 Fetch API 提供方解析与操作相同的方案([凭据解析](2026-10-29-web-provider-selection.zh.md)):在 YAML 中设置了单独字段的能力只检查其 `*ApiKeyEnv` 下的密钥;否则,在账号路由上且账号模型路由已挂载时,该路由 `/v1/tools` 基础地址的 token 算数,然后是 `deepseek-official` 路由的密钥,没有该路由时为 `DEEPSEEK_API_KEY`。`ctx.credentials.describe()` 报告其引用已配置时(没有该服务时,启动环境中存在该变量),密钥即算存在;这些存在性检查由[共享凭据函数](../../../../packages/llm/llm-deepseek/src/credential.ts)执行。读取账号 token 或凭据存储失败时回答 false,因为操作会在该次读取上失败,而不会尝试下一种凭据。提供方不点名任何工具或提示词段落。
|
||||
- **Consumer。** `dsh-tool-web` 注册 `web_search` 与 `web_fetch` 时各带一个[步骤激活条件](../../../../packages/core/tools/README.zh.md#conditional-tools),以该步骤模型选择的路由询问 `ctx.web.searchReady()` 与 `ctx.web.fetchReady()`。agent loop 在组装该步骤之前解析各条件,因此声明的工具、`tool:web_search` 与 `tool:web_fetch` 提示词段落、PTC 程序绑定与派发都读取同一个准入集合。
|
||||
|
||||
路由只决定是否在 API 密钥之前检查账号 token,这只发生在 `deepseek-account` 路由上;API 密钥在任何路由上都算数,因此只要配置了 DeepSeek 密钥,使用其他提供方模型的对话也会获得 DeepSeek Web 工具。提供方不比较聊天路由的地址与自己的端点:即使官方路由经网关聊天,每次操作也把配置的密钥(默认 `DEEPSEEK_API_KEY`)发往配置的端点。
|
||||
路由只决定是否先检查账号 token 再检查 API 密钥,这发生在 `deepseek-account` 路由上;API 密钥在每条路由上都算数,因此只要配置了 DeepSeek 密钥,使用其他提供方模型的对话也会获得 DeepSeek Web 工具。在 YAML 中设置了单独字段的 Search 或 Fetch API 能力不看路由,恰在其自身密钥已配置时就绪。
|
||||
|
||||
只看凭据是否存在:不读取 API 密钥的值,也不访问任何端点,因此错误的密钥,或不提供原生 `web_search_20250305` 服务器工具的端点,仍会让该工具保持准入。未准入某个 Web 工具的步骤也会以 `UNKNOWN_TOOL` 拒绝对它的调用,因此模型重复先前的调用也到达不了任何提供方;在模型步骤之外发起的操作在运行时解析凭据,没有凭据时以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败。
|
||||
|
||||
@@ -24,7 +24,7 @@ web 能力 seam 的每个角色各负责逐步骤检查的一部分:
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**让 DeepSeek 提供方在自己的 assemble 监听器中移除 `web_search`。** seam 保持不变,但提供方因此要点名另一个包的工具、提示词段落与 PTC 传输工具;而且即使部署选择了另一个搜索成功的提供方(例如 Exa),它也会移除该工具。
|
||||
**让 DeepSeek 提供方在自己的 assemble 监听器中移除 `web_search`。** seam 保持不变,但提供方因此要点名另一个包的工具、提示词段落与 PTC 传输工具;而且即使部署选择了另一个搜索成功的提供方(例如插件注册的提供方),它也会移除该工具。
|
||||
|
||||
**扩展 `available()`。** 它是同步的且不接收请求,而这项检查要读取异步凭据存储,并取决于请求的路由。依赖路由的 `available()` 还会让提供方选择取决于对话。
|
||||
|
||||
@@ -36,9 +36,9 @@ web 能力 seam 的每个角色各负责逐步骤检查的一部分:
|
||||
|
||||
**只在 DeepSeek 账号路由上提供搜索。** 账号 token 只对官方端点签发,因此网关的密钥不可能到达 DeepSeek。但所有发行 profile 的默认路由都是 `deepseek-official`,搜索会因此默认关闭,连官方 API 密钥的情况也一并关闭。
|
||||
|
||||
**除非搜索有自己的密钥,否则不向其他提供方的对话提供这些工具。** 其他模型对话中的搜索会由 DeepSeek 密钥计费,但 DeepSeek 搜索本就是该密钥付费使用的服务之一,配置了它的用户也期望无论用哪个模型对话,搜索都能用。Web 搜索设置页把密钥存在 `apiKeyEnv` 指向的引用下,默认是聊天路由也在读取的 `DEEPSEEK_API_KEY`,因此在那里输入的密钥永远无法服务其他模型;再加一个开放这些路由的 `routes` 配置项,只会让用户多找一样东西。
|
||||
**除非搜索有自己的密钥,否则不向其他提供方的对话提供这些工具。** 其他模型对话中的搜索会由 DeepSeek 密钥计费,但 DeepSeek 搜索本就是该密钥付费使用的服务之一,配置了它的用户也期望无论用哪个模型对话,搜索都能用。Search 与 Fetch API 发送聊天路由自己的密钥,设置页把旧版 Messages 搜索的密钥存在 `apiKeyEnv` 指向的引用下,默认是聊天路由也在读取的 `DEEPSEEK_API_KEY`,因此撤下工具会让已配置的密钥无法服务其他模型;再加一个开放这些路由的 `routes` 配置项,只会让用户多找一样东西。
|
||||
|
||||
**比较官方聊天路由的端点与搜索端点。** 两个端点不同源时撤下工具或不发送密钥,能让网关的密钥远离 DeepSeek,但提供方得自己找出聊天路由的配置。取第一条激活的 `dsh-llm-deepseek-api-key` Loader 行时,如果组合隔离了多个实例,回答的可能是无关的实例,从而关掉正确的路由,或放过网关的密钥;只有路由的所有者知道哪个端点服务某个请求,而在 `dsh-llm` 加地址字段会让它经 Remote 方法 `listProviders()` 进入浏览器。这项检查还会让经代理或开发端点对话、而密钥能被官方搜索端点接受的用户失去搜索。不做这项检查时,网关用户的搜索会在配置的端点上失败。
|
||||
**比较官方聊天路由的端点与 Web 端点,二者不同时撤下工具。** 这能让网关的密钥远离 DeepSeek,但也会让经代理或开发端点对话、而密钥能被 Web 端点接受的用户失去网页工具。Search 与 Fetch API 改为从其所发送密钥的路由的 origin 推导端点,因此密钥与端点来自同一条路由;旧版 Messages 搜索保留自己的端点,网关用户的搜索会在那里失败。
|
||||
|
||||
**保持工具可见,在执行时失败。** 模型会把一次调用花在无法鉴权的搜索上,并且每次尝试都会重复失败。
|
||||
|
||||
@@ -46,6 +46,6 @@ web 能力 seam 的每个角色各负责逐步骤检查的一部分:
|
||||
|
||||
## Consequences
|
||||
|
||||
`dsh-web` 增加以下公共成员:两个提供方接口上可选的 `ready()`、`WebRuntime.searchReady()` 与 `WebRuntime.fetchReady()`。每个就绪方法都是其唯一消费方所需的最小查询,这正是[观测接口决策](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)允许状态查询回归的方式;它们既不报告可用性,也不报告选择失败。Exa、Perplexity 与 HTTP 抓取提供方没有实现 `ready()`,其行为不变;只要开关让 HTTP 抓取保持选中,`web_fetch` 就始终准入。未准入某个 Web 工具的步骤,其 PTC 程序绑定中也没有它。无法提供原生服务器工具的端点仍会让工具保持准入,并使每次调用失败。在路由不变时移除工具会改变声明的工具,并使提示词缓存失效一次;模型切换本身已经使缓存失效。其他提供方对话中的搜索与抓取由配置的 DeepSeek 密钥计费;官方路由经网关对话的部署会把网关的密钥发往配置的端点(默认是官方端点),除非该端点接受这个密钥,否则每次操作都会失败。DeepSeek Web 包不读取 Loader 的行,也不依赖任何 LLM 包。
|
||||
`dsh-web` 增加以下公共成员:两个提供方接口上可选的 `ready()`、`WebRuntime.searchReady()` 与 `WebRuntime.fetchReady()`。每个就绪方法都是其唯一消费方所需的最小查询,这正是[观测接口决策](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)允许状态查询回归的方式;它们既不报告可用性,也不报告选择失败。HTTP 抓取提供方没有实现 `ready()`,因此只要选中 HTTP 抓取,`web_fetch` 就始终准入。未准入某个 Web 工具的步骤,其 PTC 程序绑定中也没有它。无法提供原生服务器工具的端点仍会让工具保持准入,并使每次调用失败。在路由不变时移除工具会改变声明的工具,并使提示词缓存失效一次;模型切换本身已经使缓存失效。其他提供方对话中的搜索与抓取由配置的 DeepSeek 密钥计费。官方路由经网关对话的部署,对 Search 与 Fetch API 会把网关的密钥发往该网关 origin 加 `/v1/tools`,对旧版 Messages 搜索则发往配置的 Messages 端点(默认是官方端点),除非该端点接受这个密钥,否则每次搜索都会失败。Search 与 Fetch API 通过模型提供方目录读取 DeepSeek 模型路由的 Loader 条目。
|
||||
|
||||
单元测试分别固定每个角色:`packages/web/web/tests/web.spec.ts` 覆盖 `searchReady()` 与 `fetchReady()` 询问哪个提供方以及选择失败时的回答,`packages/web/web-search-deepseek/tests/deepseek.spec.ts` 与 `packages/web/web-deepseek-api/tests/readiness.spec.ts` 覆盖 `ready()` 在账号路由与其他路由上的每种凭据情况,`packages/web/tool-web/tests/tool-web.spec.ts` 通过真实的提示词、工具与执行器服务解析两个工具的步骤准入,包括在撤下该工具的步骤中调用它时以 `UNKNOWN_TOOL` 拒绝。在真实组合中,发行版 headless 无密钥冒烟测试与 `web-search-unconfigured` 录制会话快照的搜索密钥引用都未设置,二者固定了搜索工具的撤下行为。快照回放提供占位 DeepSeek 密钥,因此其工具列表不依赖运行环境;借助该密钥,`pi-ai-conversation-updates` 中 pi-ai 路由上的对话保留 `web_search`。
|
||||
单元测试分别固定每个角色:[web.spec.ts](../../../../packages/web/web/tests/web.spec.ts) 覆盖 `searchReady()` 与 `fetchReady()` 询问哪个提供方以及选择失败时的回答,[deepseek.spec.ts](../../../../packages/web/web-search-deepseek/tests/deepseek.spec.ts) 与 [credential-resolution.spec.ts](../../../../packages/web/web-deepseek-api/tests/credential-resolution.spec.ts) 覆盖 `ready()` 在账号路由与其他路由上的每种凭据情况,包括就绪判断与每次操作一致,[tool-web.spec.ts](../../../../packages/web/tool-web/tests/tool-web.spec.ts) 通过真实的提示词、工具与执行器服务解析两个工具的步骤准入,包括在撤下该工具的步骤中调用它时以 `UNKNOWN_TOOL` 拒绝。在真实组合中,发行版 headless 无密钥冒烟测试与 `web-search-unconfigured` 录制会话快照的搜索密钥引用都未设置,二者固定了搜索工具的撤下行为。快照回放提供占位 DeepSeek 密钥,因此其工具列表不依赖运行环境;借助该密钥,`pi-ai-conversation-updates` 中 pi-ai 路由上的对话保留 `web_search`。
|
||||
|
||||
+6
-6
@@ -9,11 +9,11 @@
|
||||
en: 47ba548b869be190
|
||||
zh: c2e5038a56fe741c
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/decision:
|
||||
en: be0d17bfbd1b59d4
|
||||
zh: 7a23bccdf95c21ff
|
||||
en: a6f9ae8a53af5e4b
|
||||
zh: 0aece791574ef060
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/alternatives-considered:
|
||||
en: e7c66403fab991d2
|
||||
zh: 1580ec29116c7d42
|
||||
en: 12e0f5fc9638acc6
|
||||
zh: 24213550da27b70b
|
||||
/agent-note-asynchronous-projection-registration-and-retained-readers/consequences:
|
||||
en: 31ce6f02dd3bf39d
|
||||
zh: bcd29b2146f689b5
|
||||
en: f5c294608d87844f
|
||||
zh: 24488e439a04d81a
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Host readers coupled to registry key lookup lose access when a plugin unloads, a
|
||||
|
||||
`SessionProjectionRegistry.register` returns an awaitable registration for a typed handle. Domain owners await it during activation before exposing dependent operations. Function plugins retain their namespace exports and use asynchronous `apply`; services use their initialization hook. Each domain service keeps its definitions and handles internal and exposes methods returning business data. Consumers depend on that service rather than registering its definitions or receiving projection readers. This supersedes the optional first-access acquisition and disposer-return details in [the mandatory-reader decision](2026-08-19-session-projection-mandatory-seam.md), while retaining its mandatory-state and cropped-client-view rules.
|
||||
|
||||
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives, snapshots, and checkpoints. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
|
||||
A handle owns one reference to a shared definition and its per-Session cells. Its disposer releases global participation exactly once; the final release removes the unit from live drives and snapshots. Checkpoints keep each Session's last cell of the released unit until its key registers again: the registry no longer advances that cell, while a retained handle read still advances it forward, so a writer that runs after the owner unloads still stores the state already folded. Under the Cordis rc.11 teardown order, Web-layer plugins release their units before the Sessions they describe detach, and the final projection-cache write would otherwise drop those rows. Stored rows can therefore lag the log; the cache promises only possibly stale rows. Its reader remains usable after release, folding unseen events with the original definition. A later registration at a different state version does not replace an older handle's definition. Registration checks owner liveness after its asynchronous handoff and rejects if the owner unloaded before receiving the handle.
|
||||
|
||||
AgentRegistry owns turn and step state and exposes `turnBoundaryOf(session)`. AgentLoop acquires its inbox handle once before publishing its factory, reads the last turn before creating an Agent scope, and passes an inbox callback plus that turn number into synchronous Agent construction. The callback retains its handle through cleanup after the loop fiber removes global registrations. This preserves the existing creation/setup sequence without per-Agent registration or an extra initialization state.
|
||||
|
||||
@@ -20,6 +20,10 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Detach owned Sessions before unregistering.** Other Web-layer plugins unload in earlier fibers under the rc.11 teardown order, and plugin reload or disable reaches the same timing, so ordering one owner cannot keep every row.
|
||||
|
||||
**Checkpoint a released unit only while its cell is at the Session cursor.** A Session with a running turn at shutdown appends teardown events after its units are released, so its rows would be dropped again.
|
||||
|
||||
**Keep registry-level lookup and delay unregistering.** This couples unrelated plugin disposal order to every reader's final access. Retained readers make cleanup independent of that order.
|
||||
|
||||
**Return a synchronous handle.** This preserves callers' immediate reads but commits the acquisition API to synchronous initialization. Awaited acquisition leaves that timing explicit while state reads remain synchronous after readiness.
|
||||
@@ -36,4 +40,4 @@ Plugins compiled against the synchronous API keep running during migration. The
|
||||
|
||||
## Consequences
|
||||
|
||||
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
|
||||
Projection owners must migrate acquisition and retain handles; other consumers use the owning services. Released handles retain their definitions and weakly keyed cells for as long as the caller keeps them; they do not restore global wire participation. The registry also retains a released predecessor, with its definition closures and weakly keyed cells, until the same key registers again, so a key that a reloaded plugin renames or stops registering keeps the old definition referenced for the process lifetime. Registration-reader tests cover post-release folding, version replacement, unrelated-unit isolation, repeated disposal, concurrent owner disposal, and owner unload during acquisition. Agent lifecycle tests cover cleanup failures, scope-minting races, and background child shutdown. Compatibility tests and a real Loader subprocess verify the deprecated calls.
|
||||
|
||||
+6
-2
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
`SessionProjectionRegistry.register` 返回可等待的注册结果,用于获取类型化句柄。领域所有者在激活期间等待它,再提供依赖操作。函数插件保留命名空间导出并使用异步 `apply`;服务使用初始化钩子。领域服务在内部持有定义和句柄,通过方法返回业务数据。消费方依赖该服务,无需注册其定义或接收投影读取句柄。这取代了[必需读取方决策](2026-08-19-session-projection-mandatory-seam.zh.md)中的可选首次访问获取方式与返回释放函数的细节,同时保留必需状态和裁剪 Client 视图的规则。
|
||||
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进、快照和检查点中移除单元。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
句柄拥有共享定义及其逐 Session 单元的一次引用。其释放函数恰好一次地解除全局参与;最后一次释放会从实时推进和快照中移除单元。检查点仍保留每个 Session 中已释放单元的最后一个 cell,直到它的 key 再次注册:注册表不再推进这个 cell,但已保留的 handle 读取时仍会将其向前推进,因此 owner 卸载后才运行的写入方仍会存下已经折叠出的状态。按 Cordis rc.11 的卸载顺序,Web 层插件会在它们所描述的 Session detach 之前释放单元,否则 projection cache 的最终写入会丢掉这些行。因此存储的行可能落后于日志;cache 只承诺行可能过时。读取句柄在释放后仍可使用原定义折叠未读取事件。后续注册的其他状态版本不会替换旧句柄的定义。注册在异步交接后检查 owner 是否存活;如果 owner 在收到句柄前卸载,注册拒绝。
|
||||
|
||||
AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`。AgentLoop 在发布工厂前只获取一次 inbox 句柄,在创建 Agent 作用域前读取最后轮次,并将 inbox 回调与该轮次编号传入同步 Agent 构造过程。loop fiber 移除全局注册后,回调仍在清理期间保留其句柄。这保留既有创建与 setup 顺序,无需逐 Agent 注册或增加初始化状态。
|
||||
|
||||
@@ -20,6 +20,10 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**卸载前先 detach 自己拥有的 Session。** 按 rc.11 的卸载顺序,其他 Web 层插件在更早卸载的 fiber 里,插件重载或禁用也会碰上同样的时机,调整单个 owner 的顺序保不住所有行。
|
||||
|
||||
**只在已释放单元的 cell 位于 Session 游标时才写入检查点。** 关停时正在运行一轮的会话会在单元释放后追加 teardown 事件,因此它的这些行会再次被丢掉。
|
||||
|
||||
**保留注册表级查询并延迟注销。** 这使无关插件的释放顺序依赖每个读取方的最后一次访问。保留读取句柄使清理不再依赖该顺序。
|
||||
|
||||
**返回同步句柄。** 这保留消费者的立即读取,却将获取 API 固定为同步初始化。等待获取过程使时序显式,同时就绪后的状态读取仍同步。
|
||||
@@ -36,4 +40,4 @@ AgentRegistry 持有轮次和步骤状态,并提供 `turnBoundaryOf(session)`
|
||||
|
||||
## Consequences
|
||||
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
投影所有者必须迁移获取方式并保留句柄;其他消费方使用所属服务。只要调用方持有已释放句柄,其定义和弱键单元就仍存在;句柄不会恢复全局 wire 参与。注册表自身也会保留已释放的前任及其定义闭包和弱键单元,直到同一 key 再次注册;因此重载后改名或不再注册的 key 会在进程生命周期内一直引用旧定义。注册读取测试覆盖释放后折叠、版本替换、无关单元隔离、重复释放、并发 owner 释放以及获取期间 owner 卸载。Agent 生命周期测试覆盖清理失败、作用域创建竞态与后台子 Agent 停止。兼容测试与真实 Loader 子进程验证这些已弃用调用。
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 9f84c9bd85b6aace
|
||||
zh: 8ebb10962c219624
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/decision:
|
||||
en: 6e8f43d7a53c35fd
|
||||
zh: a6e2bccc20474515
|
||||
en: aada3ca481120bff
|
||||
zh: 1fa45a276a47e921
|
||||
/agent-note-serve-session-list-rows-from-a-summary-key-set/alternatives-considered:
|
||||
en: ce8320f1032018ed
|
||||
zh: 4c5fad5e605bf524
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ A Session-list row carries every registered client view. Four of those views are
|
||||
The Session summary is every registered client view except the keys whose `SessionProjectionMap` entry carries the summary-exclusion mark.
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) declares the type-only mark `SessionProjectionSummaryExcluded<T>` over a unique symbol that is never exported and never present at runtime. `SessionProjectionViewValue<T>` strips it distributively, so a nullable entry keeps its `null`; `SessionProjectionSummaryExcludedKey` tests the non-nullish part; `SessionProjectionSummaryValues` is every view value minus those keys.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), and `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Each owning package wraps only its own map entry: `turnOutline` (session-turn-outline), `sessionStats` (session-stats), `todos` (tool-todo), `imageLimits` (session-controller's boot-constant attachment limits, which every Session shares), and `contextPressure` and `contextBreakdown` (token-meter). The todo entry is `SessionProjectionSummaryExcluded<TodoItem[]> | null`, because an intersection with `null` would absorb the null member.
|
||||
- Registration carries the same decision as a plain boolean: `wire.summary` says whether a Session summary includes the unit, and omitting it means `true`. A key whose map entry carries the mark declares `false` so the runtime key set and the client view type agree. The mark itself has no runtime representation — `wire.view` and `viewSchema` are typed with `SessionProjectionViewValue`, which strips it, so the erase in `register` only widens the state type.
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) exposes `summaryKeys(): ReadonlySet<key>`, derived from the live registrations and rebuilt only when a new key registers or a key's last registrant disposes. `cachedSnapshot`, `viewCheckpoint`, and `snapshot` accept `ProjectionKeySelection` (`readonly Key[] | ReadonlySet<Key>`) and use a passed set as-is.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live and cold cached faces. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) forwards that selection unchanged, and [`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) passes `summaryKeys()` to the live registry's `snapshot` and to the cold cache read. `cachedPredecessorTitle` keeps its title-only read.
|
||||
- Client types only: `SessionSummary.projectionValues` narrows to `Partial<SessionProjectionSummaryValues>`, while `SessionProjectionSnapshot.values` keeps every view value. No wire field, function, or UI name changes, and an opened Session still receives every key through `session/follow`, the control baseline, and `session.projections`.
|
||||
|
||||
Estimated effect: with logs whose outlines dominate the projection payload, the four keys are estimated at roughly two thirds of a Session-list response for a few hundred Sessions — the outline alone near 57%, the figures and todo list near 5% together, and the shared image limits near 5% — and at roughly 85% of the same response after transport compression, where the outline's unique text barely compresses while the remaining keys do.
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ Session 列表行携带每个已注册的客户端视图。其中四个视图从
|
||||
Session 摘要就是每个已注册客户端视图,减去 `SessionProjectionMap` 条目带摘要排除标记的键。
|
||||
|
||||
- [`packages/session/session-projection/src/types.ts`](../../../../packages/session/session-projection/src/types.ts) 声明类型层标记 `SessionProjectionSummaryExcluded<T>`,它基于一个从不导出、运行时从不存在的 unique symbol。`SessionProjectionViewValue<T>` 以分发方式去掉标记,可空条目因此保留 `null`;`SessionProjectionSummaryExcludedKey` 取非空部分判断;`SessionProjectionSummaryValues` 是全部视图值减去这些键。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo),以及 `imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 每个 owning package 只包装自己的 map 条目:`turnOutline`(session-turn-outline)、`sessionStats`(session-stats)、`todos`(tool-todo)、`imageLimits`(session-controller 的启动常量附件上限,每个 Session 共享),以及 `contextPressure` 和 `contextBreakdown`(token-meter)。todo 条目写作 `SessionProjectionSummaryExcluded<TodoItem[]> | null`,因为与 `null` 求交会吞掉 null 成员。
|
||||
- 注册用普通布尔值承载同一决定:`wire.summary` 表示 Session 摘要是否包含该单元,省略即 `true`。map 条目带标记的 key 声明 `false`,让运行时键集合与客户端视图类型一致。标记本身没有运行时表示 —— `wire.view` 与 `viewSchema` 的签名已用 `SessionProjectionViewValue` 去标记,因此 `register` 里的擦除只是放宽状态类型。
|
||||
- [`packages/session/session-projection/src/index.ts`](../../../../packages/session/session-projection/src/index.ts) 提供 `summaryKeys(): ReadonlySet<key>`,从实时注册派生,只有新键注册或某键最后一个注册者注销时才重建。`cachedSnapshot`、`viewCheckpoint`、`snapshot` 接受 `ProjectionKeySelection`(`readonly Key[] | ReadonlySet<Key>`),传入 Set 时直接使用。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给 live 与 cold 两个缓存读取面。`cachedPredecessorTitle` 保持只读 title。
|
||||
- [`packages/session/session-projection-cache/src/index.ts`](../../../../packages/session/session-projection-cache/src/index.ts) 原样转发该选择,[`packages/api/session-controller/src/list.ts`](../../../../packages/api/session-controller/src/list.ts) 把 `summaryKeys()` 传给实时注册表的 `snapshot` 与冷会话的缓存读取。`cachedPredecessorTitle` 保持只读 title。
|
||||
- 客户端只有类型变化:`SessionSummary.projectionValues` 收窄为 `Partial<SessionProjectionSummaryValues>`,而 `SessionProjectionSnapshot.values` 保留全部视图值。wire 字段、函数名与 UI 名称均不变;打开的 Session 仍通过 `session/follow`、控制基线与 `session.projections` 收到每个键。
|
||||
|
||||
估算效果:当 outline 在投影负载中占主导时,四个键估计占几百个 Session 时列表响应的约三分之二 —— outline 单项约 57%,统计与 todo 合计约 5%,共享的图片上限约 5% —— 占传输压缩后同一响应的约 85%,因为 outline 的唯一文本几乎无法压缩,而其余键可以。
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# Bilingual-pair consistency record for 2026-10-29-web-provider-selection.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-10-29-web-provider-selection.md
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers:
|
||||
en: 2a2fa1def8a81625
|
||||
zh: 317d8b115f34e677
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/problem:
|
||||
en: 3f5dacf86015cebb
|
||||
zh: cf0d5bceb9941554
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision:
|
||||
en: 0682ebef17f6a5e7
|
||||
zh: 6ea399459262eb0c
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision/registered-providers-are-observable-availability-is-not:
|
||||
en: 38ce796dea91fa35
|
||||
zh: d312a9cd29fd5cc9
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision/independent-selection:
|
||||
en: 9d68ffca5c8be681
|
||||
zh: 36bb661f98de0ebb
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision/credential-resolution:
|
||||
en: 32f59c35d71a4832
|
||||
zh: e9288313bb18b78d
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision/defaults-and-stored-selections:
|
||||
en: 5fcd3319fbceed1d
|
||||
zh: 20771646b9c36fa6
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/decision/legacy-messages-search:
|
||||
en: 2c7c84f8fa1e8745
|
||||
zh: 5d0244e4f17ba12a
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/alternatives-considered:
|
||||
en: 0c227e270894cd71
|
||||
zh: 114484a3e69f5c0a
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/consequences:
|
||||
en: 50f046bf81a7783d
|
||||
zh: 5c0e27bdcaf9f12e
|
||||
/agent-note-web-provider-selection-independent-menus-over-registered-providers/verification:
|
||||
en: 26ade0695094e9bc
|
||||
zh: 7b265c43b1886da5
|
||||
@@ -0,0 +1,85 @@
|
||||
# Agent Note: Web provider selection - independent menus over registered providers
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-29-web-provider-selection.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The web service selects search and fetch providers through two independent fields, `web.searchProvider` and `web.fetchProvider`. A settings control that writes both at once ties fetch billing and target access policy to the search choice, and a control that offers only the DeepSeek choices it knows cannot select a provider that another plugin registers. A picker over registered providers needs the Host to report them, and the [web capability seam](2026-06-24-web-capability-seam.md) has no availability observation for it to reuse. Messages search starts an auxiliary model request for each query, whereas the [Search API](../feature/2026-10-15-deepseek-search-fetch-api.md) returns structured sources directly. The Search and Fetch APIs also need a DeepSeek credential, and a credential setting of their own makes every user reconcile it with the model provider they chat with.
|
||||
|
||||
## Decision
|
||||
|
||||
Users choose search and fetch providers independently from the Host's registered provider lists. The shipped composition selects the Search API for search and local HTTP fetch for fetch, and the DeepSeek Search and Fetch APIs follow the conversation's DeepSeek credential.
|
||||
|
||||
### Registered providers are observable; availability is not
|
||||
|
||||
`WebSearchProvider` and `WebFetchProvider` carry an optional `title`, the provider's display name, and an optional localized `description` of what the provider suits and what it costs or cannot do. `ctx.web.listProviders()` returns `{ search, fetch }`, each a registration-ordered list of `{ id, title?, description? }` entries, and the `web` Remote namespace exposes it as `web/listProviders`. Every registration and disposal emits the payload-free `web/providers-changed` event after the registry changes; the web service contains listener failures, so an observer cannot veto a registry change. [dsh-api-remotes](../../../../packages/api/remotes/README.md) mounts the namespace and forwards the event, and Clients re-read the list after each notification and reconnect.
|
||||
|
||||
The lists include providers whose `available()` answers false. A picker shows what can be pinned, including a provider whose credential the user has not configured yet, whereas availability depends on per-request facts such as the Session's model route and credential state. Availability therefore stays an execution result: `search()` and `fetch()` resolve the configured provider at call time and throw the structured `WebError`, as the seam note records. The tool layer still never enumerates providers.
|
||||
|
||||
### Independent selection
|
||||
|
||||
The Web search and fetch page writes `web.searchProvider` from its Search menu and `web.fetchProvider` from its Fetch menu. Each menu lists every registered provider for its capability by `title` (or `id` when `title` is absent), so plugin providers appear without page changes, and a selected id that the Host does not register stays visible as such. Official providers register no `title`; the page names them from its own locale dictionary and lists them first in a fixed order: the Search API (`deepseek-search-api`) followed by Messages search (`deepseek-official`) for search, and local HTTP fetch (`http`) followed by the Fetch API (`deepseek-fetch-api`) for fetch. Other providers follow in Host registration order. Selecting the deployment's composed provider clears the stored override instead of storing a duplicate value. The [settings page README](../../../../packages/client/ui-settings-web-search/README.md) owns the page's controls.
|
||||
|
||||
Every registered provider is listed; a provider has no flag that keeps it out of the menus. This note supersedes the [Search and Fetch API decision](../feature/2026-10-15-deepseek-search-fetch-api.md)'s rejection of listing every registered provider in a settings dropdown. Independent menus must offer providers that other plugins register, and the display metadata that listing needs is only the optional `title` and `description`. The description lives on the registration rather than in the page's dictionaries so that official and plugin providers explain themselves through one field, and a new provider needs no page change to describe its trade-offs. The rejected generic editor still stands: the page edits only the DeepSeek providers' settings, and other providers keep their own settings pages.
|
||||
|
||||
The Search and Fetch APIs read one `web-deepseek-api` namespace, and each resolves its own credential and endpoint, as the next section records. Messages search reads `web-search-deepseek` for its independent endpoint and key reference.
|
||||
|
||||
### Credential resolution
|
||||
|
||||
Search and Fetch follow the DeepSeek credential of the conversation, and the settings page shows no credential control for them. The conversation's route is the `provider` of the initiating Session's latest request context, which account sign-out also reads; `ready()` uses the route of the model request being prepared, so readiness and execution resolve the same plan.
|
||||
|
||||
On the `deepseek-account` route, with an active, valid `@deepseek-ai/dsh-llm-deepseek-account` Loader entry at the entry root that the model provider directory lists, the operation sends only the account token that `ctx.deepseekAccount` releases for the dispatch endpoint under that route's origin with `/v1/tools`; an HTTP 401 invalidates that token. Every other operation, including one in an account conversation without a sign-in or a token, sends the key under the `apiKeyEnv` of the active `@deepseek-ai/dsh-llm-deepseek-api-key` entry for `deepseek-official`, resolved with that package's own resolver (including `DEEPSEEK_BASE_URL`), to the origin of that route's base URL with `/v1/tools`; without that entry, `DEEPSEEK_API_KEY` goes to `https://api.deepseek.com/v1/tools`. Sending the API key in a signed-out account conversation keeps web tools available there. A user with only `DEEPSEEK_API_KEY`, including every headless, SDK, and ACP run, therefore searches and fetches by default, and a gateway or internal deployment set through the model provider's base URL serves web access at the same origin. A missing key makes the capability not ready, and an operation fails with `WEB_PROVIDER_CREDENTIAL_MISSING` and guidance naming the Models page, the reference, and the account sign-in. A mounted route entry whose settings its resolver rejects is not treated as absent: an operation that needs that route is not ready and fails with `WEB_PROVIDER_MISCONFIGURED` without reading a credential or sending the request, so a broken gateway setting never redirects requests to the official endpoint.
|
||||
|
||||
A capability whose YAML sets a non-empty `searchBaseURL` or `searchApiKeyEnv` (`fetchBaseURL` or `fetchApiKeyEnv` for Fetch) instead always sends the key under its `*ApiKeyEnv` (`DEEPSEEK_API_KEY` when unset) to its `*BaseURL` (the official base when unset) and never the account token, so an intranet or third-party endpoint receives only the key chosen for it. Each capability has its own fields because a deployment can serve search and fetch from different gateways. These fields are YAML-only, and an empty value counts as unset. Stored values of fields outside the schema, including `baseURL`, `apiKeyEnv`, and `apiKey`, load without schema errors and have no effect.
|
||||
|
||||
The [credential helpers](../../../../packages/llm/llm-deepseek/src/credential.ts) of `@deepseek-ai/dsh-llm-deepseek` read and validate API keys, check key presence, and release account tokens for the DeepSeek model routes, Messages search, and the Search and Fetch APIs, so every DeepSeek request reads the credential store the same way; the account model route, Messages search, and the Search and Fetch APIs also invalidate the token that an HTTP 401 rejects. Shipped profiles need no per-profile credential override, because the API-key path works wherever `DEEPSEEK_API_KEY` or the DeepSeek API-key route has a key.
|
||||
|
||||
### Defaults and stored selections
|
||||
|
||||
The [base composition](../../../../packages/bundle/base/cordis.patch.yml) sets `searchProvider: deepseek-search-api` and `fetchProvider: http`. Search defaults to the Search API because it returns sources without Harness starting an auxiliary model request. Fetch defaults to local HTTP fetch because it has no per-request API charge, applies Harness's own URL validation and resource limits, and reaches intranet and local addresses. The Fetch API remains one menu choice away.
|
||||
|
||||
A stored selection, whether in the settings document or in a deployment's own composition, wins over the shipped default, and an upgrade does not rewrite it. A selection is never replaced at execution: when the selected provider is missing, unavailable, or fails, the operation fails with its structured error and does not fall back to another search or fetch provider. A fallback would send the query and credential to an endpoint the user did not choose and change billing without notice.
|
||||
|
||||
### Legacy Messages search
|
||||
|
||||
The DeepSeek Messages search provider remains available as **DeepSeek (legacy)**. A stored `web.searchProvider: deepseek-official` (or `DSH_WEB_SEARCH_PROVIDER=deepseek-official`) selects it, so users whose gateways serve Messages search but not `/v1/tools` can retain their working provider.
|
||||
|
||||
The Messages provider owns and writes `web/deepseek-search-llm-request`. The declaration and payload retain compatibility with released Sessions; the Search and Fetch API package neither declares nor writes this auxiliary event.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep one switch for both APIs.** It lets Fetch API adoption need no configuration edit, but it ties fetch billing and target access policy to the search choice, and it cannot express a provider that another plugin registers.
|
||||
|
||||
**Filter the lists by availability, or add availability to each entry.** A filtered list hides a provider while its credential is missing, right when the user needs to choose and configure it. Availability also depends on the request's model route and credential state, so a listed boolean could disagree with execution. The archived [observation-surface decision](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md) allows only the smallest signal a consumer reads; the menus read registration only.
|
||||
|
||||
**Keep a fixed provider catalog in the Client.** The Client would show ids the Host does not register as normal choices and would never show a provider that a plugin registers at runtime.
|
||||
|
||||
**Carry the provider list in the change event.** A payload-free event leaves one read path for initial load, notifications, and reconnects, so the Client never has to order an event payload against a concurrent read.
|
||||
|
||||
**Keep display names in the Client or in a separate Host registry.** The registering plugin knows its provider's name; a separate map would let a provider and its label register or dispose independently.
|
||||
|
||||
**Fall back to another provider when the selected one fails.** A fallback hides gateway and credential misconfiguration, sends the query to a second service, and changes what the request costs without the user choosing it.
|
||||
|
||||
**Remove Messages search and let a stored `deepseek-official` select the Search API.** Gateways can support Messages search without Search API routes. Retaining the legacy provider preserves that choice instead of changing its protocol and billing.
|
||||
|
||||
**Default fetch to the Fetch API as well.** It adds a per-request charge to every fetch and delegates page retrieval and target access policy to the service, which fetches from its own side rather than from the user's machine.
|
||||
|
||||
**Choose a credential source per capability on the settings page**, from a menu listing the configured DeepSeek model providers plus a custom key and base URL, without fallback between sources. The visible setting would state the whole rule, but the configuration surface is too large for users to understand: each capability adds a credential menu that the user must keep in step with the model provider they chat with, and a signed-in user chatting on the account route gets no web tools until they change it. Following the conversation's credential needs no setting, and the YAML-only own fields cover endpoints that differ from the model provider.
|
||||
|
||||
**Share one custom key and base URL between Search and Fetch.** It cannot express a deployment that serves the two APIs from different gateways.
|
||||
|
||||
**Fail an account conversation without a sign-in instead of sending the API key.** It would keep the account route from ever billing the API key, but signed-out account conversations would lose web tools even when a DeepSeek API key is configured.
|
||||
|
||||
**Send the account token whenever the user is signed in, whatever the conversation's route.** It would bill web access to the account during conversations on the API-key route or on another provider, which the user did not choose for them.
|
||||
|
||||
## Consequences
|
||||
|
||||
Users without a stored search selection search through the Search API: their queries go to the Search endpoint the conversation's DeepSeek credential selects, cost a Search API request each, and send up to 128 characters of the latest human prompt as `ori_question` unless `sendOriginalQuestion` is off. Deployments whose gateway has no `/v1/tools` routes get search failures until they select Messages search or set `searchBaseURL` to a base that serves them. A stored `deepseek-official` selection uses Messages search. Users without a stored fetch selection fetch through local HTTP fetch. A signed-in user without an API key has DeepSeek web search only in conversations on the account route; the [upgrade guide](../../../../docs/upgrade-guide/v0.2.2-alpha.2/web-search-credential-modes/guide.md) lists each affected setup.
|
||||
|
||||
A plugin provider, including one written in creator mode, appears in the matching menu as soon as it registers, under its `title`. The menus can offer a provider that cannot run for the current request; the failure appears when a search or fetch runs. Each registry change costs one forwarded event and one list read in each Client that observes it.
|
||||
|
||||
## Verification
|
||||
|
||||
[web.spec.ts](../../../../packages/web/web/tests/web.spec.ts) covers list contents and order, titles, events after registration and disposal, no event for a rejected duplicate, and contained listener failures. [deepseek.spec.ts](../../../../packages/web/web-search-deepseek/tests/deepseek.spec.ts) covers the independent Messages provider, and [credential-resolution.spec.ts](../../../../packages/web/web-deepseek-api/tests/credential-resolution.spec.ts) covers, for both capabilities, the account token on the account route and its invalidation after HTTP 401, the API-key fallback without a sign-in, the route key and endpoint in official, non-DeepSeek, and Session-less conversations, `DEEPSEEK_API_KEY` and the official base without that route, YAML-only own fields that never send the token, independent capabilities, readiness matching each operation, and ignored removed fields; [composition.spec.ts](../../../../packages/web/web-deepseek-api/tests/composition.spec.ts) boots the real model routes through the Loader. [remote-events.host.spec.ts](../../../../packages/api/remotes/tests/remote-events.host.spec.ts) covers event forwarding, and [base.spec.ts](../../../../packages/bundle/base/tests/base.spec.ts) pins the shipped defaults. The settings page's controller and card specs and the [plugin configuration browser scenario](../../../../apps/web/tests/plugin-config.e2e.ts) cover menu contents, independent Search and Fetch selection, the absence of credential controls for the DeepSeek APIs, and the settings each provider keeps.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Agent Note: Web 提供方选择——基于已注册提供方的独立下拉框
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-29-web-provider-selection.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
Web 服务通过 `web.searchProvider` 和 `web.fetchProvider` 两个独立字段分别选择搜索与抓取提供方。用一个设置控件同时写入两者,会把抓取的计费和目标访问策略绑在搜索选择上;只提供已知 DeepSeek 选项的控件,也无法选用其他插件注册的提供方。基于已注册提供方的选择器需要 Host 报告这些提供方,而 [Web 能力 seam](2026-06-24-web-capability-seam.zh.md) 没有可供复用的可用性观察接口。Messages 搜索每次查询都会发起一次辅助模型请求,而 [Search API](../feature/2026-10-15-deepseek-search-fetch-api.zh.md) 能直接返回结构化来源。Search 与 Fetch API 还需要 DeepSeek 凭据;为它们单独设置凭据,会让每个用户都要把它与自己对话所用的模型提供方对齐。
|
||||
|
||||
## 决策
|
||||
|
||||
用户从 Host 已注册的提供方列表中分别选择搜索和抓取提供方。内置组合将搜索设为 Search API,将抓取设为本地 HTTP 抓取;DeepSeek Search 与 Fetch API 跟随会话的 DeepSeek 凭据。
|
||||
|
||||
### 已注册的提供方可观察,可用性不可观察
|
||||
|
||||
`WebSearchProvider` 和 `WebFetchProvider` 带有可选的 `title`(提供方的显示名称)和可选的多语言 `description`(说明该提供方适合什么、有什么代价或做不到什么)。`ctx.web.listProviders()` 返回 `{ search, fetch }`,二者均为按注册顺序排列的 `{ id, title?, description? }` 列表,`web` Remote namespace 将其暴露为 `web/listProviders`。每次注册和注销都会在注册表变更后触发无载荷的 `web/providers-changed` 事件;Web 服务会隔离监听器异常,因此观察者无法阻断注册表变更。[dsh-api-remotes](../../../../packages/api/remotes/README.zh.md) 挂载该 namespace 并转发该事件,Client 在收到通知和重连后重新读取列表。
|
||||
|
||||
列表包含 `available()` 返回 false 的提供方。选择器展示的是可固定选用的项,包括用户尚未配置凭据的提供方;而可用性取决于会话的模型路由、凭据状态等单次请求条件。因此可用性仍是执行期结果:`search()` 和 `fetch()` 在调用时解析所配置的提供方并抛出结构化 `WebError`,与 seam 记录一致。工具层仍从不枚举提供方。
|
||||
|
||||
### 分别选择
|
||||
|
||||
「网页搜索与获取」页由 Search 下拉框写入 `web.searchProvider`,由 Fetch 下拉框写入 `web.fetchProvider`。每个下拉框以 `title`(缺省时用 `id`)列出该能力的所有已注册提供方,因此插件提供方无需修改页面即可出现,且 Host 未注册的已选 id 仍会照常显示。官方提供方不注册 `title`,由页面用自己的语言字典命名,并按固定顺序排在最前:搜索依次为 Search API(`deepseek-search-api`)与 Messages 搜索(`deepseek-official`),抓取依次为本地 HTTP 抓取(`http`)和 Fetch API(`deepseek-fetch-api`)。其他提供方按 Host 注册顺序排列。选择部署组合的默认提供方会清除已保存的覆盖值,而非冗余保存一份副本。[设置页 README](../../../../packages/client/ui-settings-web-search/README.zh.md) 负责页面控件说明。
|
||||
|
||||
所有已注册的提供方都会列出;提供方没有可以把自己排除在下拉框之外的标志。本 Note 取代了 [Search 与 Fetch API 决策](../feature/2026-10-15-deepseek-search-fetch-api.zh.md)对「在设置页用下拉框列出所有已注册的提供方」的否决。独立的下拉框必须提供其他插件注册的提供方,而列出提供方所需的展示元数据只是可选的 `title` 和 `description`。说明放在注册信息里而不是页面词典里,这样官方与插件提供方都通过同一个字段说明自己,新提供方无需修改页面即可说明其取舍。被否决的通用编辑表单仍然成立:该页面只编辑 DeepSeek 提供方的设置,其他提供方保留各自的设置页。
|
||||
|
||||
Search 与 Fetch API 读取同一个 `web-deepseek-api` namespace,并各自解析自己的凭据和端点,见下一节。Messages 搜索从 `web-search-deepseek` 读取独立的端点和密钥引用。
|
||||
|
||||
### 凭据解析
|
||||
|
||||
Search 与 Fetch 跟随会话的 DeepSeek 凭据,设置页不为它们显示凭据控件。会话的路由是发起会话最近一次请求上下文的 `provider`,账号退出登录读取的也是它;`ready()` 使用正在准备的模型请求的路由,因此就绪判断与执行解析出相同的方案。
|
||||
|
||||
在 `deepseek-account` 路由上,若模型提供方目录列出了入口根部活动且有效的 `@deepseek-ai/dsh-llm-deepseek-account` Loader 条目,操作只发送 `ctx.deepseekAccount` 为该路由 origin 加 `/v1/tools` 下的请求端点给出的账号令牌;HTTP 401 会作废该令牌。其他所有操作,包括账号会话中没有登录或没有令牌的操作,都把 `deepseek-official` 的活动 `@deepseek-ai/dsh-llm-deepseek-api-key` 条目的 `apiKeyEnv` 下的密钥(用该包自己的解析函数解析,包括 `DEEPSEEK_BASE_URL`)发往该路由基础地址的 origin 加 `/v1/tools`;没有该条目时,把 `DEEPSEEK_API_KEY` 发往 `https://api.deepseek.com/v1/tools`。未登录的账号会话发送 API 密钥,网页工具因此在其中仍然可用。因此只有 `DEEPSEEK_API_KEY` 的用户(包括所有 headless、SDK 与 ACP 运行)默认即可搜索和抓取,通过模型提供方基础地址设置的网关或内部部署也在同一 origin 上提供 Web 访问。缺少密钥时,该能力未就绪,操作以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败,指引信息点名模型页面、该引用和账号登录。已挂载、但其解析函数拒绝其设置的路由条目不视为不存在:需要该路由的操作未就绪,并以 `WEB_PROVIDER_MISCONFIGURED` 失败,不读取凭据、不发送请求,因此网关设置出错时请求不会被改发到官方端点。
|
||||
|
||||
在 YAML 中设置了非空 `searchBaseURL` 或 `searchApiKeyEnv`(Fetch 为 `fetchBaseURL` 或 `fetchApiKeyEnv`)的能力,则始终把其 `*ApiKeyEnv`(未设置时为 `DEEPSEEK_API_KEY`)下的密钥发往其 `*BaseURL`(未设置时为官方基础地址),从不发送账号令牌,因此内网或第三方端点只收到为它选择的密钥。每个能力有自己的字段,因为一个部署可以由不同网关提供搜索和抓取。这些字段只能在 YAML 中设置,空值视为未设置。schema 之外字段的已保存值,包括 `baseURL`、`apiKeyEnv` 和 `apiKey`,可以加载而不产生 schema 错误,且不生效。
|
||||
|
||||
`@deepseek-ai/dsh-llm-deepseek` 的[凭据函数](../../../../packages/llm/llm-deepseek/src/credential.ts)为 DeepSeek 模型路由、Messages 搜索以及 Search 与 Fetch API 读取并校验 API 密钥、检查密钥是否存在、取得账号令牌,因此每个 DeepSeek 请求都以同一方式读取凭据库;账号模型路由、Messages 搜索以及 Search 与 Fetch API 还会作废被 HTTP 401 拒绝的令牌。内置配置不需要按 profile 覆盖凭据,因为只要 `DEEPSEEK_API_KEY` 或 DeepSeek API 密钥路由有密钥,API 密钥路径就能工作。
|
||||
|
||||
### 默认值与已保存的选择
|
||||
|
||||
[base 组合](../../../../packages/bundle/base/cordis.patch.yml)设置 `searchProvider: deepseek-search-api` 和 `fetchProvider: http`。搜索默认使用 Search API,因为它返回来源时无需 Harness 发起辅助模型请求。抓取默认使用本地 HTTP 抓取,因为它没有按次的 API 费用,执行 Harness 自身的 URL 校验和资源限制,并且可以访问内网和本地地址。Fetch API 仍可在下拉框中直接选择。
|
||||
|
||||
已保存的选择——无论位于设置文档还是部署自身的组合中——优先于内置默认值,升级不会改写它。执行时从不替换选择:所选提供方缺失、不可用或失败时,操作以结构化错误失败,不会回退到其他搜索或抓取提供方。回退会把查询和凭据发往用户未选择的端点,并在不提示的情况下改变计费。
|
||||
|
||||
### 旧版 Messages 搜索
|
||||
|
||||
DeepSeek Messages 搜索提供方仍以 **DeepSeek(旧版)**提供。已保存的 `web.searchProvider: deepseek-official`(或 `DSH_WEB_SEARCH_PROVIDER=deepseek-official`)会选中它,因此网关支持 Messages 搜索但不提供 `/v1/tools` 的用户可以保留可用的提供方。
|
||||
|
||||
Messages 提供方负责声明并写入 `web/deepseek-search-llm-request`。声明和载荷保持与已发布会话兼容;Search 与 Fetch API 包既不声明也不写入这一辅助事件。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留一个同时切换两个 API 的开关。** 它让使用 Fetch API 无需修改配置,但把抓取的计费和目标访问策略绑在搜索选择上,也无法表达其他插件注册的提供方。
|
||||
|
||||
**按可用性过滤列表,或在每个条目中加入可用性。** 过滤后的列表恰好会在凭据缺失时隐藏提供方,而这正是用户需要选择并配置它的时候。可用性还取决于请求的模型路由和凭据状态,列表中的布尔值可能与实际执行结果不一致。已归档的[观察面决策](../../archived/simplification/2026-07-04-drop-unconsumed-web-observation-surface.md)只允许引入消费方实际读取的最小信号;下拉框只读取注册情况。
|
||||
|
||||
**在 Client 中维护固定的提供方目录。** Client 会把 Host 未注册的 id 当作正常选项展示,且永远无法展示插件在运行时注册的提供方。
|
||||
|
||||
**在变更事件中携带提供方列表。** 无载荷事件让初次加载、变更通知和断线重连共用同一条读取路径,Client 无需为事件载荷与并发读取结果排序。
|
||||
|
||||
**把显示名称放在 Client 或单独的 Host 注册表中。** 注册插件最清楚自身提供方的名称;拆成独立映射会让提供方与其标签各自独立注册或注销。
|
||||
|
||||
**所选提供方失败时回退到其他提供方。** 回退会掩盖网关和凭据的配置错误,将查询发往第二个服务,并在未经用户选择的情况下改变请求的费用。
|
||||
|
||||
**移除 Messages 搜索,让已保存的 `deepseek-official` 选中 Search API。** 网关可能支持 Messages 搜索,却没有 Search API 路由。保留旧版提供方可以保留这一选择,而不改变其协议和计费。
|
||||
|
||||
**抓取也默认使用 Fetch API。** 它会给每次抓取增加按次费用,并把页面抓取和目标访问策略交给服务端,由服务端而不是用户的机器发起抓取。
|
||||
|
||||
**在设置页为每个能力选择一种凭据来源**,下拉框列出已配置的 DeepSeek 模型提供方,外加自定义密钥和基础地址,来源之间不回退。这样可见的设置就能说明全部规则,但配置面过大,用户难以理解:每个能力都多出一个凭据下拉框,用户必须让它与自己对话所用的模型提供方保持一致;在账号路由上对话的已登录用户在修改设置前没有网页工具。跟随会话的凭据不需要任何设置,与模型提供方不同的端点由只能在 YAML 中设置的单独字段覆盖。
|
||||
|
||||
**Search 与 Fetch 共用一个自定义密钥和基础地址。** 它无法表达由不同网关分别提供两个 API 的部署。
|
||||
|
||||
**账号会话未登录时直接失败,而不是发送 API 密钥。** 这样账号路由永远不会消耗 API 密钥,但即使配置了 DeepSeek API 密钥,未登录的账号会话也会失去网页工具。
|
||||
|
||||
**只要用户已登录就发送账号令牌,不论会话的路由。** 这会让在 API 密钥路由或其他提供方上进行的会话把 Web 访问记到账号上,而用户并没有为这些会话这样选择。
|
||||
|
||||
## 影响
|
||||
|
||||
没有已保存搜索选择的用户通过 Search API 搜索:查询发往会话的 DeepSeek 凭据所选的 Search 端点,每次计为一次 Search API 请求,并且除非关闭 `sendOriginalQuestion`,还会把最近一条用户提示的至多 128 个字符作为 `ori_question` 发送。网关没有 `/v1/tools` 路由的部署会遇到搜索失败,直到选择 Messages 搜索,或把 `searchBaseURL` 设为提供这些路由的基础地址。已保存的 `deepseek-official` 选择使用 Messages 搜索。没有已保存抓取选择的用户通过本地 HTTP 抓取。已登录但没有 API 密钥的用户只在账号路由上的会话中可以使用 DeepSeek 网页搜索;[升级指南](../../../../docs/upgrade-guide/v0.2.2-alpha.2/web-search-credential-modes/guide.zh.md)列出了每种受影响的配置。
|
||||
|
||||
插件提供方(包括在创作模式下编写的提供方)一经注册,就会以其 `title` 出现在对应的下拉框中。下拉框可能提供在当前请求中无法运行的提供方;失败会在执行搜索或抓取时出现。每次注册表变更会在每个观察它的 Client 中产生一次转发事件和一次列表读取。
|
||||
|
||||
## 验证
|
||||
|
||||
[web.spec.ts](../../../../packages/web/web/tests/web.spec.ts) 覆盖列表内容与顺序、标题、注册与撤销后的事件、重复注册被拒绝时不触发事件,以及监听器失败的隔离。[deepseek.spec.ts](../../../../packages/web/web-search-deepseek/tests/deepseek.spec.ts) 覆盖独立的 Messages 提供方,[credential-resolution.spec.ts](../../../../packages/web/web-deepseek-api/tests/credential-resolution.spec.ts) 针对两个能力覆盖账号路由上的账号令牌及其在 HTTP 401 后被作废、未登录时回退到 API 密钥、官方、非 DeepSeek 和会话之外的操作使用路由密钥与端点、没有该路由时使用 `DEEPSEEK_API_KEY` 与官方基础地址、从不发送令牌的 YAML 单独字段、两个能力相互独立、就绪判断与每次操作一致,以及被忽略的已移除字段;[composition.spec.ts](../../../../packages/web/web-deepseek-api/tests/composition.spec.ts) 通过 Loader 启动真实的模型路由。[remote-events.host.spec.ts](../../../../packages/api/remotes/tests/remote-events.host.spec.ts) 覆盖事件转发,[base.spec.ts](../../../../packages/bundle/base/tests/base.spec.ts) 固定内置默认值。设置页的 controller 与 card 测试以及[插件配置浏览器场景](../../../../apps/web/tests/plugin-config.e2e.ts)覆盖下拉框内容、Search 与 Fetch 的分别选择、DeepSeek API 不显示凭据控件,以及各提供方保留的设置。
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 47b44ed9313c2ccf
|
||||
zh: 7ad254f8ef88351d
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/alternatives-considered:
|
||||
en: d52fa1af379dea5b
|
||||
zh: 4e404994f7a607dc
|
||||
en: 3153c1e7563a3357
|
||||
zh: 9841db2b0156075b
|
||||
/agent-note-subagent-end-turn-guidance-stays-in-the-delegation-paragraph/verification:
|
||||
en: be076216381ea64d
|
||||
zh: 2da8d9819c20e2fe
|
||||
|
||||
@@ -29,7 +29,7 @@ The sentence stays one literal line. Rewording `the next result starts a new tur
|
||||
|
||||
**Move the guidance into the tool description.** The paragraph is assembled from the set of visible delegation tools and lists their names; the tool description describes one tool. Moving the sentence changes ownership without changing the behavior it prevents.
|
||||
|
||||
**Restore every prompt phrase reverted together with this sentence.** The read guidance and the `run_code` argument order are separate decisions with their own evidence; this note records only the delegation paragraph.
|
||||
**Restore every prompt phrase reverted together with this sentence.** The read guidance ([2026-10-26 note](2026-10-26-read-guidance-line-ranges.md)) and the `run_code` argument order are separate decisions with their own evidence; this note records only the delegation paragraph.
|
||||
|
||||
## Verification
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ Status: implemented
|
||||
|
||||
**把说明移进工具描述。** 这段指导按可见委派工具集合组装并列出工具名,工具描述只描述单个工具。移动这句会改变归属,但不改变它阻止的行为。
|
||||
|
||||
**把与这句一起被回退的所有提示词都恢复。** read 指导与 `run_code` 参数顺序是各自有证据的独立决定;本记录只覆盖委派段。
|
||||
**把与这句一起被回退的所有提示词都恢复。** read 指导([2026-10-26 note](2026-10-26-read-guidance-line-ranges.zh.md))与 `run_code` 参数顺序是各自有证据的独立决定;本记录只覆盖委派段。
|
||||
|
||||
## 验证
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# Bilingual-pair consistency record for 2026-10-26-read-guidance-line-ranges.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/bug-fix/2026-10-26-read-guidance-line-ranges.md
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges:
|
||||
en: 1926710614598539
|
||||
zh: b0106d4a6a2d648d
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges/problem:
|
||||
en: 4189d516419d8d12
|
||||
zh: 30d8496064dd44e3
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges/decision:
|
||||
en: 34f18307f7ad87af
|
||||
zh: 21c466c6ce6c598d
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges/alternatives-considered:
|
||||
en: aea7ffb1159ab81a
|
||||
zh: 4842ca79dc30654d
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges/verification:
|
||||
en: cbc16bfba4abe862
|
||||
zh: 9a71e6c020c01f69
|
||||
/agent-note-read-guidance-names-shell-readers-and-line-ranges/consequences:
|
||||
en: cf55161ea197a7a9
|
||||
zh: fa10ca413f8fd726
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Read guidance names shell readers and line ranges
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-10-26-read-guidance-line-ranges.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The read guidance named only `cat` as the shell command the read tool replaces, and described `offset`/`limit` as continuing a large file rather than reading a line range. #6057 recorded a long Session reading ranges with `sed -n X,Yp` 60 times. #6054 added `sed`, `head`, `tail`, and the line-range wording; a later prompt revert removed them together with unrelated changes.
|
||||
|
||||
The 2026-10-23 evaluation of the SDK profile (native tools, six benchmarks) measured this sentence as the only change: Pass@1 mean −0.03pp and Pass@3 mean +0.70pp over the five benchmarks that report Pass@3 (ProgramBench-New has none) versus the base commit, with mixed step and token deltas (ProgramBench-New −15.5 steps, Terminal-Bench-v4 +13.5 steps). The PTC mode runs did not include this variant; the section renders in every request where `read` is visible.
|
||||
|
||||
## Decision
|
||||
|
||||
Restore the #6054 wording, byte-identical: `Use the read tool — not shell commands like cat, sed, head, or tail — to inspect text files. Use offset and limit to read a line range or continue a large file.` The read tool and its guidance are owned by `@deepseek-ai/dsh-tool-fs`; write and edit guidance are unchanged.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Rely on the read tool schema.** The schema already documents `offset` and `limit`, but #6057 shows the model chose `sed` while that schema existed; the guidance is the only model-visible place that names `sed`, `head`, and `tail`.
|
||||
|
||||
**Restore every prompt phrase reverted together with this sentence.** The subagent end-turn sentence, the `run_code` argument order, the PTC syntax ownership, and the SDK persona are separate decisions; the first is already restored ([subagent end-turn note](2026-10-21-subagent-end-turn-guidance.md)), and the others carry their own evidence.
|
||||
|
||||
**Say "line range" without naming shell commands.** The observed failure was choosing shell commands, so the sentence names them.
|
||||
|
||||
## Verification
|
||||
|
||||
- `packages/fs/tool-fs/tests/tools.spec.ts` asserts the rendered guidance text.
|
||||
- Every recorded `system-prompt*.expected.md` sidecar that renders the read guidance carries the restored sentence: `test:snapshot` replays the session and SDK set, and `pnpm run test:web:ci` replays the `snapshots/web` set (the `node 24 / snapshots and artifacts` lane runs both). Two pinned historical sidecars (`web/cordis-tool-round`, `web/schedule-catalog`) retain their recorded text.
|
||||
- The evaluation above measured this single change on the native SDK profile.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Every request with the read tool carries three more command names and a line-range clause, about 10 tokens.
|
||||
- The model is directed to the read tool for line ranges instead of `cat`, `sed`, `head`, or `tail`.
|
||||
- The benchmark measured this sentence as neutral; keeping it rests on the #6057 production observation, not on a measured quality gain.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: read 指导点名 shell 读取命令与行区间
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-10-26-read-guidance-line-ranges.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
read 指导只把 `cat` 点名为 read 工具替代的 shell 命令,并把 `offset`/`limit` 描述成继续读大文件,而不是读行区间。#6057 记录了一个长 Session 用 `sed -n X,Yp` 读行区间 60 次。#6054 增加了 `sed`、`head`、`tail` 和行区间措辞;后来的一次提示词回退把它们和不相关的改动一起删掉了。
|
||||
|
||||
2026-10-23 对 SDK profile(native 工具、六个 benchmark)的评测里,这句是唯一改动:相对 base commit,Pass@1 均值 −0.03pp、Pass@3 均值 +0.70pp(该均值取报告 Pass@3 的五个 benchmark;ProgramBench-New 没有 Pass@3),步数与 token 变化方向不一(ProgramBench-New −15.5 步、Terminal-Bench-v4 +13.5 步)。PTC 模式的运行没有包含这个变体;只要 `read` 可见,这段就会进入每个请求。
|
||||
|
||||
## 决策
|
||||
|
||||
逐字恢复 #6054 的措辞:`Use the read tool — not shell commands like cat, sed, head, or tail — to inspect text files. Use offset and limit to read a line range or continue a large file.` read 工具及其指导归 `@deepseek-ai/dsh-tool-fs` 所有;write 与 edit 的指导不变。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**只依赖 read 工具 schema。** schema 已经写明 `offset` 与 `limit`,但 #6057 显示在那个 schema 存在时模型仍选择 `sed`;指导是唯一点名 `sed`、`head`、`tail` 的模型可见位置。
|
||||
|
||||
**把与这句一起被回退的所有提示词都恢复。** subagent 结束本轮句、`run_code` 参数顺序、PTC 语法归属、SDK persona 是各自独立的决定;第一项已经恢复([subagent 结束本轮 note](2026-10-21-subagent-end-turn-guidance.zh.md)),其余各有自己的证据。
|
||||
|
||||
**只说“行区间”,不点名 shell 命令。** 观测到的失败就是模型选择 shell 命令,所以这句点名它们。
|
||||
|
||||
## 验证
|
||||
|
||||
- `packages/fs/tool-fs/tests/tools.spec.ts` 断言渲染出的指导文本。
|
||||
- 所有渲染 read 指导的 `system-prompt*.expected.md` sidecar 都带有恢复后的句子:`test:snapshot` 回放 session 与 SDK 集合,`pnpm run test:web:ci` 回放 `snapshots/web` 集合(`node 24 / snapshots and artifacts` lane 两条都跑)。两个显式 pin 的历史 sidecar(`web/cordis-tool-round`、`web/schedule-catalog`)保留录制文本。
|
||||
- 上面的评测在 native SDK profile 上只改了这一句进行测量。
|
||||
|
||||
## 影响
|
||||
|
||||
- 只要 read 工具存在,每个请求多三个命令名和一句行区间说明,约 10 token。
|
||||
- 模型读行区间时被引导到 read 工具,而不是 `cat`、`sed`、`head`、`tail`。
|
||||
- benchmark 上这句测得为中性;保留它依据的是 #6057 的生产观测,而不是测到的质量收益。
|
||||
+1
-1
@@ -19,7 +19,7 @@
|
||||
zh: 993378f78f5a4120
|
||||
/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/two-driving-tiers:
|
||||
en: 0b9303333bf482ae
|
||||
zh: 6dfc68ad9e2924c3
|
||||
zh: 8a2ffe660bbe8730
|
||||
/agent-note-liveshare-sharing-a-dsh-session-with-a-colleague/decision/default-visibility:
|
||||
en: 754705367c350b31
|
||||
zh: e10b6541458c4a45
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ owner 在 DSH 里为某一个会话创建分享。分享发布到 Liveshare 服
|
||||
|
||||
每个分享带一个 `ViewerSessionMode`,只有两个值,且两档都会驱动共享会话:
|
||||
|
||||
- **view** —— 读者通过普通 prompt 路径驱动共享会话的 loop,在授予工作区的只读沙箱里运行,因此会话的工具绝不写文件。读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)、无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`)都限制在授权范围内:工具运行前检查其声明的每个路径,无法检查的调用一律拒绝。
|
||||
- **view** —— 读者通过普通 prompt 路径驱动共享会话的 loop,在授予工作区的只读沙箱里运行,因此会话的工具绝不写文件。读取与检索面(`read`、`read_image`、`glob`、`grep`、`present`)、无本地文件副作用的工具(`todo_write`、`skill`、`web_search`、`web_fetch`)都限制在授权范围内:工具运行前检查其声明的每个路径,无法检查的调用一律拒绝。两档也都准入 PTC 传输工具 `run_code`:程序运行在所有者自己的常驻文件策略下而不是分享档位下,因此可以触碰授权范围之外的文件,而其嵌套调用仍保留同样的守卫与路径检查;分享对话框会就此向所有者提示。
|
||||
- **write** —— 读者在受宿主写入上限约束的可写沙箱里驱动同一个 loop,沙箱也会修改授予工作区内的文件。
|
||||
|
||||
沙箱安装在会话上,但只约束分享查看者发起的轮次:`agent/pre-step` 记录一个已准入轮次的进入输入是否带有 share-viewer principal,守卫与替代定义对该轮次的每次执行读取这一结论。所有者自己发起的轮次保留会话自己的工具;所有者在查看者轮次中追加的输入仍留在该受限轮次内。该标记只存在于 Host 内存,按轮次重算,不写入会话日志。所有分享停止或全部过期后执行层被释放,会话恢复自己的工具。
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: 344bcc7e9825bbc8
|
||||
zh: c1d0f4b54f5b3786
|
||||
/agent-note-change-request-panel-and-worktree-sessions/decision:
|
||||
en: 6a0dc836205863ba
|
||||
zh: 3d4feb0385953dda
|
||||
en: 66bde24e7c601eb6
|
||||
zh: 5091591f3dc1ac85
|
||||
/agent-note-change-request-panel-and-worktree-sessions/deferred:
|
||||
en: 32ef8af85f4d2f66
|
||||
zh: b82b67f94346399e
|
||||
@@ -18,5 +18,5 @@
|
||||
en: e471e16b6b6450ca
|
||||
zh: 5fbc9242edc8d112
|
||||
/agent-note-change-request-panel-and-worktree-sessions/consequences:
|
||||
en: 81790c448f0a7d3b
|
||||
zh: ec63ccb2ad315ff8
|
||||
en: 0b785a2204963c4f
|
||||
zh: be4bd8db40b801a1
|
||||
|
||||
@@ -10,7 +10,9 @@ English | [中文](2026-10-09-forge-panel.zh.md)
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, and an isolated worktree service as one default-disabled Plugins page entry.
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.md) is a two-face package. Its Host half, the `forgePanel` Remote, assembles everything the panel shows for one Session into one streamed state; the forge registry and the binding service keep only their domain operations. Its Client half is a right-sidebar tab, a Session header button that shows or hides it, a New Session option, a creation card above the composer, a toast, and the [Session list icons and hover card section](2026-10-20-forge-session-list.md). Every surface calls a bound item a change request, the forge registry's term, whatever its code host calls it. The [forge bundle](../../../../packages/experimental/forge-bundle/README.md) ships the panel, the forge registry, the GitHub provider, the bindings, the worktree model tool, and an isolated worktree service for the panel as one default-disabled Plugins page entry.
|
||||
|
||||
Review and worktree controls serve one code-change workflow. One Plugins entry lets users enable that workflow without having to distinguish the underlying package split. Packages and the two worktree services remain independent so each owns its before-Session or in-conversation creation flow.
|
||||
|
||||
The Remote inspects the checkout through the Session's subprocess provider under its [resolved permissions and query restrictions](../bug-fix/2026-10-28-forge-git-session-permissions.md), so it reads what the Session's commands see. Inspection reads the top-level, Git, and common Git directories, the branch or the abbreviated HEAD commit, the upstream, the remotes, and the size of the branch's changes against its fork point. It runs when a Session is followed, after each `working-directory/change`, `tool/result`, and `sandbox/mode` event, and on refresh, one at a time with one trailing rerun.
|
||||
|
||||
@@ -89,4 +91,4 @@ The changes view offers no scope for one commit or for the last turn yet; both w
|
||||
|
||||
## Consequences
|
||||
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. With Git Worktrees also enabled, two independent worktree services exist; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
The panel shows current status only while it is open; checks that finish while nothing is mounted produce no notification. Changes made outside the Session appear after Refresh or the next tool result. Every inspection spawns up to twelve short Git processes in the Session's execution world with one remote, and two more for each further remote it tries for a fork point. A worktree exists only for a Session that requested it before its first step, and a failed or cancelled attempt can leave its branch or checkout behind. A message that enters while the Agent runs a turn or other maintenance gets no hold, so its worktree is created within the first turn and a removed worktree is not checked. While a message waits, the Agent stays idle in maintenance: the composer offers no stop, and callers waiting for the Agent to become idle wait as well. Restoring loses changes that were never committed. A base branch that requires a merge queue cannot merge from the panel. The bundle mounts two independent worktree services; the model's tool never sees the panel's service. A local or change request diff over `maxDiffBytes`, or one the host refuses, shows no files. Expanding unchanged lines of a change request reads each file from the code host once.
|
||||
|
||||
@@ -10,7 +10,9 @@ Status: implemented
|
||||
|
||||
## Decision
|
||||
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务与一个隔离的 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
[`dsh-experimental-client-ui-forge`](../../../../packages/experimental/client-ui-forge/README.zh.md) 是一个双端包。宿主端的 `forgePanel` Remote 把面板为一个 Session 展示的全部内容组装成一条流式状态;forge 注册表与绑定服务只保留各自的领域操作。客户端包括一个右侧边栏标签页、一个显示或隐藏它的 Session 标题栏按钮、一个新会话选项、一张位于输入框上方的创建卡片、一个 toast,以及[会话列表中的图标与悬浮卡片部分](2026-10-20-forge-session-list.zh.md)。所有界面都把已绑定的条目称为变更请求(forge 注册表的术语),不随代码托管平台的叫法变化。[forge 套件](../../../../packages/experimental/forge-bundle/README.zh.md)把面板、forge 注册表、GitHub 提供方、绑定服务、工作树模型工具与面板使用的一个隔离 worktree 服务作为插件页中默认禁用的一个条目发布。
|
||||
|
||||
评审与工作树控件服务于同一个代码修改流程。统一插件入口让用户可以一次启用这些能力,无需根据底层包的拆分判断应该开启哪张卡片。包与两套工作树服务仍保持独立,分别负责会话开始前和对话中创建工作树的流程。
|
||||
|
||||
Remote 通过 Session 的子进程提供方,按其[解析出的权限与查询限制](../bug-fix/2026-10-28-forge-git-session-permissions.zh.md)检查检出,因此它读到的与 Session 命令看到的相同。检查读取顶层目录、Git 目录与公共 Git 目录、分支或缩写的 HEAD 提交、上游与远端,以及分支相对于其分叉点的变更规模。它在 Session 被关注时、每个 `working-directory/change`、`tool/result` 与 `sandbox/mode` 事件之后以及刷新时运行,一次只运行一个,最多再追加一次。
|
||||
|
||||
@@ -89,4 +91,4 @@ Session 所在的 worktree 可能在 Session 之外被删除。每次检查都
|
||||
|
||||
## Consequences
|
||||
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。同时启用 Git Worktrees 时会存在两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
面板只在打开时显示当前状态;没有任何界面挂载时检查结束不会有通知。Session 之外的改动要等到刷新或下一个工具结果才会出现。只有一个远端时,每次检查最多在 Session 的执行环境中启动十二个短暂的 Git 进程;为寻找分叉点每多尝试一个远端,就多启动两个。只有在第一步之前提出请求的 Session 才会有 worktree,失败或被取消的尝试可能留下它的分支或检出。Agent 正在运行一轮或其他维护任务时进入的消息不会被暂留,因此它的 worktree 在第一轮之内创建,已删除的 worktree 也不会被检查。消息等待期间,Agent 以空闲状态处于维护任务中:输入框不提供停止,等待 Agent 空闲的调用方也会一并等待。恢复会丢失从未提交的修改。要求合并队列的基准分支无法从面板合并。组合包挂载两个互不影响的 worktree 服务;模型的工具看不到面板的服务。本地或变更请求的 diff 超过 `maxDiffBytes`,或被托管平台拒绝时,不显示任何文件。展开变更请求中未变更的行时,每个文件只从托管平台读取一次。
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
en: d1aad580a4db9872
|
||||
zh: 1eb3bac59353291d
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision:
|
||||
en: e70efbb3aa9d53b6
|
||||
zh: 1d25eae2461346df
|
||||
en: 43298ccae4f7a6d4
|
||||
zh: eb7ef46b6f290c15
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/protocol-selection-and-deployment:
|
||||
en: ec653b59d62ed95f
|
||||
zh: 0d675616059930ba
|
||||
en: 90e5031d0d2fa9e4
|
||||
zh: a9c0229b20555d9e
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/search-results-and-model-input:
|
||||
en: ad7b3e4fbe528cf2
|
||||
zh: 8ab8779bfdaaa4fa
|
||||
@@ -21,17 +21,17 @@
|
||||
en: 7566e400f9ab3a51
|
||||
zh: 118b2e84d0030480
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/default-migration:
|
||||
en: d5a8e36b545f1c04
|
||||
zh: d741fd0789401c62
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/supplementary-messages-fix:
|
||||
en: 738e32b488bbc1a1
|
||||
zh: f33afe570f62599a
|
||||
en: cb2f0910955b6730
|
||||
zh: 7df057048acd7858
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/decision/legacy-messages-search:
|
||||
en: 0dcc97d8105fab60
|
||||
zh: d80d40ae3a0c601d
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/alternatives-considered:
|
||||
en: 641cbfefc76f2f95
|
||||
zh: e3e718473eace85f
|
||||
en: 907e0b8424943d43
|
||||
zh: f45e583e7cb4eb63
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/consequences:
|
||||
en: a55da02f4d3c06c0
|
||||
zh: 3c1a624f03f34f28
|
||||
en: 7ec21c5b1124e5f8
|
||||
zh: af769dbdb4face9f
|
||||
/agent-note-direct-deepseek-search-and-fetch-apis/verification-and-remaining-api-adoption:
|
||||
en: 4c84e7bd035b52a7
|
||||
zh: 4e5687bda0a47706
|
||||
en: 595a35ce727a7c82
|
||||
zh: 10b2197663198015
|
||||
|
||||
@@ -10,15 +10,15 @@ DeepSeek web retrieval through Messages requires an auxiliary model request for
|
||||
|
||||
## Decision
|
||||
|
||||
The [DeepSeek Search and Fetch guide](https://api-docs.deepseek.com/zh-cn/guides/search), checked on 2026-10-16, defines the upstream API. The [web-deepseek-api plugin](../../../../packages/web/web-deepseek-api/README.md) implements it behind the existing `web_search` and `web_fetch` tools, and [web-search-deepseek](../../../../packages/web/web-search-deepseek/README.md) keeps Messages search. The [web capability decision](../architecture/2026-06-24-web-capability-seam.md) continues to own provider selection, tool responsibilities, and the fetch result fields.
|
||||
The [DeepSeek Search and Fetch guide](https://api-docs.deepseek.com/zh-cn/guides/search), checked on 2026-10-16, defines the upstream API. The [web-deepseek-api plugin](../../../../packages/web/web-deepseek-api/README.md) implements it behind the existing `web_search` and `web_fetch` tools. The [web capability decision](../architecture/2026-06-24-web-capability-seam.md) continues to own provider selection, tool responsibilities, and the fetch result fields, and the [web provider selection decision](../architecture/2026-10-29-web-provider-selection.md) owns how users choose each provider and which providers the shipped composition selects.
|
||||
|
||||
### Protocol selection and deployment
|
||||
|
||||
The APIs register as `deepseek-search-api` and `deepseek-fetch-api`; Messages search keeps `deepseek-official`. The setting **Use the new Web Search API** (**启用新版 Web 搜索 API**) on the Web search page writes `web.searchProvider` and `web.fetchProvider` together: on selects both APIs, and off restores Messages search with local HTTP fetch, the shipped selection. The switch defaults off and takes effect after saving, because the web service reads both selections for each operation. Messages remains the default until the Search API launch is announced, so existing users keep working search without configuration. Each operation resolves one settings snapshot, including its endpoint and credentials. Both APIs read one credential and one API base, because the guide serves them from one base and the switch selects them together.
|
||||
The APIs register as `deepseek-search-api` and `deepseek-fetch-api`. Messages search remains separately registered as `deepseek-official`. The web service selects each provider through `web.searchProvider` and `web.fetchProvider`, which the Search and Fetch menus on the Web search and fetch page set independently; the shipped composition selects the Search API and local HTTP fetch, as the [selection decision](../architecture/2026-10-29-web-provider-selection.md) records. A saved selection takes effect on the next operation because the web service reads both selections per operation. Each operation resolves one settings snapshot, including its endpoint and credential. Both APIs read the `web-deepseek-api` namespace, and each capability resolves its own endpoint and credential.
|
||||
|
||||
The two protocols retain independent API bases. Messages keeps `https://api.deepseek.com/anthropic/v1` and appends `/messages`. Direct retrieval uses `web-deepseek-api.baseURL`, which defaults to the guide's public base `https://api.deepseek.com/v1/tools`, and appends `/search` or `/fetch`; the verified development base is `http://api-dev.deepseek.com:8031/api/v1/tools`. These prefixes are different and are never inferred from the hostname, so another deployment enters its complete base. The switch is already the explicit opt-in, so the documented public deployment serves as its default. A blank or unparseable base leaves both providers unavailable.
|
||||
Direct retrieval appends `/search` or `/fetch` to an API base. A capability without its own YAML endpoint fields uses the origin of the conversation's DeepSeek model route base URL with `/v1/tools`, which for the public deployment is the guide's `https://api.deepseek.com/v1/tools`; the [selection decision](../architecture/2026-10-29-web-provider-selection.md) owns that derivation and the credential each operation sends. The verified development base is `http://api-dev.deepseek.com:8031/api/v1/tools`; its prefix differs and is never inferred from the hostname, so such a deployment sets the complete base in `searchBaseURL` or `fetchBaseURL`. An unparseable base leaves that capability's provider unavailable.
|
||||
|
||||
The guide specifies API-key Bearer authentication, and requests send only the documented authorization and JSON content type. A Session on the DeepSeek account route sends its account token instead, as Messages search does; the public Search and Fetch routes accepted it on 2026-10-16, so account-only users can enable direct retrieval. An Anthropic-compatible gateway checked the same day served Messages web search and had no `/v1/tools` routes; such deployments keep Messages search.
|
||||
The guide specifies API-key Bearer authentication, and requests send only the documented authorization and JSON content type. A conversation on the `deepseek-account` route sends the account token instead; the public Search and Fetch routes accepted it on 2026-10-16, so account-only users have direct retrieval. An Anthropic-compatible gateway checked the same day served Messages web search and had no `/v1/tools` routes; such deployments can select Messages search or set the capability's own base URL to one that serves `/v1/tools`.
|
||||
|
||||
### Search results and model input
|
||||
|
||||
@@ -36,45 +36,41 @@ The HTTP provider retains its own URL validation and resource limits. Selecting
|
||||
|
||||
### Default migration
|
||||
|
||||
Changing the default to the Search API is a separate change after the launch is announced. API keys and account tokens both reach the public routes; deployments behind a gateway without `/v1/tools` routes need an explicit `web.searchProvider: deepseek-official` before the default changes, and a stored Messages selection keeps Messages.
|
||||
The shipped composition selects `deepseek-search-api`; the [selection decision](../architecture/2026-10-29-web-provider-selection.md) owns that default and its no-fallback rule. API keys and account tokens both reach the public routes. Deployments behind a gateway without `/v1/tools` routes set `searchBaseURL` to a base that serves them. A stored `web.searchProvider: deepseek-official` selects Messages search, and any other stored selection keeps its provider.
|
||||
|
||||
### Supplementary Messages fix
|
||||
### Legacy Messages search
|
||||
|
||||
The retained Messages path explicitly sends `thinking: { type: 'disabled' }` and records that same body before dispatch. This avoids reasoning work in an auxiliary request whose generated answer is discarded; it does not implement or control the new retrieval API. `tool_choice` remains omitted. The [persistence acknowledgement](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.md) owns compatibility for the optional logged field.
|
||||
The independent `dsh-web-search-deepseek` provider sends Messages search requests when `deepseek-official` is selected, as the [selection decision](../architecture/2026-10-29-web-provider-selection.md) records. Its `web/deepseek-search-llm-request` Session event type, including the optional `thinking: { type: 'disabled' }` body field that the [persistence acknowledgement](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.md) covers, stays declared and written in `dsh-web-search-deepseek`, so released Sessions that contain the event still open. Messages search sends that field because Harness discards the generated answer and the auxiliary request needs no reasoning.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep Messages as the only search implementation.** It preserves auxiliary generation and response extraction even when callers need only sources. Direct Search removes that Harness model request while retaining Messages as an explicit mode.
|
||||
|
||||
**Make the Search API the default now.** The launch is not announced, and some gateways expose only Messages; their users would lose working search.
|
||||
**Keep Messages as the only search implementation.** It preserves auxiliary generation and response extraction even when callers need only sources. Direct Search removes that Harness model request.
|
||||
|
||||
**Reuse the Messages address or infer a route prefix.** The protocols and deployed prefixes differ. Separate bases preserve existing configurations and avoid sending a valid request to the wrong route.
|
||||
|
||||
**Require an explicit Search API base.** Enabling the switch is already an explicit choice, and an empty default would make every user copy the documented public address. Other deployments still override it.
|
||||
**Require an explicit Search API base.** The Search API is the shipped search default, and an empty default would leave it unavailable until every user enters an address that the DeepSeek model route already determines. Other deployments still set their own base.
|
||||
|
||||
**Keep the APIs inside the Messages provider behind a provider-local switch.** A switch inside `web-search-deepseek` cannot change fetch routing, so turning it on left `web_fetch` on HTTP unless the user also edited `web.fetchProvider`; it also tied the APIs' lifecycle to Messages search, which the default migration may retire. Separate registrations let the web service's existing selection express the switch, and each package can change or retire alone.
|
||||
**Keep the APIs inside the Messages provider behind a provider-local switch.** A switch inside the Messages provider cannot change fetch routing, so `web_fetch` would stay on HTTP unless the user also edited `web.fetchProvider`; it also ties the APIs' lifecycle to Messages search. Separate registrations let the web service's existing selection choose each provider, without coupling either API to the Messages provider.
|
||||
|
||||
**Share one transport between Messages search and the new APIs.** Both resolve credentials through the same seams, `ctx.credentials` and `ctx.deepseekAccount`, so a shared module would save only the dispatch and failure-mapping code. It would make the Messages package depend on the new API package and tie their releases, while the default migration may retire Messages search. Each package keeps its own transport until then.
|
||||
**Share one transport between Messages search and the new APIs.** A shared web transport would couple different request formats and failure mappings. Credential values instead come from the shared [`llm-deepseek` credential helpers](../../../../packages/llm/llm-deepseek/src/credential.ts); Messages search and the Search/Fetch API transports remain independent.
|
||||
|
||||
**Split Search and Fetch into separate packages.** The guide serves both APIs from one base with one credential, and the switch selects them together; separate packages would duplicate the settings a user edits once. Each operation lives in its own module, so either API can change without the other.
|
||||
**Split Search and Fetch into separate packages.** The guide serves both APIs from one base with one credential; separate packages would duplicate settings that address one deployment. Independent selection does not require separate packages, because each API registers its own provider id. Each operation lives in its own module, so either API can change without the other.
|
||||
|
||||
**List every registered provider in a settings dropdown.** A generic provider form derives editors from Config schemas and needs host-side display metadata on the provider interfaces. The page needs only the DeepSeek choice, and other providers keep their own settings pages.
|
||||
**List every registered provider in a settings dropdown.** A generic provider form derives editors from Config schemas and needs host-side display metadata on the provider interfaces. This rejection stands for the generic form: the page edits only the DeepSeek providers' settings, and other providers keep their own settings pages. The [selection decision](../architecture/2026-10-29-web-provider-selection.md) supersedes it for the provider menus, which list every registered provider by its `title`.
|
||||
|
||||
**Let the model supply `ori_question` as a tool argument.** The API defines the field as the user's original question, which the latest human prompt already records. A tool argument would change the model-facing schema shared by every search provider and invite paraphrase.
|
||||
|
||||
**Keep HTTP fetch when the switch turns on.** The switch adopts the new DeepSeek retrieval APIs, and leaving fetch on HTTP made Fetch API adoption depend on a hand-edited `web.fetchProvider`. HTTP fetch, with its own access policy, remains the selection whenever the switch is off, and configuration can still select either fetch provider alone.
|
||||
|
||||
**Add an extraction tool or widen the fetch result.** The API retrieves one concrete web page as Markdown, which fits the existing fetch operation. Its success already implies a retrieved page, and the Markdown body carries the page heading, so widening the result would change every fetch consumer for little model-visible gain.
|
||||
|
||||
## Consequences
|
||||
|
||||
Retrieval no longer requires auxiliary model tokens in direct mode, but its results still consume the main model's context. One Messages web search through a gateway on 2026-10-16 consumed 8,154 input and 828 output tokens for a single server-side search. The guide prices Search and Fetch independently at CNY 5 per 1,000 successful requests with results; result count does not change a request's price. Multiple queries remain multiple requests. The stated Claude Code and Codex promotion does not establish a Harness exemption. Search API mode also sends up to 128 characters of the user's prompt to the search service with each query unless `sendOriginalQuestion` is off.
|
||||
Direct retrieval requires no auxiliary model tokens, but its results still consume the main model's context. One Messages web search through a gateway on 2026-10-16 consumed 8,154 input and 828 output tokens for a single server-side search. The guide prices Search and Fetch independently at CNY 5 per 1,000 successful requests with results; result count does not change a request's price. Multiple queries remain multiple requests. The stated Claude Code and Codex promotion does not establish a Harness exemption. Search API mode also sends up to 128 characters of the user's prompt to the search service with each query unless `sendOriginalQuestion` is off.
|
||||
|
||||
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The current tool can issue four queries concurrently and fails the batch if one query fails. The provider has no shared rate scheduler, automatic retries, or fallback; other Sessions or processes can exhaust the same account allowance.
|
||||
Search and Fetch each have an account-wide 5 QPS limit, independent of model concurrency and shared across that account's API keys. Fetch also has a target-domain limit. The tool accepts up to `searchMaxQueries` queries per call, 10 by default, and fails the batch if one query fails. The Search provider sends at most `searchRequestsPerSecond` requests, 5 by default, in any one-second window of one plugin instance, so the second five queries of a full batch start one second after the first. The pacing is not shared across processes, and the provider has no automatic retries or fallback; other processes or clients can exhaust the same account allowance.
|
||||
|
||||
## Verification and remaining API adoption
|
||||
|
||||
Provider tests cover request mapping, empty results, malformed responses, independent and default bases, account-token authentication, cancellation, and blank configuration. Settings tests and the [plugin configuration browser scenario](../../../../apps/web/tests/plugin-config.e2e.ts) cover staged saves, the default base, and switching both selections. The [web search replay](../../../../apps/web/tests/web-search-round.e2e.ts) and [Fetch Session replay](../../../../snapshots/session/deepseek-fetch/snapshot.yml) pin model-visible results. On 2026-10-16 the [real provider tests](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts) passed Search and Fetch against the public base, and a headless round on the shipped profile returned eight sources and fetched Markdown without an auxiliary request event. The account-token, gateway, and status observations above come from manual probes the same day.
|
||||
Provider tests cover request mapping, empty results, malformed responses, independent and derived bases, account-token authentication and its invalidation after HTTP 401, cancellation, and blank configuration. Settings tests and the [plugin configuration browser scenario](../../../../apps/web/tests/plugin-config.e2e.ts) cover staged saves, the result count, and independent Search and Fetch selection. The [web search replay](../../../../apps/web/tests/web-search-round.e2e.ts) and [Fetch Session replay](../../../../snapshots/session/deepseek-fetch/snapshot.yml) pin model-visible results. On 2026-10-16 the [real provider tests](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts) passed Search and Fetch against the public base, and a headless round on the shipped profile returned eight sources and fetched Markdown without an auxiliary request event. The account-token, gateway, and status observations above come from manual probes the same day.
|
||||
|
||||
The following capabilities remain outside the implemented integration:
|
||||
|
||||
|
||||
@@ -10,15 +10,15 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
2026-10-16 核对的 [DeepSeek Search 与 Fetch 指南](https://api-docs.deepseek.com/zh-cn/guides/search)定义上游 API。[web-deepseek-api 插件](../../../../packages/web/web-deepseek-api/README.zh.md)通过现有 `web_search` 和 `web_fetch` 工具接入它,[web-search-deepseek](../../../../packages/web/web-search-deepseek/README.zh.md) 保留 Messages 搜索。[Web 能力决策](../architecture/2026-06-24-web-capability-seam.zh.md)继续负责提供方选择、工具职责和抓取结果字段。
|
||||
2026-10-16 核对的 [DeepSeek Search 与 Fetch 指南](https://api-docs.deepseek.com/zh-cn/guides/search)定义上游 API。[web-deepseek-api 插件](../../../../packages/web/web-deepseek-api/README.zh.md)通过现有 `web_search` 和 `web_fetch` 工具接入它。[Web 能力决策](../architecture/2026-06-24-web-capability-seam.zh.md)继续负责提供方选择、工具职责和抓取结果字段;[Web 提供方选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)负责用户如何选择各个提供方,以及内置组合选择哪些提供方。
|
||||
|
||||
### 协议选择与部署
|
||||
|
||||
两个 API 注册为 `deepseek-search-api` 和 `deepseek-fetch-api`;Messages 搜索保留 `deepseek-official`。网页搜索页上的设置 **Use the new Web Search API**(**启用新版 Web 搜索 API**)同时写入 `web.searchProvider` 和 `web.fetchProvider`:开启时选择两个 API,关闭时恢复内置选择,即 Messages 搜索与本地 HTTP 抓取。开关默认关闭,保存后生效,因为 Web 服务在每次操作时读取两项选择。在 Search API 正式发布公告之前,Messages 仍是默认值,已有用户无需配置即可继续使用搜索。每次操作解析一份设置快照,包含端点和凭据。两个 API 读取同一份凭据和同一个 API 基础地址,因为指南从同一基础地址提供它们,开关也同时选择它们。
|
||||
两个 API 注册为 `deepseek-search-api` 和 `deepseek-fetch-api`;Messages 搜索仍独立注册为 `deepseek-official`。Web 服务通过 `web.searchProvider` 和 `web.fetchProvider` 选择各个提供方,「网页搜索与获取」页上的 Search 和 Fetch 下拉框分别设置这两项;内置组合选择 Search API 与本地 HTTP 抓取,见[选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)。已保存的选择从下一次操作起生效,因为 Web 服务在每次操作时读取两项选择。每次操作解析一份包含端点和凭据的设置快照。两个 API 读取 `web-deepseek-api` namespace,每个能力各自解析端点与凭据。
|
||||
|
||||
两种协议分别保存 API 基础地址。Messages 保留 `https://api.deepseek.com/anthropic/v1`,追加 `/messages`。直接检索使用 `web-deepseek-api.baseURL`,默认值为指南中的公网地址 `https://api.deepseek.com/v1/tools`,追加 `/search` 或 `/fetch`;已验证的开发地址为 `http://api-dev.deepseek.com:8031/api/v1/tools`。两者前缀不同,程序不会根据主机名推断,其他部署需填写完整基础地址。开关本身已是显式启用,因此以文档中的公网部署作为默认值。基础地址为空白或无法解析时,两个提供方都不可用。
|
||||
直接检索在 API 基础地址后追加 `/search` 或 `/fetch`。没有在 YAML 中单独设置端点字段的能力,使用会话的 DeepSeek 模型路由基础地址的 origin 加 `/v1/tools`,公网部署即指南中的 `https://api.deepseek.com/v1/tools`;该推导及每次操作发送的凭据由[选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)负责。已验证的开发地址为 `http://api-dev.deepseek.com:8031/api/v1/tools`;其前缀不同,程序不会根据主机名推断,此类部署需在 `searchBaseURL` 或 `fetchBaseURL` 中填写完整基础地址。基础地址无法解析时,该能力的提供方不可用。
|
||||
|
||||
指南规定使用 API 密钥进行 Bearer 鉴权,请求只发送文档规定的鉴权与 JSON 内容类型请求头。DeepSeek 账号路由上的会话改为发送其账号 token,与 Messages 搜索一致;2026-10-16 公网 Search 与 Fetch 路由已接受该 token,因此只登录账号的用户也可以启用直接检索。同日检查的一个 Anthropic 兼容网关提供 Messages 网页搜索,但没有 `/v1/tools` 路由;此类部署继续使用 Messages 搜索。
|
||||
指南规定使用 API 密钥进行 Bearer 鉴权,请求只发送文档规定的鉴权与 JSON 内容类型请求头。`deepseek-account` 路由上的会话改为发送账号 token;2026-10-16 公网 Search 与 Fetch 路由已接受该 token,因此只登录账号的用户也能使用直接检索。同日检查的一个 Anthropic 兼容网关提供 Messages 网页搜索,但没有 `/v1/tools` 路由;此类部署可以选择 Messages 搜索,或把该能力的单独基础地址设为提供 `/v1/tools` 的地址。
|
||||
|
||||
### 搜索结果与模型输入
|
||||
|
||||
@@ -36,45 +36,41 @@ HTTP 提供方继续负责自身的 URL 校验和资源限制。选择 DeepSeek
|
||||
|
||||
### 默认值迁移
|
||||
|
||||
将默认值改为 Search API 是发布公告之后的另一项变更。API 密钥和账号 token 都能访问公网路由;位于没有 `/v1/tools` 路由的网关之后的部署,需要在默认值变更前显式设置 `web.searchProvider: deepseek-official`,已保存的 Messages 选择仍使用 Messages。
|
||||
内置组合选择 `deepseek-search-api`;[选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)负责这项默认值及其不回退规则。API 密钥和账号 token 都能访问公网路由。位于没有 `/v1/tools` 路由的网关之后的部署,需把 `searchBaseURL` 设为提供这些路由的基础地址。已保存的 `web.searchProvider: deepseek-official` 会选中 Messages 搜索,其他已保存的选择保持其提供方。
|
||||
|
||||
### 附带的 Messages 修复
|
||||
### 旧版 Messages 搜索
|
||||
|
||||
保留的 Messages 路径显式发送 `thinking: { type: 'disabled' }`,并在发送前记录同一请求体。这避免了辅助请求在生成回答被丢弃的情况下执行思考;它不实现或控制新的检索 API。`tool_choice` 仍不传递。[持久化确认记录](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.zh.md)负责日志中可选字段的兼容性。
|
||||
选中 `deepseek-official` 时,独立的 `dsh-web-search-deepseek` 提供方发送 Messages 搜索请求,见[选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)。它的 `web/deepseek-search-llm-request` 会话事件类型(包括[持久化确认记录](../../../../docs/persistence-changes/2026-10-13-deepseek-search-thinking.zh.md)覆盖的可选请求体字段 `thinking: { type: 'disabled' }`)仍由 `dsh-web-search-deepseek` 声明并写入,因此包含该事件的已发布会话仍能打开。Messages 搜索发送该字段,是因为 Harness 丢弃生成的回答,辅助请求无需推理。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**仅保留 Messages 搜索实现。** 即使调用方只需要来源,这也保留了辅助生成和响应提取。直接 Search 去掉这次 Harness 模型请求,同时保留 Messages 供显式选择。
|
||||
|
||||
**立即将 Search API 设为默认值。** 尚未发布公告,部分网关也只提供 Messages;这些用户会失去可用的搜索。
|
||||
**仅保留 Messages 搜索实现。** 即使调用方只需要来源,这也保留了辅助生成和响应提取。直接 Search 去掉这次 Harness 模型请求。
|
||||
|
||||
**复用 Messages 地址或推断路由前缀。** 协议与部署前缀不同。分别保存基础地址可保留现有配置,避免把有效请求发往错误路由。
|
||||
|
||||
**要求显式填写 Search API 基础地址。** 开启开关已是显式选择,空默认值会迫使每位用户复制文档中的公网地址。其他部署仍可覆盖它。
|
||||
**要求显式填写 Search API 基础地址。** Search API 是内置的搜索默认值,空默认值会让它在每位用户填写一个 DeepSeek 模型路由已经确定的地址之前都不可用。其他部署仍可设置自己的基础地址。
|
||||
|
||||
**把两个 API 放在 Messages 提供方内部,由提供方自己的开关切换。** `web-search-deepseek` 内的开关无法改变抓取路由,因此开启后除非用户另外修改 `web.fetchProvider`,`web_fetch` 仍走 HTTP;它还把两个 API 的生命周期绑在 Messages 搜索上,而默认值迁移后 Messages 搜索可能被移除。独立注册让 Web 服务现有的选择来表达这个开关,每个包都可以单独修改或移除。
|
||||
**把两个 API 放在 Messages 提供方内部,由提供方自己的开关切换。** Messages 提供方内的开关无法改变抓取路由,因此除非用户另外修改 `web.fetchProvider`,`web_fetch` 仍会走 HTTP;它还会把两个 API 的生命周期绑在 Messages 搜索上。独立注册让 Web 服务通过现有的提供方选择机制选择各个提供方,不会把任一 API 与 Messages 提供方绑定。
|
||||
|
||||
**让 Messages 搜索与新 API 共用一套传输层。** 两者都通过同样的 seam,即 `ctx.credentials` 和 `ctx.deepseekAccount` 解析凭据,共享模块只能省下请求派发与失败映射代码。它会让 Messages 包依赖新 API 包、两者必须一起发版,而默认值迁移后 Messages 搜索可能被移除。在此之前,两个包各自保留传输层。
|
||||
**让 Messages 搜索与新 API 共用一套传输层。** 共享 Web 传输层会耦合不同的请求格式和失败映射。凭据值改为通过共享的 [`llm-deepseek` 凭据函数](../../../../packages/llm/llm-deepseek/src/credential.ts)获取;Messages 搜索与 Search/Fetch API 传输层保持独立。
|
||||
|
||||
**把 Search 和 Fetch 拆成两个包。** 指南从同一基础地址、以同一份凭据提供两个 API,开关也同时选择它们;拆成两个包会让用户只需编辑一次的设置重复出现。每种操作位于独立模块,任一 API 都可以单独修改。
|
||||
**把 Search 和 Fetch 拆成两个包。** 指南从同一基础地址、以同一份凭据提供两个 API;拆成两个包会让指向同一部署的设置重复出现。分别选择不需要拆包,因为每个 API 注册自己的提供方 id。每种操作位于独立模块,任一 API 都可以单独修改。
|
||||
|
||||
**在设置页用下拉框列出所有已注册的提供方。** 通用提供方表单从 Config schema 推导编辑控件,还需要在提供方接口上增加宿主侧展示元数据。该页面只需要 DeepSeek 的选择,其他提供方保留各自的设置页。
|
||||
**在设置页用下拉框列出所有已注册的提供方。** 通用提供方表单从 Config schema 推导编辑控件,还需要在提供方接口上增加宿主侧展示元数据。这一否决对通用表单仍然成立:该页面只编辑 DeepSeek 提供方的设置,其他提供方保留各自的设置页。对于提供方下拉框,[选择决策](../architecture/2026-10-29-web-provider-selection.zh.md)取代了这一否决,下拉框按 `title` 列出所有已注册的提供方。
|
||||
|
||||
**让模型以工具参数提供 `ori_question`。** API 将该字段定义为用户的原始问题,而最近一条用户提示已记录了这段文本。工具参数会改变所有搜索提供方共用的面向模型的 schema,并引入改写。
|
||||
|
||||
**开启开关时保留 HTTP 抓取。** 该开关用于启用 DeepSeek 新的检索 API,保留 HTTP 抓取会让使用 Fetch API 依赖手动修改 `web.fetchProvider`。开关关闭时,抓取仍选择带有自身访问策略的 HTTP 提供方,配置中也仍可单独选择任一抓取提供方。
|
||||
|
||||
**新增提取工具或扩展抓取结果。** API 以 Markdown 获取一个具体网页,符合现有抓取操作。其成功本身即表示已获取页面,Markdown 正文也带有页面标题,因此扩展结果会改动每个抓取消费方,而模型可见的收益很小。
|
||||
|
||||
## 影响
|
||||
|
||||
直接模式中的检索不再需要辅助模型 token,但结果仍消耗主模型上下文。2026-10-16 通过网关进行的一次 Messages 网页搜索,为一次服务端搜索消耗了 8,154 个输入 token 和 828 个输出 token。指南对 Search 和 Fetch 分别按每 1,000 次成功且有结果的请求收取 5 元;结果条数不影响单次价格。多个查询仍对应多次请求。文档中的 Claude Code 与 Codex 优惠不能作为 Harness 免收费用的依据。除非关闭 `sendOriginalQuestion`,Search API 模式还会随每个查询把用户提示的至多 128 个字符发送给搜索服务。
|
||||
直接检索不需要辅助模型 token,但结果仍消耗主模型上下文。2026-10-16 通过网关进行的一次 Messages 网页搜索,为一次服务端搜索消耗了 8,154 个输入 token 和 828 个输出 token。指南对 Search 和 Fetch 分别按每 1,000 次成功且有结果的请求收取 5 元;结果条数不影响单次价格。多个查询仍对应多次请求。文档中的 Claude Code 与 Codex 优惠不能作为 Harness 免收费用的依据。除非关闭 `sendOriginalQuestion`,Search API 模式还会随每个查询把用户提示的至多 128 个字符发送给搜索服务。
|
||||
|
||||
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。当前工具可并发发送四个查询,任一查询失败会使整批失败。提供方没有共享限速调度器、自动重试或回退;其他会话或进程也会消耗同一账号的额度。
|
||||
Search 和 Fetch 各有账号级 5 QPS 限制,独立于模型并发,同一账号的 API 密钥共享额度。Fetch 还有目标域名限制。工具每次调用最多接受 `searchMaxQueries` 个查询,默认 10 个;任一查询失败会使整批失败。Search 提供方在单个插件实例的任意一秒窗口内最多发送 `searchRequestsPerSecond` 个请求,默认 5 个,因此满批次的后五个查询比前五个晚一秒开始。该限速不跨进程共享,提供方也没有自动重试或回退;其他进程或客户端也会消耗同一账号的额度。
|
||||
|
||||
## 验证与尚未接入的 API 能力
|
||||
|
||||
提供方测试覆盖请求映射、空结果、格式错误的响应、独立地址与默认地址、账号 token 鉴权、取消与空白配置。设置测试及[插件配置浏览器场景](../../../../apps/web/tests/plugin-config.e2e.ts)覆盖暂存后保存、默认地址和同时切换两项选择。[网页搜索回放](../../../../apps/web/tests/web-search-round.e2e.ts)与 [Fetch Session 回放](../../../../snapshots/session/deepseek-fetch/snapshot.yml)固定模型可见结果。2026-10-16,[真实提供方测试](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts)在公网地址通过了 Search 与 Fetch 验证;在内置配置上运行的一轮 headless 对话返回了八个来源并抓取到 Markdown,且没有辅助请求事件。上文的账号 token、网关和状态码观察来自同日的手动探测。
|
||||
提供方测试覆盖请求映射、空结果、格式错误的响应、独立地址与推导地址、账号 token 鉴权及其在 HTTP 401 后被作废、取消与空白配置。设置测试及[插件配置浏览器场景](../../../../apps/web/tests/plugin-config.e2e.ts)覆盖暂存后保存、结果数,以及分别选择 Search 与 Fetch。[网页搜索回放](../../../../apps/web/tests/web-search-round.e2e.ts)与 [Fetch Session 回放](../../../../snapshots/session/deepseek-fetch/snapshot.yml)固定模型可见结果。2026-10-16,[真实提供方测试](../../../../packages/web/web-deepseek-api/tests/web-deepseek-api.e2e.ts)在公网地址通过了 Search 与 Fetch 验证;在内置配置上运行的一轮 headless 对话返回了八个来源并抓取到 Markdown,且没有辅助请求事件。上文的账号 token、网关和状态码观察来自同日的手动探测。
|
||||
|
||||
以下能力尚未包含在已实现的接入中:
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-02-liveshare-plugin-method-catalog.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-11-02-liveshare-plugin-method-catalog.md
|
||||
/agent-note-liveshare-plugin-method-catalog:
|
||||
en: 1daaf32c7ea8d3cd
|
||||
zh: 70a94350263d22c9
|
||||
/agent-note-liveshare-plugin-method-catalog/problem:
|
||||
en: 2122ac5fdd75f900
|
||||
zh: dc135aaa0d1dddc0
|
||||
/agent-note-liveshare-plugin-method-catalog/decision:
|
||||
en: 6e0b897ba6460894
|
||||
zh: f05866ccde01a5af
|
||||
/agent-note-liveshare-plugin-method-catalog/alternatives-considered:
|
||||
en: 57896311cada7804
|
||||
zh: 01fbd1cbd05353ef
|
||||
/agent-note-liveshare-plugin-method-catalog/consequences:
|
||||
en: e2069a5f9801f767
|
||||
zh: 0698f0b9e1e49201
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note: LiveShare plugin method catalog
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-02-liveshare-plugin-method-catalog.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
LiveShare used to expose plugin endpoints through a declaration API. The API let a plugin name arbitrary endpoints, so a catalog mistake could widen a viewer's reach without a corresponding Gateway check. The replacement needs one source of truth that says which plugin methods a share may expose.
|
||||
|
||||
## Decision
|
||||
|
||||
The catalog is enumerated from the Typert local registry at runtime. One entry is emitted per bundle, using the bundle's localized title when available and the component package title otherwise. Endpoint descriptions come from the registered Remote descriptors. A share stores only method-level policies: the Host intersects each policy with the catalog before the Gateway dispatches a call.
|
||||
|
||||
The catalog excludes the DSH core bundles. Their methods remain governed by the existing Session and Workspace sharing roles, not by plugin permissions. The core bundle names are derived from `PROFILE_TEMPLATES`, so the exclusion follows the shipped profile composition instead of a second hand-maintained list. LiveShare's own bundle is excluded because it is the sharing implementation, not a capability a share can grant itself.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
Keeping the declaration API would leave two sources of truth: a plugin-declared endpoint and the Gateway's own classification. A hand-maintained allowlist in LiveShare would drift from the shipped profile templates. Deriving the exclusion from the actual profile templates keeps the catalog aligned with composition without a second list.
|
||||
|
||||
## Consequences
|
||||
|
||||
A method that is not in the Typert local registry cannot be selected or called through plugin permissions. A plugin that uses an owner-only exact route is absent from the catalog; the viewer sees an `owner-route-not-shared` notice when it calls that route. Adding a shareable method is therefore a plugin-side registration decision, not a LiveShare catalog edit.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Agent Note:LiveShare 插件方法目录
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-02-liveshare-plugin-method-catalog.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
LiveShare 过去通过声明 API 暴露插件端点。该 API 允许插件声明任意端点,因此目录写错时,viewer 的可达范围会在没有对应 Gateway 校验的情况下被扩大。替代方案需要一个唯一事实来源,明确一条分享可以暴露哪些插件方法。
|
||||
|
||||
## 决定
|
||||
|
||||
目录在运行时从 Typert 本地注册表枚举。每个 bundle 生成一行,优先使用 bundle 的本地化标题,没有时使用组件包标题;端点说明来自已注册的 Remote descriptor。分享记录只保存方法级策略,Host 在 Gateway 派发前把策略与目录取交集。
|
||||
|
||||
目录排除 DSH 核心 bundle。它们的方法仍由原有的 Session 和 Workspace 分享角色控制,而不是插件权限。核心 bundle 名从 `PROFILE_TEMPLATES` 派生,使排除规则跟随实际下发的 profile 组合,而不是再维护一份手写名单。LiveShare 自身的 bundle 也被排除,因为它是分享实现,不是分享可以授予自己的能力。
|
||||
|
||||
## 备选方案
|
||||
|
||||
保留声明 API 会留下两个事实来源:插件声明的端点和 Gateway 自己的分类。由 LiveShare 维护手写白名单又会与实际下发的 profile 模板漂移。从真实 profile 模板派生排除规则,让目录跟随组合而不需要第二份名单。
|
||||
|
||||
## 影响
|
||||
|
||||
不在 Typert 本地注册表中的方法无法被插件权限选中或调用。使用 owner-only exact route 的插件不会出现在目录中;viewer 调用该路由时会看到 `owner-route-not-shared` 提示。因此新增可分享方法是插件侧的注册决定,而不是修改 LiveShare 目录。
|
||||
+2
-2
@@ -9,8 +9,8 @@
|
||||
en: 3e229ecf646af866
|
||||
zh: d358507b1e1d381f
|
||||
/agent-note-wine-and-native-windows-ci/decision:
|
||||
en: 1125b44a170f70fb
|
||||
zh: 37838b9da2003651
|
||||
en: 9038773113cf3553
|
||||
zh: 67875d393093c105
|
||||
/agent-note-wine-and-native-windows-ci/alternatives-considered:
|
||||
en: 0e9a322cfc38d313
|
||||
zh: f7bdeb86ba4fea1a
|
||||
|
||||
@@ -30,7 +30,7 @@ Native watchers use `canonicalizeWatchPath()` to realpath the deepest existing a
|
||||
|
||||
Windows durable JSONL paths keep drive roots in native spelling and apply the extended-length namespace only to descendants and staging paths. The ACP teardown ladder uses real Node children, proves graceful and forced tiers with host-appropriate outcomes, and avoids claiming POSIX signal delivery on Windows. Executable fixtures provide `.cmd` shims and `PATHEXT` where the product accepts a bare command. Repository-cache helpers live inside the selected Git subpath so their declared `file:` dependencies expose command shims identically on Windows. The bundled installer exports pnpm's own workspace-ignore configuration, retains `PNPM_HOME` for pnpm data while removing that directory from lifecycle `PATH`, and prioritizes `.CMD` in `PATHEXT`; nested Git-package installation therefore cannot rejoin the enclosing workspace or let an inherited Windows pnpm executable outrank the transaction-owned wrapper.
|
||||
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include serializes debounced writes, retries only transient access or busy failures with bounded backoff, and observes every timer rejection. A terminal persistence failure remains on the queue and is rethrown to the teardown owner, while successful teardown drains the latest write.
|
||||
Post-boot profile watcher setup proceeds only while the root fiber and Loader are both live. A concurrent setup error is contained only when the same invocation's recorded signal already owns shutdown; unrelated HMR failures remain loud. The [process-shutdown controller](../../../../apps/cli/src/process-shutdown.ts) lets a successful one-shot completion drain Node's remaining handles after root disposal, while teardown failure, deadline, and signal escalation retain forced exit. The vendored Include journals runtime changes and writes them through an atomic rename, retrying only transient access or busy failures with bounded backoff. `flush()` and `stop()` reject while changes remain unsaved, and a later successful flush clears the previous error.
|
||||
|
||||
Shiki disables lazy TextMate-regex compilation and warms each boot grammar before user content enters the unchanged per-line tokenization budget, so scheduler contention cannot publish a partial highlighted stream. The Codex real-product fixture is pinned to stable 0.153.4 schemas and selects an actually advertised command tool and argument shape, preserving the provider-owned protocol while proving unattended rejection and whole-tree exit on each host.
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Wine 在 Linux 内核与区分大小写的 ext4 之上采用 hoisted 依赖布
|
||||
|
||||
Windows 的持久 JSONL 路径会保留驱动器根目录的原生写法,并仅对后代路径与暂存路径应用扩展长度命名空间。ACP(Agent Client Protocol)拆卸阶梯使用真实 Node 子进程,以符合宿主语义的结果证明优雅终止与强制终止两个层级,并避免声称 Windows 会交付 POSIX 信号。产品接受裸命令时,可执行 fixture 会提供 `.cmd` 包装脚本与 `PATHEXT`。repository-cache 辅助包位于所选 Git 子路径内,因此它们声明的 `file:` 依赖会在 Windows 上以相同方式暴露命令包装脚本。随附的安装器会导出 pnpm 自有的 workspace-ignore 配置,保留 `PNPM_HOME` 作为 pnpm 数据配置,同时从生命周期 `PATH` 中移除该目录,并在 `PATHEXT` 中优先选择 `.CMD`;因此,嵌套 Git 包安装既不会重新加入外层 workspace,也不会让继承的 Windows pnpm 可执行文件抢在事务持有的 wrapper 之前。
|
||||
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 会串行化防抖写入,只对瞬时访问或忙碌故障执行有界退避重试,并确保每个由计时器触发的拒绝都得到观察。持久化最终失败后,该故障会保留在队列中,并重新抛给拆卸责任方;成功拆卸则会排空最新写入。
|
||||
启动后,只有根 fiber 与 Loader 均处于活跃状态时,系统才会继续设置 profile watcher。只有当同一次调用所记录的信号已取得关闭流程所有权时,系统才会隔离并发设置错误;无关 HMR 故障仍会响亮失败。[进程关闭控制器](../../../../apps/cli/src/process-shutdown.ts)会在根级 dispose 成功后让单次任务的正常完成流程排空 Node 剩余句柄,同时让拆卸失败、截止时间到期和信号升级继续强制退出。vendored Include 通过 journal 记录运行时变更,并以原子 rename 写入,只对瞬时访问或忙碌故障执行有界退避重试。仍有未保存变更时,`flush()` 和 `stop()` 会拒绝;之后一次成功的 flush 会清除先前的错误。
|
||||
|
||||
Shiki 会禁用 TextMate 正则的延迟编译,并在用户内容进入保持不变的逐行 tokenization(词元化)预算前预热每种启动语法,从而避免调度器争用发布不完整的高亮流。Codex 真实产品 fixture 固定使用稳定版 0.153.4 schema,并选择实际提供的命令工具与对应参数形态;这样既保留由提供方负责的协议,也能在每种宿主上证明无人值守拒绝和整棵进程树退出。
|
||||
|
||||
|
||||
+4
-4
@@ -9,11 +9,11 @@
|
||||
en: d4ab65a39cf4fd7c
|
||||
zh: 62144be9b4fd7956
|
||||
/agent-note-keep-loader-mutations-non-transactional/decision:
|
||||
en: 9b661159de63d421
|
||||
zh: 15697f468a90edb9
|
||||
en: 83268169e9436127
|
||||
zh: ce740df03e9fe24a
|
||||
/agent-note-keep-loader-mutations-non-transactional/alternatives-considered:
|
||||
en: 72b871064c7ae315
|
||||
zh: a09e5168f8124c4c
|
||||
en: 645db43a48b9cfc1
|
||||
zh: d03fabd1e82dadbd
|
||||
/agent-note-keep-loader-mutations-non-transactional/consequences:
|
||||
en: 100f687b9d498308
|
||||
zh: 1c4bc2d989e43ec7
|
||||
|
||||
@@ -12,19 +12,19 @@ Transactional config reload preserves an old plugin generation after a failed ed
|
||||
|
||||
Revert the five commits in #932, resolving package moves and retaining independent later behavior. The reported merge commit belongs to the larger #936 dependency chain; reverting its first-parent diff would remove unrelated repository-plugin support. The [vendor ledger](../../../../vendor/README.md#local-modifications) records every retained source change against the unchanged pins.
|
||||
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include retains parse validation and patch reapplication, but plugin failures can leave a partially applied tree.
|
||||
Loader changes entry options eagerly. EntryGroup starts siblings concurrently and logs application failures; EntryTree waits for outstanding work without rejecting failed fibers. Neither restores a previous plugin or configuration. Include keeps the running tree after an invalid file, but plugin failures can leave a partially applied tree.
|
||||
|
||||
Application consumers own their completion checks. The CLI waits for its fallback HMR service before installing live patch watchers. The directory chooser checks the entries it mounts. The chooser and browser package runner capture the first fiber-disposal result before removing the entry, then await it before reporting teardown complete. Preset mounting waits for its subtree and reports import, activation, and missing-service failures. [App boot](../../../../packages/boot/app-boot/README.md) owns exact patch-file watching, activation audits, and partial-context cleanup. Web startup audits activation before printing a URL or opening the browser. These adaptations preserve existing consumer behavior after the reverse patch.
|
||||
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Durable Include writes drain before and after child removal so a later teardown write cannot erase an earlier terminal write failure.
|
||||
Fiber, Entry, and isolate keep their upstream update return behavior. App boot observes discarded restart promises through the existing `internal/update` waterfall and waits for fibers before auditing a patch reload. The detached import-completion observer handles both fiber outcomes; the fiber still retains its failure for an explicit audit. Include persistence follows the journal adaptation in the vendor ledger: `flush()` and `stop()` reject while changes remain unsaved.
|
||||
|
||||
Two #932-specific vendor changes remain: awaited initial-file creation and forced rereading in Include, and Schemastery conditional exports. Restoring the pre-#932 debounced write/read sequence reproduces `ENOENT` in the missing-file initialization test. Keeping these two lines preserves the existing `initial` option without an application-side file writer or a second YAML serializer. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators, conditional patch cloning, and update return values use the pre-#932 behavior. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
One #932-specific vendor change remains: Schemastery conditional exports. Include 1.1.1 writes the `initial` file before reading it, so missing-file initialization no longer needs a local change. Removing Schemastery exports reproduces `ERR_REQUIRE_ESM_RACE_CONDITION` while the Web preset suite boots: Node falls back to the CJS entry during concurrent ESM imports. The HMR injection decorators and update return values use the pre-#932 behavior; patch cloning follows upstream Include. Explicit `workspace:` dependencies make the #932 workspace-link switch and dedicated lockfile check unnecessary.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep transactional Loader updates.** They provide automatic recovery from a rejected plugin candidate, but retain the vendored lifecycle machinery being removed. Parse failures can be contained without plugin rollback.
|
||||
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, durable writes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
**Restore every vendored file verbatim.** This would also remove lazy injected config evaluation, conditional disabled entries, lifecycle disposal fixes, and module-loader compatibility. Those changes have independent consumers and remain recorded in the vendor ledger.
|
||||
|
||||
**Move generic rollback into app boot.** This would retain the same candidate-generation and restoration obligations under another owner. Applications instead report failures and allow a later valid edit to recover.
|
||||
|
||||
|
||||
@@ -12,19 +12,19 @@ Status: implemented
|
||||
|
||||
撤销 #932 中的五个提交,解决包移动冲突并保留后续独立行为。记录的合并提交属于更大的 #936 依赖链;撤销其第一父提交差异还会删除无关的仓库插件支持。[Vendor 修改记录](../../../../vendor/README.md#local-modifications) 按不变的固定来源记录每项保留的源码更改。
|
||||
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 保留解析校验和 patch 重应用,但插件失败可能留下部分应用的配置树。
|
||||
Loader 立即更改条目选项。EntryGroup 并发启动同级条目并记录应用失败;EntryTree 等待未完成的工作,但不因失败的 fiber 而拒绝。两者均不恢复旧插件或配置。Include 在文件无效时保留正在运行的配置树,但插件失败可能留下部分应用的配置树。
|
||||
|
||||
应用消费者负责完成检查。CLI 在安装实时 patch 监视器前等待其回退 HMR 服务。目录选择器检查其挂载的条目。选择器和浏览器包运行器在移除条目前取得首次 fiber 释放的结果,并等待该结果后才报告拆卸完成。预设挂载等待其子树,并报告导入、激活和缺少服务的失败。[应用启动](../../../../packages/boot/app-boot/README.zh.md) 负责精确 patch 文件监视、激活检查和部分上下文清理。Web 启动在打印 URL 或打开浏览器前检查激活状态。这些适配维持应用反向补丁后已有的消费者行为。
|
||||
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久写入在删除子条目前后均排空,防止后续拆卸写入掩盖更早的终止性写入失败。
|
||||
Fiber、Entry 和 isolate 保持上游的更新返回行为。App boot 通过现有的 `internal/update` waterfall 观察被丢弃的重启 promise,并在检查 patch 重载前等待 fiber。游离的导入完成观察器处理 fiber 的两种结果;fiber 仍保留失败信息供显式检查。Include 的持久化遵循 vendor 修改记录中的 journal 适配:仍有未保存变更时,`flush()` 和 `stop()` 会拒绝。
|
||||
|
||||
保留两项 #932 专属 vendor 改动:Include 中等待初始文件创建并强制重新读取,以及 Schemastery 条件导出。恢复 #932 前的防抖写入和读取顺序,会在缺失文件初始化测试中复现 `ENOENT`。保留这两行可以维持已有 `initial` 选项,而无需在应用侧增加文件写入器或另一份 YAML 序列化逻辑。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器、条件 patch 克隆及更新返回值采用 #932 前的行为。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
保留一项 #932 专属 vendor 改动:Schemastery 条件导出。Include 1.1.1 会先写入 `initial` 文件再读取,因此缺失文件的初始化不再需要本地改动。移除 Schemastery exports 后,Web preset 测试在启动时复现 `ERR_REQUIRE_ESM_RACE_CONDITION`:并发 ESM 导入使 Node 回退到 CJS 入口。HMR 注入装饰器及更新返回值采用 #932 前的行为;patch 克隆遵循上游 Include。显式 `workspace:` 依赖使 #932 的 workspace 链接开关与专用锁文件检查不再必要。
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
**保留事务化 Loader 更新。** 它们能从被拒绝的插件候选自动恢复,但会保留本次删除的 vendor 生命周期机制。解析失败可以独立于插件回滚进行处理。
|
||||
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复、持久写入和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
**逐字恢复所有 vendor 文件。** 这还会删除延迟注入配置求值、条件禁用条目、生命周期释放修复和模块加载器兼容性。这些更改具有独立消费者,并继续记录在 vendor 修改记录中。
|
||||
|
||||
**将通用回滚移入应用启动。** 这会在另一归属下保留相同的候选代次与恢复义务。应用改为报告失败,并允许后续有效编辑恢复。
|
||||
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Bilingual-pair consistency record for 2026-11-02-deepseek-deferred-tool-failures.md (docs/i18n/README.md): per heading
|
||||
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-11-02-deepseek-deferred-tool-failures.md
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures:
|
||||
en: 1c4af8b4e2ec0e46
|
||||
zh: fcb806d33973025e
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/problem:
|
||||
en: c974406f21854541
|
||||
zh: 701cd64cabdfee79
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/decision:
|
||||
en: 6878caaaebd91a41
|
||||
zh: 2ebd5da04dfb1ac6
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/alternatives-considered:
|
||||
en: d10efff97379cfbb
|
||||
zh: 572b124ddbb2d86d
|
||||
/agent-note-expose-deepseek-deferred-only-request-failures/consequences:
|
||||
en: d60323c595b076f4
|
||||
zh: 1bf1068424777b7e
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: Expose DeepSeek deferred-only request failures
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-11-02-deepseek-deferred-tool-failures.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A synthetic immediate tool can make an all-deferred DeepSeek request acceptable while hiding an invalid tool-update projection. It also changes the request prefix and advertises a tool with no executor.
|
||||
|
||||
## Decision
|
||||
|
||||
The DeepSeek adapter sends deferred declarations unchanged and propagates provider rejection. This replaces the placeholder accommodation described by the [tool-update decision](../architecture/2026-09-20-dynamic-tool-updates.md), whose shared history projection remains authoritative.
|
||||
|
||||
Ordinary addition-only histories start with native tools or an empty set. Removals and first additions to an empty set rebuild native declarations; later deferred additions retain a native baseline. Explicit deferred-only registrations, direct adapter calls, and custom in-history routes can still produce all-deferred requests. Those configurations receive no adapter workaround.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Keep a synthetic immediate tool.** It conceals unexpected deferred-only requests and cannot preserve a prefix that lacked that tool. Automatically activating a real tool instead changes its declared availability.
|
||||
|
||||
**Reject locally.** Provider validation owns acceptance; another local restriction would duplicate it. Reconsider accommodation only for a supported workflow that requires deferred-only requests and cannot retain a native declaration.
|
||||
|
||||
## Consequences
|
||||
|
||||
Deferred-only compatibility is deliberately given up so request errors remain visible. Serializer and HTTP tests verify exact declarations and propagation of a provider rejection. The all-deferred keyless snapshot verifies request construction only; replay success does not establish provider acceptance. Replacement and empty-set scenarios verify the supported addition-only path.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
# Agent Note: 显露 DeepSeek 全延迟请求失败
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-11-02-deepseek-deferred-tool-failures.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
合成的立即加载工具可使 DeepSeek 接受全延迟请求,却掩盖无效的工具更新投影。它还会改变请求前缀,并声明一个没有执行器的工具。
|
||||
|
||||
## Decision
|
||||
|
||||
DeepSeek 适配器原样发送延迟声明,并传递提供方的拒绝错误。这取代了[工具更新决策](../architecture/2026-09-20-dynamic-tool-updates.zh.md)中描述的占位工具兼容机制;其中的共享历史投影仍是权威依据。
|
||||
|
||||
普通 addition-only 历史以原生工具或空集开始。移除工具或首次向空集添加工具会重建原生声明;后续延迟添加保留原生基线。显式全延迟注册、直接调用适配器和自定义 in-history 路由仍可产生全延迟请求。适配器不为这些配置提供变通处理。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**保留合成的立即加载工具。** 它会掩盖意外的全延迟请求,也无法保留原本没有该工具的前缀。改为自动激活真实工具则会改变其声明的可用性。
|
||||
|
||||
**在本地拒绝。** 是否接受请求由提供方验证决定;额外的本地限制会重复该验证。只有受支持工作流确实需要全延迟请求,且无法保留原生声明时,才重新考虑兼容处理。
|
||||
|
||||
## Consequences
|
||||
|
||||
有意放弃全延迟兼容性,使请求错误保持可见。序列化和 HTTP 测试验证精确声明以及提供方拒绝错误的传递。全延迟无密钥快照仅验证请求构造;回放成功不代表提供方接受。替换和空集场景验证受支持的 addition-only 路径。
|
||||
+4
-4
@@ -21,11 +21,11 @@
|
||||
en: f07bb6fbbdd01fe4
|
||||
zh: 9313a0ef2a22af12
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/decision/performance-gate:
|
||||
en: 37faaffb4662fd54
|
||||
zh: df0a4bf016393be9
|
||||
en: 3ceb80f455e9a47d
|
||||
zh: 89d8bd11b42d31b0
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/alternatives-considered:
|
||||
en: 559a8037caa9e3aa
|
||||
zh: 4f3719ed09445cc2
|
||||
en: b5bfb4cfa231ce0f
|
||||
zh: 631b9f2978241bc1
|
||||
/agent-note-linear-credential-matching-and-performance-gates-for-user-text/consequences:
|
||||
en: 8f9d25b72d3e7660
|
||||
zh: e5c6ae0194905876
|
||||
|
||||
@@ -43,7 +43,7 @@ English | [中文](2026-10-11-user-text-credential-matching.zh.md)
|
||||
| Item | Content |
|
||||
|---|---|
|
||||
| Workloads | Without credentials: ordinary messages, and long prose, code, base64, Markdown, log, JSON, and shell text. With credentials: messages each carrying one kind (model key, GitHub token, cloud key ID, JWT, address password, private-key block), and batches of `.env` keys. Adversarial: repeated input shaped against each format's prefix, length, and separator rules, at two sizes. All generated from constants; credential samples are assembled at run time |
|
||||
| Timing | Ten unmeasured pairs warm each workload with the same evaluation count as measured pairs. Scaling inputs share one group: every warm-up and measured round evaluates both sizes adjacently and alternates which size goes first, limiting drift between sizes. Five measured pairs alternate baseline-first and matcher-first order; each evaluation builds a fresh registry, document, and matcher instance and times evaluation only. A single-document sample averages twenty evaluations, or forty for the half-sized input of a scaling pair, so both sizes process the same total keys or characters per sample; a 5,000-message sample evaluates the batch once. Garbage collection during evaluation remains included. Each side selects its minimum sample mean, with the mean process CPU time from that sample. The worker JSON in CI logs includes every measured pair and averaging count; the HTML report shows the selected with-credential mean. Registry disposal completes outside the timer before the next evaluation, preventing cleanup and retained registries from accumulating. Warm-up retains no matcher memo |
|
||||
| Timing | Ten unmeasured pairs warm each workload with the same evaluation count as measured pairs. Scaling inputs share one group: every warm-up and measured round evaluates both sizes adjacently and alternates which size goes first, limiting drift between sizes. Five measured pairs alternate baseline-first and matcher-first order; each evaluation builds a fresh registry, document, and matcher instance and times evaluation only. A single-document sample averages twenty evaluations, or forty for the half-sized input of a scaling pair, so both sizes process the same total keys or characters per sample; a 5,000-message sample evaluates the batch once. Garbage collection during evaluation remains included. Absolute time budgets use each side's minimum sample mean, with the mean process CPU time from that sample. Doubling checks take the median of the five within-round ratios, keeping the adjacent samples paired through aggregation. The worker JSON in CI logs includes every measured pair and averaging count; the HTML report shows the selected with-credential mean. Registry disposal completes outside the timer before the next evaluation, preventing cleanup and retained registries from accumulating. Warm-up retains no matcher memo |
|
||||
| Allocation | A separate process with `--expose-gc` and a fixed semi-space larger than any evaluation; after warm-up and GC, the matcher alone answers every query once and the young-generation growth is taken; GC event timestamps confirm no GC ran inside the interval |
|
||||
| Assertions | Without credentials: absolute budgets and a cap on the with/without time ratio; with credentials: absolute budgets and the time ratio when the key count doubles; adversarial: absolute budgets and doubling ratios; allocation: a floor plus caps per credential, per line break inside masks, and per masked character |
|
||||
| Excluded | Cold code compilation, React rendering, browser paint and layout; allocation results are asserted and printed to the console, not added to the HTML report |
|
||||
@@ -54,7 +54,7 @@ The reference constants are calibrated for minima of five single evaluations. Mi
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Finishing one scaling size before measuring the other**: separate phases compare different load and V8 tiering conditions. Identical code measured JWT doubling at 1.91× in one CI run and 2.70× in another; a local Node 24 run also exceeded the unchanged 2.5× limit on a different input. Adjacent, alternating size rounds retain the existing sample counts, minimum-of-five aggregation, GC inclusion, workloads, and limits. A controlled extra evaluation cost on only the 128 KiB input must still fail the doubling assertion.
|
||||
- **Finishing one scaling size before measuring the other**: separate phases compare different load and V8 tiering conditions. Identical code measured JWT doubling at 1.91× in one CI run and 2.70× in another; a local Node 24 run also exceeded the unchanged 2.5× limit on a different input. Adjacent, alternating size rounds retain the sample counts, GC inclusion, workloads, and limits. Independent minima can still select rounds at different speeds; the median of paired ratios prevents one faster half-sized sample from deciding the scaling result. A controlled extra evaluation cost on only the 128 KiB input must still fail the doubling assertion.
|
||||
- **Equal evaluation counts at both scaling sizes**: the larger input allocates twice as much per sample, so its fastest sample can include a collection while the smaller input's fastest sample includes none. Equal total input per sample makes the per-evaluation means compare similar allocation volumes without excluding GC or changing the doubling limit.
|
||||
- **Unwarmed, separate baseline and matcher batches**: sub-millisecond baseline measurements can reach optimized code before the matcher’s newly exercised paths do, and separate batches amplify clock-speed drift. Per-workload warm-up, adjacent pairs, and fixed batches compare warmed code while limiting noise in short intervals and preserving fresh documents and matcher state.
|
||||
- **One quantified pattern per format, run from every query**: patterns without a hit scan to the end of the text each time, so cost grows quadratically with credentials, and length quantifiers backtrack on failure.
|
||||
|
||||
@@ -43,7 +43,7 @@ Status: implemented
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 负载 | 无凭据:普通消息,以及散文、代码、base64、Markdown、日志、JSON、shell 长文本。含凭据:每条消息含一种凭据(模型密钥、GitHub 令牌、云密钥 ID、JWT、地址密码、私钥块),以及成批的 `.env` 密钥。对抗:针对每种格式的前缀、长度与分隔规则构造的重复输入,两种长度。全部由常量生成,凭据样本运行时拼接 |
|
||||
| 计时 | 每个负载先用十组未计入结果的配对预热,每组的求值次数与测量配对相同。翻倍比较的两个输入属于同一组:每轮预热和测量相邻求值两个尺寸,并交替先测的尺寸,减少尺寸之间的漂移。五组测量交替以基线或凭据匹配器开头;每次求值新建注册表、文档和匹配器实例,只计求值。单文档样本取二十次求值的平均值;翻倍比较中的小输入取四十次,使两种大小的每个样本处理相同总量的密钥或字符;5,000 条消息的样本则整批求值一次。求值期间的垃圾回收仍计入耗时。两侧各取样本均值的最小值,并保留对应样本的平均进程 CPU 时间。CI 日志中的 worker JSON 包含全部测量配对和求平均值的次数,HTML 报告展示选中的带凭据匹配器均值。每次求值在计时结束后等待注册表清理完成,再开始下一次,避免清理工作和未释放注册表积累。预热不保留匹配器记忆 |
|
||||
| 计时 | 每个负载先用十组未计入结果的配对预热,每组的求值次数与测量配对相同。翻倍比较的两个输入属于同一组:每轮预热和测量相邻求值两个尺寸,并交替先测的尺寸,减少尺寸之间的漂移。五组测量交替以基线或凭据匹配器开头;每次求值新建注册表、文档和匹配器实例,只计求值。单文档样本取二十次求值的平均值;翻倍比较中的小输入取四十次,使两种大小的每个样本处理相同总量的密钥或字符;5,000 条消息的样本则整批求值一次。求值期间的垃圾回收仍计入耗时。绝对耗时预算取两侧样本均值的最小值,并保留对应样本的平均进程 CPU 时间。翻倍检查取五个同轮次耗时比的中位数,使相邻样本在汇总时仍保持配对。CI 日志中的 worker JSON 包含全部测量配对和求平均值的次数,HTML 报告展示选中的带凭据匹配器均值。每次求值在计时结束后等待注册表清理完成,再开始下一次,避免清理工作和未释放注册表积累。预热不保留匹配器记忆 |
|
||||
| 分配 | 单独进程,`--expose-gc`,semi-space 固定为大于任何一次求值的大小;预热后 GC,再单独驱动匹配器回答全部查询一遍,取新生代增量;用 GC 事件的时间戳确认区间内没有 GC |
|
||||
| 断言 | 无凭据:绝对预算,以及"有/无凭据匹配器"的耗时比上限;含凭据:绝对预算,密钥数翻倍时的耗时比;对抗:绝对预算与翻倍耗时比;分配:底噪加每个凭据、每个遮挡内换行和每个遮挡字符的上限 |
|
||||
| 排除 | 冷代码编译、React 渲染、浏览器绘制与布局;分配结果只用于断言并输出到控制台,不进 HTML 报告 |
|
||||
@@ -54,7 +54,7 @@ Status: implemented
|
||||
|
||||
## 考虑过的替代方案
|
||||
|
||||
- **测完一个翻倍尺寸后再测另一个**:两个阶段比较的负载和 V8 编译层级状态不同。同一份代码的 JWT 翻倍比在两轮 CI 中分别为 1.91 倍和 2.70 倍;本地 Node 24 也在另一个输入上超过既有 2.5 倍上限。相邻、交替的尺寸轮次保留既有样本数、五组取最小值的汇总方式、GC 计入规则、负载与上限。仅给 128 KiB 输入增加受控求值开销时,翻倍断言仍须失败。
|
||||
- **测完一个翻倍尺寸后再测另一个**:两个阶段比较的负载和 V8 编译层级状态不同。同一份代码的 JWT 翻倍比在两轮 CI 中分别为 1.91 倍和 2.70 倍;本地 Node 24 也在另一个输入上超过既有 2.5 倍上限。相邻、交替的尺寸轮次保留样本数、GC 计入规则、负载与上限。独立选取最小值仍可能选中运行速度不同的轮次;配对比值的中位数避免单个更快的小输入样本决定翻倍结果。仅给 128 KiB 输入增加受控求值开销时,翻倍断言仍须失败。
|
||||
- **两种大小使用相同求值次数**:大输入每个样本的分配量翻倍,因而其最快样本可能包含一次垃圾回收,而小输入的最快样本没有。让每个样本的输入总量相等,使单次求值均值比较相近的分配量,无需排除垃圾回收或修改翻倍上限。
|
||||
- **不预热、分批测量基线和匹配器**:亚毫秒基线测量可能先进入优化后的代码,而匹配器新触及的路径还未完成优化;分批测量也会放大时钟频率漂移。逐负载预热、相邻配对和固定批次比较预热后的代码,降低短区间测量的噪声,同时保持文档和匹配器状态全新。
|
||||
- **每次查询对每个格式各跑一个带量词的正则**:没有命中的正则每次都扫到文末,凭据多时平方增长;长度量词在失败时回溯。
|
||||
|
||||
+1
-1
@@ -4,6 +4,6 @@ Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. Native Win
|
||||
|
||||
The manual [Windows packaging workflow](workflows/windows-package.yml) uses the dedicated self-hosted Windows x64 label `dsh-win-package-trial` under native `pwsh`; preserve this routing separately from the automatic CI lanes.
|
||||
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` so their definitions come from master, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check`, which also publishes the review verdict as the check run `Policy / Reviewer Check`, and `pr-check-on-base-branch.yml` on master pushes re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each job reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, and `MERGE_POLICY_HEAD_DIFF`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `MERGE_POLICY_HEAD_DIFF` decides whether `Policy / Approve Check` runs and fails; while it runs, `Policy / Reviewer Check` is published whatever that mode says, with `success` when every file that needs review has one and `action_required` with the reviewers to ask otherwise. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
The merge checks run from three workflows named `Policy`, which run on `pull_request_target` and cover pull requests into master or a `release/dsh-x.y.z` branch, so their definitions come from the base branch, and call the `deepseek-harness-check` action directly: `pr-check-on-head-branch.yml` on pull-request updates gives `Policy / Master Change` and `Policy / Pre-Merge Check`, `pr-check-after-approve.yml` on pull-request updates and submitted or dismissed reviews gives `Policy / Approve Check`, which also publishes the review verdict as the check run `Policy / Reviewer Check`, and `pr-check-on-base-branch.yml` on pushes to master or a release branch re-runs the head-branch checks as `Policy / Recheck`. Branch rules require those check names, so keep the job names and the workflow name. Each job reads its mode from a repository variable: `MERGE_POLICY_MASTER_DIFF`, `MERGE_POLICY_PRE_MERGE`, and `MERGE_POLICY_HEAD_DIFF`, each `off` (skip), `report` (run without failing the job), or `block` (fail the job); a missing value or an unknown one counts as `report`. `MERGE_POLICY_HEAD_DIFF` decides whether `Policy / Approve Check` runs and fails; while it runs, `Policy / Reviewer Check` is published whatever that mode says, with `success` when every file that needs review has one and `action_required` with the reviewers to ask otherwise. `Policy / Pre-Merge Check` merges the commit `Policy / Master Change` reports, so it runs only after that job and stays off while `MERGE_POLICY_MASTER_DIFF` is `off`; a merge it cannot do and a build that fails both follow its own mode, so `report` warns and skips the build while `block` fails the job. `Policy / Pre-Merge Check` runs `scripts/pre-merge-check.ts` under plain Node and knows nothing else about the merged tree: that script checks the released manifests against the root version, installs, and builds the Host and Client contracts, so change it in one pull request together with the build it orchestrates.
|
||||
|
||||
The manual Desktop publication workflow selects a successful package run and attempt, then routes each retained build through `DESKTOP_PUBLISH_RUNNERS` to its original machine's unique label. Keep Windows run steps on native PowerShell and Mac run steps on the default POSIX shell; shared entrypoints are Node scripts. Test and production jobs use separate `desktop-test` and `desktop-production` approval Environments after a credential-free preparation job.
|
||||
|
||||
@@ -73,6 +73,12 @@ jobs:
|
||||
- name: Run blocking Windows builds
|
||||
shell: pwsh
|
||||
run: pnpm run check:ci:windows-blocking
|
||||
- name: Verify Desktop updater dependency versions
|
||||
shell: pwsh
|
||||
run: |
|
||||
node --test apps/desktop/tests/updates/electron-updater/versions.test.mjs
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
node apps/desktop/tests/updates/electron-updater/check-versions.mjs
|
||||
- name: Parse Windows runner supervision scripts
|
||||
shell: pwsh
|
||||
run: |
|
||||
|
||||
@@ -15,9 +15,9 @@ on:
|
||||
default: test
|
||||
required: true
|
||||
update_channel:
|
||||
description: '升级通道:nightly 或 latest(须为稳定版本);自动更新不会切换通道'
|
||||
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
|
||||
type: choice
|
||||
options: [nightly, latest]
|
||||
options: [nightly, stable]
|
||||
default: nightly
|
||||
required: true
|
||||
version_mode:
|
||||
@@ -127,6 +127,23 @@ jobs:
|
||||
args=()
|
||||
if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi
|
||||
pnpm run package:desktop:mac:x64 --check --build-version "$PACKAGE_VERSION" "${args[@]}"
|
||||
- name: Build and stage notarized Computer Use helpers
|
||||
# A source whose dsh ships the Computer Use bundle packs the workspace provider into the Desktop runtime,
|
||||
# which therefore needs the same notarized helper npm publication stages.
|
||||
run: |
|
||||
if ! node -e "process.exit(require('./apps/cli/package.json').dependencies?.['@deepseek-ai/dsh-cua-bundle'] ? 0 : 1)"; then
|
||||
echo 'This source does not ship Computer Use in Desktop; no helper is staged.'
|
||||
exit 0
|
||||
fi
|
||||
if [[ ! -f scripts/cua-helper/package.ts ]]; then
|
||||
echo '::warning::This source ships Computer Use but predates helper packaging; Computer Use cannot prepare its helper in this package.'
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p dist
|
||||
pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper
|
||||
mkdir dist/cua-helper/extracted
|
||||
tar -xzf dist/cua-helper/cua-helper.tgz -C dist/cua-helper/extracted
|
||||
pnpm exec tsx scripts/cua-helper/stage.ts --from dist/cua-helper/extracted
|
||||
- name: Build signed installers
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.settings.outputs.version }}
|
||||
|
||||
@@ -15,9 +15,9 @@ on:
|
||||
default: test
|
||||
required: true
|
||||
update_channel:
|
||||
description: '升级通道:nightly 或 latest(须为稳定版本);自动更新不会切换通道'
|
||||
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
|
||||
type: choice
|
||||
options: [nightly, latest]
|
||||
options: [nightly, stable]
|
||||
default: nightly
|
||||
required: true
|
||||
version_mode:
|
||||
@@ -124,6 +124,23 @@ jobs:
|
||||
args=()
|
||||
if [[ "$PACKAGE_SKIP_NOTARIZATION" = true ]]; then args+=(--skip-notarization); fi
|
||||
pnpm run package:desktop:mac:arm64 --check --build-version "$PACKAGE_VERSION" "${args[@]}"
|
||||
- name: Build and stage notarized Computer Use helpers
|
||||
# A source whose dsh ships the Computer Use bundle packs the workspace provider into the Desktop runtime,
|
||||
# which therefore needs the same notarized helper npm publication stages.
|
||||
run: |
|
||||
if ! node -e "process.exit(require('./apps/cli/package.json').dependencies?.['@deepseek-ai/dsh-cua-bundle'] ? 0 : 1)"; then
|
||||
echo 'This source does not ship Computer Use in Desktop; no helper is staged.'
|
||||
exit 0
|
||||
fi
|
||||
if [[ ! -f scripts/cua-helper/package.ts ]]; then
|
||||
echo '::warning::This source ships Computer Use but predates helper packaging; Computer Use cannot prepare its helper in this package.'
|
||||
exit 0
|
||||
fi
|
||||
mkdir -p dist
|
||||
pnpm exec tsx scripts/cua-helper/package.ts --out dist/cua-helper
|
||||
mkdir dist/cua-helper/extracted
|
||||
tar -xzf dist/cua-helper/cua-helper.tgz -C dist/cua-helper/extracted
|
||||
pnpm exec tsx scripts/cua-helper/stage.ts --from dist/cua-helper/extracted
|
||||
- name: Build signed installers
|
||||
env:
|
||||
PACKAGE_VERSION: ${{ steps.settings.outputs.version }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
pull_request_review:
|
||||
types: [submitted, dismissed]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
workflow_dispatch:
|
||||
permissions: {}
|
||||
concurrency:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
name: Policy
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: [master]
|
||||
branches: [master, 'release/dsh-[0-9]+.[0-9]+.[0-9]+']
|
||||
types: [opened, synchronize, reopened, ready_for_review, review_requested, auto_merge_enabled, labeled]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -15,9 +15,9 @@ on:
|
||||
default: test
|
||||
required: true
|
||||
update_channel:
|
||||
description: '升级通道:nightly 或 latest(须为稳定版本);自动更新不会切换通道'
|
||||
description: '升级通道:nightly 或 stable(须为稳定版本);默认通道由此打包输入确定'
|
||||
type: choice
|
||||
options: [nightly, latest]
|
||||
options: [nightly, stable]
|
||||
default: nightly
|
||||
required: true
|
||||
version_mode:
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"@deepseek-ai/dsh-experimental-liveshare": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-session-search": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-tool-worktree": "workspace:*",
|
||||
"@deepseek-ai/dsh-cua-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-plugin-market-bundle": "workspace:*",
|
||||
"@deepseek-ai/dsh-personalized-recommendations": "workspace:*",
|
||||
"@deepseek-ai/dsh-experimental-forge-bundle": "workspace:*"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# After editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md
|
||||
/dsh-cli-behavior-reference:
|
||||
en: 5a4f43756dae900e
|
||||
en: abffba1bcc44698c
|
||||
zh: d58c479481cfa5fe
|
||||
/dsh-cli-behavior-reference/profile-boot:
|
||||
en: b474a6fc3c98837d
|
||||
@@ -24,8 +24,8 @@
|
||||
en: 149c5571acd51baf
|
||||
zh: 300e3c2425273a92
|
||||
/dsh-cli-behavior-reference/shared-deployment-behavior:
|
||||
en: f40edcf903a51f3f
|
||||
zh: 8a9a915451047c8b
|
||||
en: f02219118dcec531
|
||||
zh: fe2977628d7340da
|
||||
/dsh-cli-behavior-reference/source-execution:
|
||||
en: 05c2b23790f7b296
|
||||
zh: 51f809020fb230a1
|
||||
en: 0fd6c72ba9a7eb3b
|
||||
zh: 011c0ac4008ee480
|
||||
|
||||
@@ -6,6 +6,8 @@ This reference defines the profile, plugin-management, and config-dump command m
|
||||
|
||||
The [generated command help](../../../docs/cli-help.md) contains complete launcher, plugin-management, and shipped-profile help. Run `pnpm run gen-cli-help` to refresh it; `pnpm run verify-cli-help` checks it against the current CLI.
|
||||
|
||||
<a id="profile-boot"></a>
|
||||
|
||||
## Profile boot
|
||||
|
||||
`dsh <name>` abbreviates `dsh --profile <name>` and boots the profile at `$DSH_HOME/profiles/<name>`. The shorthand name must immediately follow `dsh`; `plugin` remains the plugin-management command, so boot a profile with that name using `dsh --profile plugin`. The effective tree is composed over an empty root by applying, in order: each bundle patch named in the profile manifest's `dsh.profile.bundles` list, the profile's own `cordis.patch.yml`, the home-level `$DSH_HOME/cordis.patch.yml` (machine-local preferences shared by every profile, so it outranks the per-profile layer), and each `--patch <path>` overlay in argv order. Later layers win per row; a patch replaces the targeted row's complete `config` value rather than deep-merging keys, and may insert new rows. The final YAML composition controls whether `dsh-hmr` watches configuration; without HMR, changes require restart. A parse, schema, resolution, or plugin boot failure is reported and exits nonzero. SIGINT and SIGTERM dispose the mounted root before exit.
|
||||
@@ -134,7 +136,7 @@ Web, headless, and SDK inherit native tool execution from the base bundle; use a
|
||||
|
||||
## Shared deployment behavior
|
||||
|
||||
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the HTTP fetch provider, default-on DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. Search uses `DEEPSEEK_API_KEY` and accepts `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation and can reach loopback and private-network hosts. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
|
||||
The base bundle mounts the native DeepSeek adapter, settings and credential providers, stable `web_search` and `web_fetch`, the HTTP fetch provider, default-on DeepSeek session-log upload, and feedback-gated OTel upload for all users. Provider credentials resolve from the inherited environment, `$DSH_HOME/.credentials.yaml`, the invoking directory's `.env`, then `$DSH_HOME/.env`; the managed document is never materialized into `process.env`, while both `.env` files are ordinary launch environment layers. The DeepSeek Search and Fetch APIs follow the session's DeepSeek credentials; custom endpoints and keys can only be set in YAML through `web-deepseek-api.searchBaseURL` / `searchApiKeyEnv` and `fetchBaseURL` / `fetchApiKeyEnv` ([authentication](../../../packages/web/web-deepseek-api/README.md#authentication)). Legacy Messages search reads `DEEPSEEK_SEARCH_BASE_URL`. Enabled fetch calls run in every sandbox and approval mode without per-call confirmation and can reach loopback and private-network hosts. The Web app disables the base tool row and exposes the same tools through its `cordis`, `ptc`, and `standard` agent presets.
|
||||
|
||||
Feedback is recorded in the Session log without starting model work. The [DeepSeek session-log contributor](../../../packages/session/session-log-deepseek/README.md) sends complete unaccepted log suffixes with subsequent DeepSeek requests by default, including requests sent through configured gateways; turn off **Settings → General → Upload Session Log when using the official model API** in Web, or set its `enabled` configuration to `false` to opt out. [OTel session upload](../../../packages/session/session-telemetry-otel/README.md) applies to all users and providers, including `deepseek-official`, without requiring a request header. The base defaults to `FEEDBACK_ONLY`: new own text feedback, message ratings, edits, and withdrawals release the complete canonical prefix through that event, including stored context; later records wait for the next explicit feedback. Inherited parent feedback does not authorize a fork. Requests, restoration, mount, and HMR do not trigger capture. SDK batching may finish an authorized upload without further interaction or model work. `DSH_TELEMETRY_MODE=DISABLED` disables OTel delivery; `FULL` is rejected, and any non-empty `DSH_TELEMETRY_DISABLED` disables its row. `DSH_TELEMETRY_OTLP_URL` selects the collector. Handoff is best-effort, not collector acceptance; no durable outbox or retry guarantee is provided. These OTel settings do not enable or disable the DeepSeek contribution. Neither path changes model input, but exports can include message text, tool arguments and results, and workspace paths.
|
||||
|
||||
@@ -143,4 +145,4 @@ Install external plugin bundles through `dsh plugin --profile <name> add <packag
|
||||
<a id="source-execution"></a>
|
||||
## Source execution
|
||||
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment, and `runProfile` resolves the outbound proxy from that snapshot before any entry mounts, so `HTTP_PROXY`/`HTTPS_PROXY` (and a proxy declared in a `.env` layer) apply without any further flag. The source entry explicitly selects linked profile resolution so tsx workspace imports and profile plugins share module identities. The installed form launches the built `apps/cli/lib/bin.js` with runtime profile resolution without rebuilding the repository.
|
||||
From the repository root, run `pnpm run build` separately after a fresh checkout and whenever artifacts need updating, then use `pnpm dsh <args...>`. The `package.json` script launches `apps/cli/src/bin.ts` with `node --import tsx/esm` without building and forwards every argument. Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend or client-plugin bundles fail at startup with an instruction to run `pnpm run build`. The launcher does not check freshness, so existing stale bundles can run older browser code until rebuilt. The process inherits the launch environment, and `runProfile` resolves the outbound proxy from that snapshot before any entry mounts, so `HTTP_PROXY`/`HTTPS_PROXY` (and a proxy declared in a `.env` layer) apply without any further flag. The source entry explicitly selects linked profile resolution so tsx workspace imports and profile plugins share module identities. On Windows, the installed command expands 8.3 paths before loading profile dependencies. The installed form launches the built `apps/cli/lib/bin.js` with runtime profile resolution without rebuilding the repository.
|
||||
|
||||
@@ -136,7 +136,7 @@ Web、headless 与 SDK 继承 base bundle 的原生工具执行;使用 [PTC ov
|
||||
|
||||
## 共享部署行为
|
||||
|
||||
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、HTTP fetch 提供方,默认开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。搜索使用 `DEEPSEEK_API_KEY` 并接受 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认,并且可以访问 loopback 与内网主机。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
|
||||
基础组合包挂载原生 DeepSeek 适配器、settings 与凭据提供方、稳定的 `web_search` 和 `web_fetch`、HTTP fetch 提供方,默认开启的 DeepSeek 会话日志上传,以及面向所有用户的反馈门控 OTel 上传。提供方凭据依次从继承环境、`$DSH_HOME/.credentials.yaml`、调用目录的 `.env` 和 `$DSH_HOME/.env` 解析;受管文档从不物化进 `process.env`,而两个 `.env` 文件都是普通启动环境层。DeepSeek Search 与 Fetch API 跟随会话的 DeepSeek 凭据;自定义 endpoint 和密钥只能在 YAML 中通过 `web-deepseek-api.searchBaseURL` / `searchApiKeyEnv` 和 `fetchBaseURL` / `fetchApiKeyEnv` 设置([鉴权](../../../packages/web/web-deepseek-api/README.zh.md#authentication))。旧版 Messages 搜索读取 `DEEPSEEK_SEARCH_BASE_URL`。已启用的抓取调用会在所有 sandbox 与审批模式下执行,无需逐次确认,并且可以访问 loopback 与内网主机。Web app 会禁用 base 工具配置项,再通过 `cordis`、`ptc` 与 `standard` agent preset 暴露相同工具。
|
||||
|
||||
反馈记录在会话日志中,不会启动模型工作。[DeepSeek 会话日志贡献器](../../../packages/session/session-log-deepseek/README.zh.md)默认随后续 DeepSeek 请求发送尚未确认接收的完整日志后缀,包括经已配置网关发送的请求;可在 Web 的**设置 → 通用 → 在使用官方模型 API 时上传 Session Log**中关闭上传,或将其 `enabled` 配置设为 `false`。[OTel 会话上传](../../../packages/session/session-telemetry-otel/README.zh.md)适用于所有用户和提供方,包括 `deepseek-official`,无需请求头。基础配置默认使用 `FEEDBACK_ONLY`:新的自身文本反馈、消息评分、编辑与撤回会释放截至该事件的完整规范日志前缀,包含存储的上下文;后续记录等待下一次显式反馈。继承的父级反馈不构成 fork 的授权。请求、恢复、挂载和 HMR 不触发捕获。SDK 批处理可完成已授权上传,无需进一步交互或模型工作。`DSH_TELEMETRY_MODE=DISABLED` 禁止 OTel 投递;`FULL` 被拒绝,任何非空的 `DSH_TELEMETRY_DISABLED` 都会禁用其配置行。`DSH_TELEMETRY_OTLP_URL` 选择采集端。交接尽力而为,不代表采集端接受;不提供持久化 outbox 或重试保证。这些 OTel 设置不会开启或关闭 DeepSeek 贡献。两条路径都不改变模型输入,但导出可能包含消息文本、工具参数和结果,以及工作区路径。
|
||||
|
||||
@@ -145,4 +145,4 @@ Web、headless 与 SDK 继承 base bundle 的原生工具执行;使用 [PTC ov
|
||||
<a id="source-execution"></a>
|
||||
## 源码执行
|
||||
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境,且 `runProfile` 会在任何 entry 挂载之前从该快照解析出站代理,因此 `HTTP_PROXY`/`HTTPS_PROXY`(以及写在 `.env` 层中的代理)无需任何额外开关即可生效。源码入口显式选择链接式 profile 解析,使 tsx workspace 导入与 profile 插件共享模块身份。安装形式会使用 runtime profile 解析直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
请在仓库根目录中,于全新 checkout 之后及产物需要更新时单独运行 `pnpm run build`,然后使用 `pnpm dsh <args...>`。`package.json` 中的脚本不会构建,而是通过 `node --import tsx/esm` 启动 `apps/cli/src/bin.ts`,并转发所有参数。Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 组合包缺失,启动会失败并提示运行 `pnpm run build`。启动器不会检查产物是否为最新,因此已有的陈旧组合包可能继续运行旧版浏览器代码,直至重新构建。该进程会继承启动环境,且 `runProfile` 会在任何 entry 挂载之前从该快照解析出站代理,因此 `HTTP_PROXY`/`HTTPS_PROXY`(以及写在 `.env` 层中的代理)无需任何额外开关即可生效。源码入口显式选择链接式 profile 解析,使 tsx workspace 导入与 profile 插件共享模块身份。在 Windows 上,安装版命令在加载 profile 依赖前展开 8.3 短路径。安装形式会使用 runtime profile 解析直接启动构建后的 `apps/cli/lib/bin.js`,不会重新构建仓库。
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Installed CLI entry for disk launchers and embedded runtimes.
|
||||
* @module @deepseek-ai/dsh/installed-bin
|
||||
*/
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { RunCliOptions } from './bin.ts'
|
||||
|
||||
/**
|
||||
* Run the CLI in the importing carrier's module tree.
|
||||
* @param options - Installation-owned runtime options.
|
||||
* @returns Settlement of the selected command.
|
||||
*/
|
||||
export async function runCli(options?: RunCliOptions): Promise<void> {
|
||||
const cli = await import('./bin.ts')
|
||||
await cli.runCli(options)
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
// Windows ESM can retain an 8.3 spelling while profile resolution expands it.
|
||||
const entry = process.platform === 'win32' ? pathToFileURL(realpathSync.native(import.meta.filename)).href : import.meta.url
|
||||
if (entry === import.meta.url) await runCli()
|
||||
else {
|
||||
const canonical = await import(entry) as typeof import('./installed-bin.ts')
|
||||
await canonical.runCli()
|
||||
}
|
||||
}
|
||||
@@ -344,6 +344,20 @@ function startStartupProfile(fixture: StartupFixture, args: readonly string[]) {
|
||||
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
|
||||
testProfileResolution('lib')
|
||||
|
||||
it.runIf(process.platform === 'win32')('resolves the installed entry before loading CLI dependencies', async () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'dsh-cli-alias-'))
|
||||
try {
|
||||
const alias = join(home, 'launcher')
|
||||
symlinkSync(join(repoRoot, 'apps/cli/lib'), alias, 'junction')
|
||||
const { stdout } = await execa(process.execPath, [
|
||||
'--preserve-symlinks-main', join(alias, 'bin.js'), '--version',
|
||||
], { timeout: SPAWN_TIMEOUT_MS, env: { DSH_HOME: home, DSH_TELEMETRY_DISABLED: '1' } })
|
||||
expect(stdout).toBe(cliVersion)
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true })
|
||||
}
|
||||
}, SPAWN_TIMEOUT_MS + 5_000)
|
||||
|
||||
it('requires a profile and rejects removed flags', async () => {
|
||||
const bare = await runBuiltBin()
|
||||
expect(bare.code).toBe(1)
|
||||
|
||||
@@ -40,3 +40,8 @@
|
||||
- id: session-log-deepseek
|
||||
config:
|
||||
enabled: false
|
||||
|
||||
# The recorded tool catalog includes the optional patch tool.
|
||||
- id: tool-fs
|
||||
config:
|
||||
applyPatch: true
|
||||
|
||||
@@ -42,8 +42,14 @@
|
||||
mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
|
||||
workspaceRoot: !!js process.cwd()
|
||||
|
||||
# A search key reference no runner sets: the turn runs as a deployment without
|
||||
# web search credentials, whatever the launching environment exports.
|
||||
# Own key references no runner sets on the DeepSeek Search and Fetch APIs: the
|
||||
# turn runs as a deployment without web search credentials, whatever
|
||||
# DEEPSEEK_API_KEY the launching environment exports.
|
||||
- id: web-deepseek-api
|
||||
config:
|
||||
searchApiKeyEnv: DSH_KEYLESS_SMOKE_SEARCH_KEY
|
||||
fetchApiKeyEnv: DSH_KEYLESS_SMOKE_FETCH_KEY
|
||||
|
||||
- id: web-search-deepseek
|
||||
config:
|
||||
apiKeyEnv: DSH_KEYLESS_SMOKE_SEARCH_KEY
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('headless-agent keyless smoke', () => {
|
||||
expect(String(result?.['output'])).toContain('CLI_TOOL_ROUND_TRIP')
|
||||
expect(persistedHeader).toMatchObject({ type: 'session' })
|
||||
expect(persistedToolNames).toEqual(expect.arrayContaining(['read', 'write', 'edit', 'web_fetch']))
|
||||
// Search has no key in this composition, so the model request does not offer it.
|
||||
// Neither DeepSeek search provider has a key in this composition, so the model request does not offer search.
|
||||
expect(persistedToolNames).not.toContain('web_search')
|
||||
expect(persistedToolNames).not.toContain('str_replace_editor')
|
||||
}, PRODUCTION_PROFILE_TEST_TIMEOUT_MS)
|
||||
|
||||
@@ -57,6 +57,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
{ label: 'reports max-token turns with mapping enabled through env', envValue: 'true', editorEnabled: false },
|
||||
{ label: 'reports max-token turns with mapping disabled through env', envValue: 'false', editorEnabled: false },
|
||||
{ label: 'allows an explicit patch to enable str_replace_editor', envValue: undefined, editorEnabled: true },
|
||||
{ label: 'allows an explicit patch to enable apply_patch', envValue: undefined, editorEnabled: false, patchEnabled: true },
|
||||
{ label: 'uses the PTC executor overlay despite the retired environment selector', envValue: undefined, editorEnabled: false, ptc: true },
|
||||
// The shipped sdk profile mounts no observation policy: an edit of a file the model never read lands on disk.
|
||||
{ label: 'edits a file the model never read through the shipped profile', envValue: undefined, editorEnabled: false, unreadEdit: true },
|
||||
@@ -64,11 +65,14 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const { envValue, editorEnabled } = scenario
|
||||
const ptc = 'ptc' in scenario && scenario.ptc
|
||||
const unreadEdit = 'unreadEdit' in scenario && scenario.unreadEdit
|
||||
const patchEnabled = 'patchEnabled' in scenario && scenario.patchEnabled
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), 'dsh-python-sdk-runtime-smoke-'))
|
||||
const unreadFile = join(root, 'notes.txt')
|
||||
if (unreadEdit) await writeFile(unreadFile, 'alpha\n')
|
||||
const editorPatch = join(root, 'editor.patch.yml')
|
||||
const fsPatch = join(root, 'fs.patch.yml')
|
||||
if (patchEnabled) await writeFile(fsPatch, '- {id: tool-fs, config: {applyPatch: true}}\n')
|
||||
if (editorEnabled) await writeFile(editorPatch, [
|
||||
'- insert:',
|
||||
' - id: tool-str-replace-editor',
|
||||
@@ -103,6 +107,7 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
'--profile',
|
||||
'sdk',
|
||||
...(editorEnabled ? ['--patch', editorPatch] : []),
|
||||
...(patchEnabled ? ['--patch', fsPatch] : []),
|
||||
...(ptc ? ['--patch', join(repoRoot, 'apps/cli/config/examples/ptc.yml')] : []),
|
||||
], {
|
||||
cwd: repoRoot,
|
||||
@@ -189,16 +194,16 @@ describe('Python SDK dsh profile keyless smoke', () => {
|
||||
const toolNames = tools.map(tool => tool.name)
|
||||
expect(modelRequests[0]?.output_config).toEqual({ effort: 'max' })
|
||||
expect(modelRequests[0]?.max_tokens).toBe(1234)
|
||||
const expectedTools = ['read', 'write', 'edit', 'apply_patch', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
const expectedTools = ['read', 'write', 'edit', 'web_fetch', 'web_search', 'subagent', 'subagent_fork',
|
||||
'present', 'ask_user_question', 'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view', 'plugin_manager']
|
||||
const requestText = JSON.stringify(modelRequests[0])
|
||||
if (ptc) {
|
||||
expect(toolNames).toEqual(['run_code'])
|
||||
// The PTC overlay keeps the pre-apply_patch tool set, as the Web ptc preset does.
|
||||
for (const tool of expectedTools.filter(tool => tool !== 'apply_patch')) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
for (const tool of expectedTools) expect(requestText.includes(`${tool}:`), tool).toBe(true)
|
||||
expect(requestText.includes('apply_patch:')).toBe(false)
|
||||
} else {
|
||||
expect(toolNames).toEqual(expect.arrayContaining(expectedTools))
|
||||
expect(toolNames.includes('apply_patch')).toBe(patchEnabled)
|
||||
expect(toolNames.includes('str_replace_editor')).toBe(editorEnabled)
|
||||
expect(toolNames).not.toContain('list_subagent_models')
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ describe('the shipped Web composition', () => {
|
||||
// excluded for the reason the TUI composition e2e excludes them — they
|
||||
// depend on ripgrep being present on the machine.
|
||||
expect(toolNames(ctx, handle.agent).filter(name => name !== 'glob' && name !== 'grep')).toEqual([
|
||||
'apply_patch', 'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'ask_user_question', 'bash', 'create_goal', 'edit', 'exit_plan_mode',
|
||||
'get_goal', 'interrupt_agent', 'job_kill', 'job_list', 'job_output', 'list_agents', 'list_subagent_models', 'plugin_manager', 'present', 'read', 'read_image',
|
||||
'schedule_create', 'schedule_edit', 'schedule_list', 'schedule_view',
|
||||
'send_message', 'skill',
|
||||
|
||||
@@ -5,7 +5,7 @@ import { defineConfig } from 'tsdown'
|
||||
* Declarations come from `tsc -b` (dts: false), matching every package.
|
||||
*/
|
||||
export default defineConfig({
|
||||
entry: ['lib/types/bin.js', 'lib/types/profile-boot.js'],
|
||||
entry: { bin: 'lib/types/installed-bin.js', 'profile-boot': 'lib/types/profile-boot.js' },
|
||||
outDir: 'lib',
|
||||
format: ['esm'],
|
||||
platform: 'node',
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
en: 127ea90553cb6af7
|
||||
zh: 7b1c5aec8558c684
|
||||
/deepseek-harness-desktop/application-overview:
|
||||
en: 1667e3d6ffddea4a
|
||||
zh: 4661a42b9ef4f673
|
||||
en: d4afa46403997884
|
||||
zh: 9b9fe126afda70f0
|
||||
/deepseek-harness-desktop/terminal-command:
|
||||
en: 5f2d25a1d8472647
|
||||
zh: 2acb5c5d7d1917e2
|
||||
@@ -60,8 +60,8 @@
|
||||
en: 67bf0a18faa7aa76
|
||||
zh: 782e14ce62bb939f
|
||||
/deepseek-harness-desktop/updates/local-desktop-settings:
|
||||
en: 2e57457c44c85b5f
|
||||
zh: 5b8510a3dc4c6c9d
|
||||
en: 01461be5b81e8609
|
||||
zh: 58e818df821a3aa2
|
||||
/deepseek-harness-desktop/updates/mandatory-update-policy:
|
||||
en: 182a9c6705213e13
|
||||
zh: 681762f83490f4c6
|
||||
|
||||
+12
-2
@@ -35,7 +35,7 @@ Electron caches the last confirmed UI language in `userData/desktop/locale-cache
|
||||
|
||||
Desktop analytics follows the [product collection policy](../../packages/client/product-analytics/README.md), including its live application setting. Web usage is excluded. Update installation waits for its local analytics intake before locking API admission and stopping the Host. Intake has a one-second deadline; failure does not prevent installation, and collector delivery is not awaited.
|
||||
|
||||
The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s `3080` or a reserved port; a `webserver.config.port` patch can override it.
|
||||
The desktop application is an Electron shell around the complete dsh Web application. An Electron RunAsNode child starts the shared profile runner, and Electron immediately loads the packaged Web entry at `dsh-app://app/`. Its shared loading page waits for Host boot injections, then starts the client without navigating to another document. Electron forwards application HTTP requests to the authenticated Web Host, dropping connection-level response headers (`transfer-encoding`, `connection`, `keep-alive`) that describe the Node fetch rather than the resource, and marking plugin bundle responses `no-store` because their per-launch revisions would only accumulate in Chromium's disk cache; WebSocket streams connect to that Host with credentials attached only for the owned application window. Node IPC carries boot injections, readiness, and shutdown. Desktop listens on an OS-assigned port by default, so it cannot collide with Web’s `3080` or a reserved port; a `webserver.config.port` patch can override it. Native Host requests send the current Host's authentication cookie explicitly, and no Electron session stores it; each Host start removes `dsh-auth-*` cookies that earlier releases left in the default session, because cookies ignore ports and would otherwise accompany every request to `127.0.0.1`.
|
||||
|
||||
Desktop binds `127.0.0.1`, the address Electron dials for the ready URL and the WebSocket credential filter. WebSocket streams use `ws:` or `wss:` according to the Host listener; credentials require matching authority and scheme.
|
||||
|
||||
@@ -302,7 +302,17 @@ On first launch, including after an upgrade, Desktop creates this document if it
|
||||
}
|
||||
```
|
||||
|
||||
Existing documents are never overwritten; an omitted `updates` object or field resolves to `false`. Set the field to `true` to permit test authentication dialogs, then fully quit and restart the application. Set it back to `false` and restart to suppress them again. Creation, read, or validation failures use the default `false` without blocking startup. Desktop attempts to warn through the Electron console with the file path; unavailable logging does not block startup. Correct the file and restart to apply an explicit setting. Unknown fields are preserved. Installation updates retain this user-data file; Windows uninstallation removes it under the [uninstall rules](#windows-uninstall-rules).
|
||||
Startup loading never overwrites existing documents; an omitted `updates` object or field resolves to `false`. Set the field to `true` to permit test authentication dialogs, then fully quit and restart the application. Set it back to `false` and restart to suppress them again. Creation, read, or validation failures use the default `false` without blocking startup. Desktop attempts to warn through the Electron console with the file path; unavailable logging does not block startup. Correct the file and restart to apply an explicit setting. Unknown fields are preserved. Installation updates retain this user-data file; Windows uninstallation removes it under the [uninstall rules](#windows-uninstall-rules).
|
||||
|
||||
In Desktop **Settings → General → Update channel**, **Stable** selects `stable` and **Development** selects `nightly`. The main process saves `updates.channel` in this same file and applies the choice immediately through IPC; no Host settings service is involved. An omitted channel uses the build-injected package default. External edits and deletion are read only on the next launch; no file watcher runs. The authentication-popup setting remains startup-only. UI writes retain unrelated fields from the latest valid document; external editors must not write concurrently with a UI save.
|
||||
|
||||
During a UI switch, the selector shows the pending choice with a loading indicator. Failure restores the actual runtime selection and shows a short toast; ordinary save failures keep the current channel and update target accessible. Invalid startup settings or failure to retire the previous update suspends update operations; without a mandatory policy block, the application remains usable. Startup settings errors show a non-blocking ten-second toast with a details action. The update indicator retains the suspension reason independently of download progress; open it for recovery guidance and a button to reveal the settings file. Fix the file, then choose **Reload settings** in the error details or restart; this explicit reload applies only update settings and never rewrites the file. The reload button shows a busy indicator for at least half a second and rejects repeated clicks. Invalid files keep updates paused. Selecting a valid channel in Settings can also recover an invalid channel; malformed JSON must be repaired first. A known mandatory-update block remains enforced; its configuration-error action opens the same file-location and reload dialog. Successful channel changes trigger a fresh mandatory-policy check, waiting for an older channel’s in-flight check if necessary. Successful switching retires pending checks, downloads and installation eligibility before checking the new feed, including when both feeds advertise the same version. Switching never downgrades or installs automatically. Installation preparation rejects UI switches; once `quitAndInstall()` hands off, file edits cannot cancel that installation.
|
||||
|
||||
The shared settings storage distinguishes missing files, read failures, malformed JSON, invalid document structure and invalid individual fields. Popup and channel fields are validated independently with Zod; an invalid popup field does not invalidate a valid channel, and vice versa. Reads never create files; startup initialization is explicit and shares the writer lock with saves. A save preserves unknown and unrelated invalid fields. Write diagnostics distinguish no write, uncertain completion and a committed write followed by lock-cleanup failure; the last case is not a rollback.
|
||||
|
||||
Once installation eligibility is revoked, obsolete-file removal failures do not block switching. They are flushed to `update-cleanup.jsonl` in Electron’s logs directory with the time, operation, path and filesystem error; inability to write that log falls back to stderr. Later download failures are reported independently and are not attributed to an earlier cleanup warning. Structured filesystem errors distinguish insufficient space, denied writes and busy files; the error dialog retains the current error code and supplied file paths in its details. Errors without structured evidence keep the generic download-failure guidance.
|
||||
|
||||
Updater adapter compatibility and platform version records are documented in [Electron updater compatibility](tests/updates/electron-updater/README.md).
|
||||
|
||||
<a id="mandatory-update-policy"></a>
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Electron 在 `userData/desktop/locale-cache` 缓存最近确认的界面语言
|
||||
|
||||
桌面埋点遵循[产品采集策略](../../packages/client/product-analytics/README.zh.md)及其动态应用配置,不包含 Web 使用情况。安装更新会等待该操作的本地埋点接收请求结束,再锁定 API 准入并停止 Host。接收请求的时限为一秒,失败不会阻止安装,也不等待收集端完成发送。
|
||||
|
||||
桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认监听系统分配的端口,因此不会与 Web 的 `3080` 或系统保留端口冲突;可通过 `webserver.config.port` patch 覆盖。
|
||||
桌面应用是完整 dsh Web 应用外的一层 Electron 壳。Electron RunAsNode 子进程启动共享 profile runner,Electron 立即从 `dsh-app://app/` 加载打包内的 Web 入口。共享加载页等待 Host 启动注入,然后在同一文档中启动客户端。Electron 将应用 HTTP 请求转发给已认证的 Web Host,转发时丢弃描述 Node fetch 连接而非资源本身的响应头(`transfer-encoding`、`connection`、`keep-alive`),并把插件 bundle 响应标记为 `no-store`,因为其每次启动都变化的 revision 只会在 Chromium 磁盘缓存中累积;WebSocket 流连接到该 Host,仅为归属的应用窗口附加凭据。Node IPC 承载启动注入、就绪与关闭。Desktop 默认监听系统分配的端口,因此不会与 Web 的 `3080` 或系统保留端口冲突;可通过 `webserver.config.port` patch 覆盖。原生 Host 请求会显式发送当前 Host 的认证 Cookie,任何 Electron session 都不保存该 Cookie;每次启动 Host 时都会移除早期版本遗留在默认 session 中的 `dsh-auth-*` Cookie,因为 Cookie 不区分端口,否则这些 Cookie 会随每个发往 `127.0.0.1` 的请求一并发送。
|
||||
|
||||
Desktop 绑定 `127.0.0.1`,即 Electron 为就绪 URL 与 WebSocket 凭据过滤器拨号的地址。WebSocket 流按 Host 监听器使用 `ws:` 或 `wss:`;附加凭据要求 authority 与 scheme 均匹配。
|
||||
|
||||
@@ -302,7 +302,17 @@ Electron 主进程在启动时读取一次 `app.getPath('userData')/desktop/sett
|
||||
}
|
||||
```
|
||||
|
||||
已有文件不会被覆盖;缺少 `updates` 对象或该字段时均按 `false` 处理。将字段设为 `true` 后完全退出并重启应用,即可允许测试鉴权弹窗。改回 `false` 并重启即可再次禁止。创建、读取或校验失败时使用默认值 `false`,不阻止启动。Desktop 尝试在 Electron 控制台记录包含文件路径的警告;日志不可用也不阻止启动。修正文件并重启后才会应用显式设置。未知字段会保留。安装更新保留此用户数据文件;Windows 卸载按[卸载规则](#windows-uninstall-rules)删除它。
|
||||
启动读取不会覆盖已有文件;缺少 `updates` 对象或该字段时均按 `false` 处理。将字段设为 `true` 后完全退出并重启应用,即可允许测试鉴权弹窗。改回 `false` 并重启即可再次禁止。创建、读取或校验失败时使用默认值 `false`,不阻止启动。Desktop 尝试在 Electron 控制台记录包含文件路径的警告;日志不可用也不阻止启动。修正文件并重启后才会应用显式设置。未知字段会保留。安装更新保留此用户数据文件;Windows 卸载按[卸载规则](#windows-uninstall-rules)删除它。
|
||||
|
||||
在桌面版**设置 → 通用设置 → 更新渠道**中,**正式版**选择 `stable`,**开发版**选择 `nightly`。主进程在同一文件中保存 `updates.channel`,并通过 IPC 立即应用选择,不依赖 Host 设置服务。省略 channel 时使用打包注入的包内默认值。外部编辑和删除仅在下次启动读取,不运行文件监听器。鉴权弹窗设置仍须重启生效。UI 写入保留最新有效文件中的其他字段;外部编辑器不能与 UI 保存同时写入。
|
||||
|
||||
UI 切换期间,选择器显示待应用选项和加载指示;失败时恢复实际运行渠道,并显示简短 toast。普通保存失败不会阻止使用当前渠道及旧更新目标。启动配置无效或无法撤销旧更新安装资格会暂停更新操作;没有强更策略阻塞时,主应用仍可使用。启动配置错误会显示持续十秒、可查看详情的非阻塞提示。更新入口独立于下载进度保留暂停原因;打开后可查看恢复指引、配置路径并在系统文件管理器中定位文件。修正文件后,可在错误详情中选择**重新读取配置**或重启;显式重读只应用更新配置,不改写文件。重读按钮至少显示半秒加载指示,期间不接受重复点击;文件仍无效时继续暂停更新。在设置中选择有效渠道也可恢复无效渠道;JSON 损坏须先修复文件。已知强更阻塞继续生效;强更页面的配置异常入口可打开同一文件定位和重读对话框。渠道切换成功后立即重新检查强更策略;如旧渠道的检查仍在进行,则等待其完成后检查新渠道。切换成功时先收尾检查、下载并撤销安装资格,再检查新渠道;两个渠道版本号相同也遵循该规则。切换不降级,也不自动安装。安装准备期间拒绝 UI 切换;调用 `quitAndInstall()` 交接后,编辑文件不能取消本次安装。
|
||||
|
||||
共享设置存储区分文件缺失、读取失败、JSON 损坏、文档结构无效和单项字段无效。鉴权开关与渠道字段分别通过 Zod 校验;鉴权开关无效不会使有效渠道失效,反之亦然。读取不创建文件;启动初始化是显式操作,并与保存共用写锁。保存保留未知字段和无关的无效字段。写入诊断区分未写入、完成情况不确定,以及写入已提交但释放锁失败;最后一种情况不表示文件已回滚。
|
||||
|
||||
撤销安装资格后,旧文件删除失败不阻止渠道切换。失败时间、操作、路径及文件系统错误会刷新写入 Electron 日志目录的 `update-cleanup.jsonl`;日志本身无法写入时回退到标准错误输出。后续下载失败独立报告,不归因于之前的清理警告。有结构化文件系统错误时,区分空间不足、无法写入和文件占用;错误弹窗详情保留当次错误码及已提供的文件路径。没有结构化依据时保留通用下载失败提示。
|
||||
|
||||
更新适配层兼容性和平台版本记录见 [Electron updater 兼容性](tests/updates/electron-updater/README.zh.md)。
|
||||
|
||||
<a id="mandatory-update-policy"></a>
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"bundle": "tsdown --config-loader native",
|
||||
"dev": "tsx scripts/dev.ts",
|
||||
"start": "tsx scripts/dev.ts --skip-build",
|
||||
"test:updates:channels": "pnpm run build && node scripts/test-update-channels.mjs",
|
||||
"test:updates:local": "pnpm run build && node scripts/test-local-updater.mjs",
|
||||
"prepare:runtime": "tsx scripts/prepare-runtime.ts",
|
||||
"render:tray-icon": "tsx scripts/render-tray-icon.ts",
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 0b35c7471c09b27a
|
||||
zh: cc4a382b99fc3210
|
||||
/desktop-packaging/release-versions:
|
||||
en: 3f38ab26ee195b39
|
||||
zh: f00eddafa8f3b9ca
|
||||
en: 8cc17b1e5ae3e422
|
||||
zh: 7de3e4f1e76be3e3
|
||||
/desktop-packaging/configuration-and-local-builds:
|
||||
en: 5b4f0f80a117f202
|
||||
zh: ff05be196c181087
|
||||
@@ -27,17 +27,17 @@
|
||||
en: 253e17998951931c
|
||||
zh: b582b377391d2ec2
|
||||
/desktop-packaging/macos-self-hosted-packaging:
|
||||
en: f4cc5209bdaeefba
|
||||
zh: 1db959d1ed345fcc
|
||||
en: b7e03e2e9807a62c
|
||||
zh: e5b0c43910e085d8
|
||||
/desktop-packaging/unsigned-windows-test-installer:
|
||||
en: f429bc0c280e2dfa
|
||||
zh: 4adbcc9e906bdf80
|
||||
/desktop-packaging/local-macos-dmg-without-release-signing:
|
||||
en: 2feaa0412247d81f
|
||||
zh: 461dde8e13fd4734
|
||||
en: 1146bbc11be43960
|
||||
zh: 7c13543a0513c059
|
||||
/desktop-packaging/macos-release-signing-and-diagnostics:
|
||||
en: 29fa6bd8919f3ec0
|
||||
zh: 9e31e037fa3d9378
|
||||
en: a0b4aa1080ecf6fb
|
||||
zh: 37d4b8934f89bee8
|
||||
/desktop-packaging/directory-builds-and-preparation:
|
||||
en: 5860c306375ddd63
|
||||
zh: a76e3cdbbe25c79e
|
||||
@@ -45,11 +45,11 @@
|
||||
en: b42f103358d0dfd7
|
||||
zh: a534164ad9eb00da
|
||||
/desktop-packaging/runtime-file-selection-and-installation-checks:
|
||||
en: 83e1075417039410
|
||||
zh: 3088a7ae0a2cc3c5
|
||||
en: 44044a5c14b75533
|
||||
zh: 39d91a369c4ec2fa
|
||||
/desktop-packaging/desktop-runtime-preparation:
|
||||
en: ab73b67d899e611d
|
||||
zh: d77b0525c6b81e91
|
||||
en: 84be38ab3136fff7
|
||||
zh: bb4ed12ee0e1dfa0
|
||||
/desktop-packaging/installation-and-release-verification:
|
||||
en: eb35eb973b796643
|
||||
zh: c1c52311632b7296
|
||||
|
||||
@@ -63,7 +63,7 @@ pnpm --dir apps/desktop run package:win:x64 --build-version 0.1.6-alpha.1.202609
|
||||
|
||||
A production release publishes the product version and takes no `--build-version`. Uploading one tags the packaged commit as `desktop-v<version>` once the artifacts are public; a build from a modified checkout is not tagged, and a tagging failure prints the command to run by hand rather than failing a completed upload. Test and local builds are deliberately left untagged, and every artifact records `dshBuildCommit` and `dshBuildDirty` in its manifest so a build handed over directly remains traceable.
|
||||
|
||||
The installer embeds `DSH_DESKTOP_UPDATE_CHANNEL` (`nightly` by default, or `latest`) independently of version and deployment. Automatic updates remain on that channel even for a stable version; manually installing the other channel switches it. Latest packages reject prerelease versions. A stable product can produce a nightly `-test.<date>.<sequence>` build, but that version ranks below the stable base and cannot upgrade an already installed higher version. Keep automatic downgrade disabled; stable corrective releases require a product version bump. CI version selection and publication are documented in [CI publication](../publishing/ci/README.md#package-and-publish).
|
||||
The installer embeds `DSH_DESKTOP_UPDATE_CHANNEL` (`nightly` by default, or `stable`) independently of version and deployment. This value supplies the default channel; the saved selection in [Desktop settings](../README.md#local-desktop-settings) takes precedence, including after installing another package. Stable-channel packages reject prerelease versions. A stable product can produce a nightly `-test.<date>.<sequence>` build, but that version ranks below the stable base and cannot upgrade an already installed higher version. Keep automatic downgrade disabled; stable corrective releases require a product version bump. CI version selection and publication are documented in [CI publication](../publishing/ci/README.md#package-and-publish).
|
||||
|
||||
<a id="configuration-and-local-builds"></a>
|
||||
|
||||
@@ -125,7 +125,7 @@ Installer uploads store the already-compressed release files without ZIP recompr
|
||||
|
||||
## macOS self-hosted packaging
|
||||
|
||||
The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG.
|
||||
The [Apple Silicon workflow](../../../.github/workflows/macos-package.yml) and [Intel workflow](../../../.github/workflows/macos-intel-package.yml) produce separate arm64 and x64 packages on the Apple Silicon runner labeled `dsh-mac-package-trial`. Both share one concurrency group. The Intel workflow checks Rosetta execution before dependency installation; its selected source must include the target-aware macOS CI adapter. Both select `test` or `production` and use the existing signing, notarization and packaged-runtime checks. When the selected source's dsh ships the Computer Use bundle, each workflow builds, notarizes and stages the helper for both architectures with `scripts/cua-helper/package.ts` and `stage.ts` after the configuration check, the steps npm publication uses; `skip_notarization` does not skip the helper's notarization. A source that predates helper packaging only emits a warning, and Computer Use cannot prepare its helper in that package. Successful DMG/ZIP files and diagnostic logs are retained for 14 days without publishing a release. Installer uploads disable redundant ZIP compression. The operator must verify a complete Actions run and installation on the target architecture before release qualification. The `compression_level` dropdown defaults to `3` and also offers `7`; it overrides the machine preset for this run and affects only ZIP and automatic-update downloads, not DMG.
|
||||
|
||||
All three platform forms put source, environment, update channel, version mode, manual version, test release ID and compression level first, with matching defaults. macOS notarization and Windows signing and cache measurement options follow these common fields.
|
||||
|
||||
@@ -172,19 +172,21 @@ pnpm run package:desktop:mac:x64:unsigned
|
||||
|
||||
Add `--check` to validate local configuration and tools without building, or `--dir` to stop at the unpacked application. The DMG is `apps/desktop/.desktop-build/targets/<target>/unsigned-artifacts/deepseek-harness-<version>-mac-<arch>-unsigned.dmg`. After all checks pass, the command prints `DMG:` followed by its absolute path. Open it and copy the application to a local test directory.
|
||||
|
||||
These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application.
|
||||
These commands ignore `.env.macos` and inherited release settings, use `com.deepseek.harness` as the app ID, and omit automatic-update and mandatory-update configuration. Build tools, standard network proxies, and `DSH_DESKTOP_NPM_REGISTRY` remain available from the shell. They skip Developer ID signing, Apple notarization, temporary signing keychains, and notarization proxy changes. Electron receives a local ad-hoc signature without certificates so the modified executable can run; the DMG is unsigned. Both preparation and the assembled application still run runtime integrity and smoke checks. Local builds omit update feeds, ZIPs, and release completion records, and the upload commands cannot publish them. macOS may require approval in System Settings → Privacy & Security to open an unnotarized application. These builds stage no Computer Use helper, so Computer Use reports the missing helper when it prepares.
|
||||
|
||||
<a id="macos-release-signing"></a>
|
||||
|
||||
## macOS release signing and diagnostics
|
||||
|
||||
The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. Ephemeral CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job; self-hosted machine presets retain private files outside the checkout. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>`.
|
||||
The signed macOS configuration uses the required release environment instead of accepting whichever certificate appears first in a keychain. It rejects empty values, a malformed Team ID, a signing identity that includes electron-builder's unsupported `Developer ID Application:` prefix, and incomplete notarization credentials. Signed macOS packaging requires the configured identity and its private key. Runtime preparation applies that identity, a secure timestamp, and hardened runtime to every embedded Mach-O file outside the notarized Computer Use helper bundles; for those bundles it verifies the deep signature, release identity, stapled ticket, and Gatekeeper acceptance without re-signing their files. A signed local build of a source that ships Computer Use therefore needs the notarized helper staged first with `scripts/cua-helper/package.ts` and `stage.ts`; an ad-hoc `build:native` output under `native/bin` fails this verification; after signing the application, a deep strict check rejects any other leaf authority or Team ID before artifact creation. The signed fixed-target macOS installer commands create separate copies of the signed application and run two artifact lanes concurrently. One lane notarizes and staples the App before generating the ZIP and its update metadata. The other encloses its signed App copy in a signed DMG, then notarizes, staples, and verifies the DMG; its inner App has no individually stapled ticket. Both lanes must finish successfully before their artifacts reach the final directory and the release completion record is written. Signed directory-only commands also require notarization credentials and wait for Apple notarization and App stapling. The [parallel notarization decision](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.md) owns copy isolation and container ticket semantics. `CSC_LINK` must name a local p12 containing the Developer ID Application certificate and private key; URLs and Base64 inputs are not supported. `CSC_KEY_PASSWORD` is its export password, not an Apple account or login password; an explicitly empty value supports an unencrypted p12. Before building, packaging creates and unlocks a private temporary keychain, imports the p12, authorizes signing, and signs a small probe. Runtime and App signing use this keychain explicitly; existing login keychains require no setup or manual unlocking. Child processes receive its path without the p12 password. The keychain is deleted after success or ordinary failure; CI must clean temporary credentials after forced termination. Ephemeral CI writes the certificate and `.env.macos` from its secret store, restricts file access, and deletes both after the job; self-hosted machine presets retain private files outside the checkout. Ambient `CSC_NAME` and certificate discovery order do not select the release owner. Notary credentials may instead use electron-builder's complete Apple ID or keychain-profile strategy. The two macOS identity variables are also required when repeating the application check manually with `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>`.
|
||||
|
||||
macOS signing visits real files without following Framework symlink aliases. PAK resources retain all shipped languages and are sealed by the enclosing Framework or application signature instead of receiving individual signatures. The [release policy](../../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.md) owns the dependency patch and verification requirements.
|
||||
|
||||
macOS runtime preparation caches verified single-architecture Mach-O signatures in `.desktop-build/targets/<target>/signature-cache` unless `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` selects external storage. An explicit path must be absolute and already exist, contain no linked ancestors, and be owned by the current account with mode `0700` and read/write/search access; invalid or empty settings fail preflight without fallback. Omit the field to retain target-local storage. Keys include input bytes and permissions, the signing probe’s actual leaf certificate, identifier, entitlement bytes, macOS version, and signing tools and policy. Git commits do not determine reuse: uncommitted byte changes invalidate the affected file. Every hit verifies cached bytes, the strict signature, certificate, identifier, entitlements, secure timestamp and hardened runtime before replacing an unchanged input. Universal binaries are signed each time. Runtime integrity and smoke checks, App signing, and Apple notarization still run. The cache requires trusted local build storage. Cache corruption or unsafe links fail the build; stop packaging before deleting the affected cache directory and retrying. Successful signing stages prune complete entries to a one-GiB payload budget; interrupted temporary entries require manual removal. Concurrent eviction can fail a reader safely. Logs report hit, miss and uncached counts.
|
||||
|
||||
Mac package commands print `DESKTOP_PACKAGING_RECORD` with a unique directory under `apps/desktop/.desktop-build/packaging-runs/`. After reading local configuration, each run retains `run.json` (published version, product version, Git commit, dirty-worktree flag, target and Node version), `events.jsonl` (timestamped phases, durations, parallel output attribution and proxy restoration), redacted child `stdout.log` / `stderr.log`, and `result.json` (overall outcome, stage results and artifact directory). Events also record pack concurrency and whether each proxy is configured. Logs survive failures and later builds; no automatic deletion runs. Missing `result.json` means completion is unconfirmed. Known credential values are redacted; environments and notarytool authentication arguments are not recorded. Runtime preparation records package staging, installation, materialization, signing, manifests, smoke checks, descriptor verification and cleanup separately. Nested phase durations overlap and must not be added together.
|
||||
Mac package commands print `DESKTOP_PACKAGING_RECORD` with a unique directory under `apps/desktop/.desktop-build/packaging-runs/`. After reading local configuration, each run retains `run.json` (published version, product version, Git commit, dirty-worktree flag, target and Node version), `events.jsonl` (timestamped phases, durations, parallel output attribution and proxy restoration), redacted child `stdout.log` / `stderr.log`, and `result.json` (overall outcome, stage results and artifact directory). Events also record pack concurrency and whether each proxy is configured. Logs survive failures and later builds; no automatic deletion runs. Missing `result.json` means completion is unconfirmed. Known credential values are redacted, and environments are not recorded. Apple command failures retain their exit code, signal, stdout and stderr in redacted form; the adapter also writes these details to stderr when no journal is available. Runtime preparation records package staging, installation, materialization, signing, manifests, smoke checks, descriptor verification and cleanup separately. Nested phase durations overlap and must not be added together.
|
||||
|
||||
CUA helper packaging prints `CUA_PACKAGING_RECORD` and creates its own record in the same directory before desktop installer startup. Its `run.json` identifies the helper target, product version and source commit; `events.jsonl` retains command and notarization phases, submission IDs and Apple results. `result.json` records success or failure. These records survive temporary payload cleanup, and the Desktop Actions diagnostic upload includes them even when helper preparation fails.
|
||||
|
||||
App and DMG notarization each record a submission ID and separate `notarytool:upload:*` and `notarytool:wait:*` durations. Upload uses `submit --no-wait` and includes authentication, local validation, transfer and server acceptance; it is not pure transfer time. Waiting starts after that command returns and includes polling and remaining Apple processing; Apple can start processing before upload submission returns. Apple status and diagnostic logs are retained, including rejection; signature checks, acceptance checks and stapling remain owned by `@electron/notarize`. Directory-only package commands use this same measured App notarization path. Individual subprocess output remains live in the terminal, while phase failures and nested errors are retained in the journal. Both platforms record parent-process packaging failures and print redacted diagnostics, including proxy recovery instructions, to the terminal. The configuration-only `check:package` command creates no run log.
|
||||
|
||||
@@ -246,7 +248,7 @@ Desktop packs workspace packages locally and installs external dependencies thro
|
||||
|
||||
The [Electron shell file list](../scripts/electron-builder-config.mjs) excludes recognized JavaScript/CSS/TypeScript source maps and TypeScript build caches from the separate `resources/app.asar/node_modules` dependency collection. These exclusions preserve runtime code, native modules, WASM, data files (including ordinary `.map` assets), licenses, and notices; [shell archive tests](../tests/shell-file-selection.spec.ts) check retained bytes and exclusions.
|
||||
|
||||
The [Office conversion provider](../../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in).
|
||||
The [Office conversion provider](../../../packages/document/office-to-pdf/README.md) carries the target’s declared native engine, or WASM when the kit declares no matching native target. Preparation rejects a missing target engine before packaging. The complete Office dependencies, including CLI, JavaScript libraries, and the selected engine’s executables, data, licenses, and notices, are unpacked under `resources/app.asar.unpacked/dsh/node_modules/`. The Desktop Host resolves engine manifests to these physical directories and supplies loaded skills with absolute standalone Node and unpacked CLI paths. Node lives in `resources/runtime/primary-runtime/dependencies/node/bin/`; the CLI is the unpacked `@deepseek-ai/libreoffice-kit/lib/cli.js`. On macOS, the native helper receives the JIT entitlement required by [LibreOffice’s UNO bridge](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in). The Computer Use helper applications under `@deepseek-ai/dsh-experimental-cua-macos/native/bin` are also unpacked whole under `resources/app.asar.unpacked/dsh/node_modules/`, and the provider opens that copy.
|
||||
|
||||
The packaged application runs compiled JavaScript and pre-generated Typert metadata; it does not compile TypeScript plugins. Source-level debugger navigation and editor declarations remain available in development packages. [Copy-policy tests](../tests/runtime-file-policy.spec.ts) cover exclusions and retained assets; The [payload smoke](../tests/fixtures/runtime-payload-smoke.mjs) runs under Electron RunAsNode before the Host smoke and final inventory verification. The payload smoke resolves the search tool’s ripgrep executable and verifies text search and file enumeration. Signed Windows builds run these checks after dependency signing; other builds run them during `prepare:dsh`. The [Host smoke](../scripts/smoke-runtime.ts) creates DOCX, XLSX, and PPTX inputs with bundled Python, converts each through the real Office provider, and checks PDF output. Every assembled application, including directory and unsigned builds, repeats the payload and Host checks against ASAR. Archive integrity checks compare the complete archived descriptor with preparation, file bytes and membership in both archive and unpacked trees, recorded executable flags for archived files, and physical permissions for unpacked files. The Host smoke also obtains CLI paths from the loaded skill and runs capabilities and DOCX conversion with an empty PATH. Conversion failures stop packaging before a release record is written; macOS DMG/ZIP builds run these checks before notarization.
|
||||
|
||||
@@ -280,7 +282,7 @@ Desktop [patch policy](../scripts/runtime-patch-policy.ts) explicitly classifies
|
||||
|
||||
Desktop dependency overrides are declared separately in the patch-policy module, not inferred from patch versions. The pi-ai constraint keeps its runtime version at the version qualified by the shared patch. Desktop still resolves a fresh runtime lockfile for each build, then installs with `--frozen-lockfile`; dependency resolution across separate builds is not yet reproducible. CLI packed-installation tests and other distribution flows retain their own configuration.
|
||||
|
||||
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
|
||||
Every package command builds the repository, packs the first-party production closures rooted at dsh and the private Desktop Host, and prepares the target Electron distribution plus primary-runtime with its shared pnpm CLI. `prepare:dsh` installs the production graph once at build time, prepares materialized packages for electron-builder to archive under `app.asar/dsh`, removes package-manager metadata, and writes `desktop-runtime.json` with shared package versions and final file hashes. Signed macOS builds sign and verify native files, and verify the notarized Computer Use helper bundles without re-signing them, before inventory generation; electron-builder excludes this already-signed tree from nested re-signing. Resource mappings explicitly include `dsh/node_modules`, which the default root-directory filter omits; the prepared runtime inventory is checked after native signing. Native executables and libraries are unpacked beside ASAR; Python, standalone Node and pnpm remain in external runtime resources. Windows packaging checks every prepared PE against its ASAR unpacked entry and byte-identical disk copy, including unsigned builds. Builder glob rules match braces in PE filenames as single-character wildcards, so a matching neighboring file may also be unpacked. Prepared runtime smoke uses the verified target descriptor rather than the build host architecture. Signed installer, notarization, installed upgrade, and target-specific native-module qualification require the release environment.
|
||||
|
||||
Windows runtime sealing reads at most eight files concurrently, before and after native signing. A read failure drains outstanding reads without replacing the descriptor; successful sealing still requires the subsequent complete integrity and smoke checks.
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ pnpm --dir apps/desktop run package:win:x64 --build-version 0.1.6-alpha.1.202609
|
||||
|
||||
production 发布使用产品版本本身,不传 `--build-version`。其上传成功后会把打包所用 commit 打成 `desktop-v<版本>` 标签;来自有改动工作区的构建不打标签,打标签失败也只打印手工命令,不会让已完成的上传变成失败。test 与本地构建有意不留标签,而所有产物的清单都记录 `dshBuildCommit` 与 `dshBuildDirty`,直接分发的构建同样可溯源。
|
||||
|
||||
安装包独立于版本和发布环境内置 `DSH_DESKTOP_UPDATE_CHANNEL`(默认 `nightly`,也可选择 `latest`)。自动更新始终保留该通道,即使安装的是稳定版本;手动安装另一通道的包才会切换。latest 包拒绝预发布版本。稳定产品可生成 nightly 的 `-test.<date>.<sequence>` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。
|
||||
安装包独立于版本和发布环境内置 `DSH_DESKTOP_UPDATE_CHANNEL`(默认 `nightly`,也可选择 `stable`)。该值提供默认渠道;[桌面设置](../README.zh.md#local-desktop-settings)中保存的选择优先,包括安装另一个包之后。stable 包拒绝预发布版本。稳定产品可生成 nightly 的 `-test.<date>.<sequence>` 构建,但该版本低于稳定基准,不能升级已安装的更高版本。保持自动降级关闭;稳定修复版需要提升产品版本。CI 版本选择与发布见 [CI 发布](../publishing/ci/README.zh.md#package-and-publish)。
|
||||
|
||||
<a id="configuration-and-local-builds"></a>
|
||||
|
||||
@@ -125,7 +125,7 @@ runner 的受监督启动和恢复见 [Windows runner 机器准备](../windows-s
|
||||
|
||||
## macOS 自托管打包
|
||||
|
||||
[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。
|
||||
[Apple Silicon 工作流](../../../.github/workflows/macos-package.yml)与 [Intel 工作流](../../../.github/workflows/macos-intel-package.yml)在带有 `dsh-mac-package-trial` 标签的 Apple Silicon runner 上分别生成 arm64 和 x64 安装包,两者共用一个并发组。Intel 工作流在安装依赖前检查 Rosetta 能否执行;所选源码必须包含按目标架构校验的 macOS CI 适配脚本。两者均选择 `test` 或 `production`,并复用现有签名、公证和打包运行时检查。所选源码的 dsh 随附 Computer Use 组合包时,两者都在配置检查之后用 `scripts/cua-helper/package.ts` 和 `stage.ts` 构建、公证并放入两种架构的 helper,与 npm 发布的步骤相同;`skip_notarization` 不会跳过 helper 的公证。源码早于 helper 打包支持时只输出警告,该安装包中的 Computer Use 无法准备 helper。成功的 DMG/ZIP 与诊断日志保留 14 天,不发布 release;安装包上传关闭重复 ZIP 压缩。操作人员须验收完整 Actions 运行及目标架构上的安装,才能确认可交付。`compression_level` 下拉选项默认为 `3`,也可选择 `7`;本次运行以该选项覆盖机器预设,只影响 ZIP 和自动升级下载,不影响 DMG。
|
||||
|
||||
三个平台的表单均依次展示源码、环境、升级通道、版本模式、手动版本号、测试 release ID 和压缩等级,默认值一致。macOS 公证选项和 Windows 签名、缓存测量选项位于公共字段之后。
|
||||
|
||||
@@ -172,19 +172,21 @@ pnpm run package:desktop:mac:x64:unsigned
|
||||
|
||||
添加 `--check` 仅验证本地配置和工具而不构建,或添加 `--dir` 只生成解包应用。DMG 路径为 `apps/desktop/.desktop-build/targets/<target>/unsigned-artifacts/deepseek-harness-<version>-mac-<arch>-unsigned.dmg`。所有检查通过后,命令打印 `DMG:` 及其绝对路径。打开后将应用复制到本地测试目录。
|
||||
|
||||
这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。
|
||||
这些命令忽略 `.env.macos` 和继承的发布配置,使用 `com.deepseek.harness` 作为应用 ID,并省略自动更新和强制更新配置。构建工具、标准网络代理与 `DSH_DESKTOP_NPM_REGISTRY` 仍可从 shell 提供。它们跳过 Developer ID 签名、Apple 公证、临时签名钥匙串和公证代理修改。Electron 使用无需证书的本地 ad-hoc 签名,以便修改后的可执行文件能够运行;DMG 不签名。准备阶段和组装后的应用仍执行运行时完整性与冒烟检查。本地构建不生成更新源、ZIP 或发布完成记录,上传命令无法发布这些产物。macOS 可能要求在“系统设置 → 隐私与安全性”中批准打开未经公证的应用。这类构建不放入 Computer Use helper,Computer Use 准备时会报告 helper 缺失。
|
||||
|
||||
<a id="macos-release-signing"></a>
|
||||
|
||||
## macOS 发布签名与诊断
|
||||
|
||||
macOS 签名配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 签名打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到每个内嵌 Mach-O 文件;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 签名固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。签名的仅目录命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。临时 CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者;自托管机器 preset 在 checkout 外保留私有文件。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
|
||||
macOS 签名配置使用必填发布环境,不会接受钥匙串中最先发现的证书。空值、格式错误的 Team ID、包含 electron-builder 不支持的 `Developer ID Application:` 前缀的签名身份,以及不完整的公证凭据都会被拒绝。macOS 签名打包要求已配置的身份及其私钥可用。运行时准备会把该身份、安全时间戳与 hardened runtime 应用到已公证 Computer Use helper 应用包之外的每个内嵌 Mach-O 文件;对这些应用包,则校验深度签名、发布身份、钉票和 Gatekeeper 评估结果,不重签其中的文件。因此,打包会随附 Computer Use 的源码时,本地签名构建需要先用 `scripts/cua-helper/package.ts` 和 `stage.ts` 放入已公证的 helper;`native/bin` 下由 `build:native` 生成的临时签名产物无法通过此校验;应用签名完成后,深度严格检查会拒绝其他叶证书 Authority 或 Team ID,验证通过才生成发布产物。macOS 签名固定目标安装包命令为已签名应用创建独立副本,并发执行两条产物流。一路先公证 App 并钉票,再生成 ZIP 及其更新元数据。另一路把已签名 App 副本封装进签名 DMG,再公证 DMG、钉票并验证;其中的 App 不单独附加票据。只有两路均成功结束,产物才会移入最终目录并写入发布完成记录。签名的仅目录命令同样需要公证凭据,并等待 Apple 公证和 App 钉票完成。[并行公证决策](../../../.agents/notes/implemented/process/2026-09-09-parallel-macos-notarization.zh.md)负责副本隔离与容器票据语义。`CSC_LINK` 必须指向包含 Developer ID Application 证书及私钥的本地 p12,不支持 URL 或 Base64 输入。`CSC_KEY_PASSWORD` 是其导出密码,不是 Apple 账号或登录密码;未加密的 p12 可显式填写空值。构建前,打包流程自动创建并解锁私有临时钥匙串、导入 p12、授权签名并签署小型探针。运行时与 App 签名显式使用该钥匙串,无需预先配置或手动解锁登录钥匙串。子进程只接收钥匙串路径,不接收 p12 密码。成功或普通失败后删除临时钥匙串;强制终止后由 CI 清理临时凭据。临时 CI 从密钥存储生成证书文件和 `.env.macos`,限制文件访问权限,并在作业结束后删除二者;自托管机器 preset 在 checkout 外保留私有文件。环境中的 `CSC_NAME` 与证书发现顺序都不能选择发布所有者。公证凭据也可以使用 electron-builder 支持的完整 Apple ID 或钥匙串 profile 方式。手动执行 `pnpm --dir apps/desktop run verify:mac-signature -- <path-to-app>` 重复应用检查时,也必须提供两个 macOS 身份变量。
|
||||
|
||||
macOS 签名遍历真实文件,不跟随 Framework 的软链接别名。PAK 资源保留全部随附语言,由外层 Framework 或应用签名记录完整性,不逐个签名。[发布策略](../../../.agents/notes/implemented/architecture/2026-08-25-electron-desktop-packaging-and-updates.zh.md)负责依赖补丁和验证要求。
|
||||
|
||||
macOS 运行时准备将已验证的单架构 Mach-O 签名缓存在 `.desktop-build/targets/<target>/signature-cache`,除非通过 `DSH_DESKTOP_MACOS_SIGNATURE_CACHE_DIR` 选择外置存储。显式路径必须是已存在的绝对目录,祖先路径不能包含链接,目录须归当前账户所有、权限为 `0700` 且可读写及遍历;非法或空配置在预检时失败,不回退。省略该字段保留目标本地缓存。缓存键包含输入字节与权限、签名探针实际使用的叶证书、签名标识、entitlement 字节、macOS 版本,以及签名工具与策略。复用不取决于 Git 提交:未提交的字节变更会使对应文件失效。每次命中都验证缓存字节、严格签名、证书、标识、entitlements、安全时间戳和 hardened runtime,再替换未被并发修改的输入。通用二进制每次重新签名。运行时完整性与 smoke 检查、App 签名和 Apple 公证仍会执行。缓存要求受信任的本地构建存储。缓存损坏或不安全链接会令构建失败;停止打包后删除对应缓存目录再重试。成功的签名阶段会将完整缓存条目的内容总量裁剪至一 GiB;中断留下的临时条目需手动清理。并发淘汰可能使读取方安全失败。日志记录命中、未命中及未缓存数量。
|
||||
|
||||
Mac 打包命令通过 `DESKTOP_PACKAGING_RECORD` 输出 `apps/desktop/.desktop-build/packaging-runs/` 下的唯一目录。读取本地配置后,每次运行保留 `run.json`(发布版本、产品版本、Git 提交、工作区是否有改动、目标及 Node 版本)、`events.jsonl`(带时间戳的阶段、耗时、并行输出归属及代理恢复状态)、脱敏后的子进程 `stdout.log` / `stderr.log`,以及 `result.json`(整体结果、阶段结果和产物目录)。事件还记录打包并发数及各代理是否配置。失败和后续打包不会清除日志;没有自动删除流程。缺少 `result.json` 表示完成状态未经确认。已知凭据值会被遮盖;不记录环境变量全集或 notarytool 认证参数。运行时准备分别记录包暂存、安装、依赖树复制、签名、清单生成、smoke 检查、描述文件校验和清理。嵌套阶段的耗时存在重叠,不能直接相加。
|
||||
Mac 打包命令通过 `DESKTOP_PACKAGING_RECORD` 输出 `apps/desktop/.desktop-build/packaging-runs/` 下的唯一目录。读取本地配置后,每次运行保留 `run.json`(发布版本、产品版本、Git 提交、工作区是否有改动、目标及 Node 版本)、`events.jsonl`(带时间戳的阶段、耗时、并行输出归属及代理恢复状态)、脱敏后的子进程 `stdout.log` / `stderr.log`,以及 `result.json`(整体结果、阶段结果和产物目录)。事件还记录打包并发数及各代理是否配置。失败和后续打包不会清除日志;没有自动删除流程。缺少 `result.json` 表示完成状态未经确认。已知凭据值会被遮盖,不记录环境变量全集。Apple 命令失败时保留脱敏后的退出码、信号、stdout 和 stderr;没有日志目录时,适配器也会将这些详情写入 stderr。运行时准备分别记录包暂存、安装、依赖树复制、签名、清单生成、smoke 检查、描述文件校验和清理。嵌套阶段的耗时存在重叠,不能直接相加。
|
||||
|
||||
CUA helper 打包通过 `CUA_PACKAGING_RECORD` 输出同一目录下的独立记录路径,在桌面安装包开始构建前创建。其 `run.json` 记录 helper 目标、产品版本和源码提交,`events.jsonl` 保留命令与公证阶段、已返回的提交 ID 和 Apple 处理结果,`result.json` 记录成功或失败。这些记录不会随临时 payload 清理而删除;即使 helper 准备失败,Desktop Actions 的诊断上传步骤也会收集它们。
|
||||
|
||||
App 和 DMG 公证分别记录 submission ID,以及独立的 `notarytool:upload:*` 和 `notarytool:wait:*` 耗时。上传使用 `submit --no-wait`,包含认证、本地校验、传输和服务端受理,并非纯传输时间。等待从该命令返回后开始,包含轮询及剩余的 Apple 处理时间;Apple 可能在上传提交命令返回前已开始处理。日志保留 Apple 状态与诊断信息,包括拒绝结果;签名检查、接受状态校验和 stapling 仍由 `@electron/notarize` 负责。仅生成目录的打包命令也使用同一条可计时的 App 公证路径。各子进程输出仍实时显示在终端,阶段失败及嵌套错误保留在事件日志中。两个平台均记录父进程打包失败,并在终端显示脱敏诊断,包括代理恢复操作指引。仅检查配置的 `check:package` 命令不创建运行日志。
|
||||
|
||||
@@ -246,7 +248,7 @@ Desktop 在本地打包工作区包,并通过目标捆绑的 Node 和 pnpm 安
|
||||
|
||||
[Electron 外壳文件清单](../scripts/electron-builder-config.mjs)从单独收集的 `resources/app.asar/node_modules` 依赖中排除已识别的 JavaScript/CSS/TypeScript source map 和 TypeScript 构建缓存。这些排除规则保留运行时代码、原生模块、WASM、数据文件(包括普通 `.map` 资源)、许可证和 notices;[外壳归档测试](../tests/shell-file-selection.spec.ts)检查保留文件的字节和排除项。
|
||||
|
||||
[Office 转换提供方](../../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。
|
||||
[Office 转换提供方](../../../packages/document/office-to-pdf/README.zh.md)携带目标已声明的原生引擎;kit 未声明匹配原生目标时携带 WASM 引擎。准备阶段在打包前拒绝缺少目标引擎的情况。完整 Office 依赖(CLI、JavaScript 库和选定引擎的可执行文件、数据、许可证及 notices)解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下。Desktop Host 将引擎清单解析到这些物理目录,并向加载的技能提供独立 Node 和解包后 CLI 的绝对路径。Node 位于 `resources/runtime/primary-runtime/dependencies/node/bin/`;CLI 位于解包后的 `@deepseek-ai/libreoffice-kit/lib/cli.js`。macOS 上的原生辅助程序获得 [LibreOffice UNO 桥](https://github.com/LibreOffice/core/blob/master/sysui/desktop/macosx/hardened_runtime.xcent.in)所需的 JIT entitlement。`@deepseek-ai/dsh-experimental-cua-macos/native/bin` 下的 Computer Use helper 应用也整体解包到 `resources/app.asar.unpacked/dsh/node_modules/` 下,提供方打开该副本。
|
||||
|
||||
打包应用运行编译后的 JavaScript 和预生成的 Typert 元数据,不编译 TypeScript 插件。源码级调试导航和编辑器声明仍可从开发包中获取。[复制规则测试](../tests/runtime-file-policy.spec.ts)覆盖排除项和保留资源;[产物 smoke](../tests/fixtures/runtime-payload-smoke.mjs) 在 Host smoke 和最终清单验证之前,使用 Electron RunAsNode 执行。产物 smoke 解析搜索工具使用的 ripgrep 可执行文件,并验证文本搜索和文件枚举。Windows 签名构建在依赖签名后运行这些检查;其他构建在 `prepare:dsh` 中运行。[Host smoke](../scripts/smoke-runtime.ts) 使用捆绑的 Python 创建 DOCX、XLSX 和 PPTX 输入,通过真实 Office 提供方逐一转换并检查 PDF 输出。每个组装后的应用(包括目录包和未签名构建)都会针对 ASAR 重复产物和 Host 检查。归档完整性检查将归档内完整描述符与准备结果比对,并核对归档和解包目录中的文件内容与清单、归档内文件记录的执行标志,以及解包文件的物理权限。转换失败会在写入发布记录前终止打包;macOS DMG/ZIP 构建在公证前执行这些检查。
|
||||
|
||||
@@ -280,7 +282,7 @@ Desktop [补丁策略](../scripts/runtime-patch-policy.ts) 将每份工作区补
|
||||
|
||||
Desktop 依赖 overrides 在补丁策略模块中单独声明,不从补丁版本推导。pi-ai 约束使运行时版本保持在共享补丁已验证的版本。Desktop 仍会在每次构建时重新解析运行时锁文件,再通过 `--frozen-lockfile` 安装;不同构建之间的依赖解析尚不保证可复现。CLI 打包安装测试及其他交付流程保留各自的配置。
|
||||
|
||||
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
|
||||
每条打包命令都会构建仓库,打包以 dsh 和私有 Desktop Host 为根的第一方生产依赖闭包,并准备目标专用的 Electron 分发包和包含共享 pnpm CLI 的 primary-runtime。`prepare:dsh` 在构建时安装一次生产依赖图,准备物化包供 electron-builder 归档到 `app.asar/dsh`,移除包管理器元数据,并生成包含共享包版本和最终文件哈希的 `desktop-runtime.json`。macOS 签名构建先签名并验证原生文件,并在不重签的前提下校验已公证的 Computer Use helper 应用包,再生成清单;electron-builder 不对已签名的此目录重复进行嵌套签名。资源映射明确包含默认根目录过滤器会忽略的 `dsh/node_modules`;准备完成的运行时清单在原生签名后检查。原生可执行文件及库解包到 ASAR 旁;Python、独立 Node 和 pnpm 保留在外部 runtime 资源中。Windows 打包逐项检查准备好的 PE,确认其 ASAR 条目已标记为解包,且磁盘副本字节一致;未签名构建也执行此检查。Builder glob 规则用单字符通配符匹配 PE 文件名中的花括号,因此同目录中名称匹配的文件也可能被解包。准备好的运行时 smoke 沿用已验证的目标描述符,不使用构建宿主的架构。签名安装包、公证、已安装应用升级和各目标原生模块的验收需要发布环境。
|
||||
|
||||
Windows 运行时在原生签名前后封存文件清单,每次最多并发读取八个文件。读取失败时会等待未完成的读取结束,不替换描述符;封存成功后仍须完成后续的完整性与 smoke 检查。
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
en: eed7c4ed0d4e2758
|
||||
zh: 026fe6076e11018e
|
||||
/desktop-publication/publish-installers-and-update-feeds:
|
||||
en: 3c3e3a09573c4386
|
||||
zh: c851ae6a94f2284a
|
||||
en: 4723059d6f16b25b
|
||||
zh: 54e5afad8fff8eb6
|
||||
/desktop-publication/fixed-installer-downloads:
|
||||
en: ce8296f8f1ee881e
|
||||
zh: 749ab63bd6ed4248
|
||||
|
||||
@@ -59,9 +59,9 @@ pnpm run package:desktop:mac:arm64
|
||||
pnpm run upload:mac:arm64
|
||||
```
|
||||
|
||||
The target dotenv file for internal-test packaging explicitly sets `DSH_DESKTOP_AUTO_UPDATE_ENV=test` and `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`; uploads use `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` and separate test credentials. Both test and production use the fixed Nightly channel; deployment selection does not enable channel switching.
|
||||
The target dotenv file for internal-test packaging explicitly sets `DSH_DESKTOP_AUTO_UPDATE_ENV=test` and `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`; uploads use `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` and separate test credentials. Deployment and update channel are independent; see [package channel selection](../packaging/README.md#release-versions).
|
||||
|
||||
Early internal-test packages use the `test` deployment. Select `production` explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Every release publishes `nightly.yml` or `nightly-mac.yml`; stable releases also publish `latest.yml` or `latest-mac.yml` pointing at the same artifacts. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset, including the empty header the COS SDK would otherwise add: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification.
|
||||
Early internal-test packages use the `test` deployment. Select `production` explicitly only for a production release; changing upload credentials does not retarget an existing package. Packaging requires no COS credentials, disables electron-builder publishing, strips COS credentials from child processes, and records completion only after signing and notarization succeed. Upload validates this record, deployment, target, shared version, filenames, sizes, and SHA-512 before reading credentials. Packages and blockmaps upload before YAML; historical objects are retained. Each package publishes only its selected channel: `nightly.yml` / `nightly-mac.yml` or `stable.yml` / `stable-mac.yml`. Published YAML uses absolute binary URLs. The uploader leaves Cache-Control unset, including the empty header the COS SDK would otherwise add: deployment infrastructure owns cache policy, with uncached feeds and separately configurable binary caching. Serialize publication per target and verify public artifacts and feed contents before release qualification.
|
||||
|
||||
Windows NSIS uploads require the generated, nonempty `.exe.blockmap` beside the installer. The blockmap is uploaded before channel YAML; NSIS installer metadata does not require the embedded `blockMapSize` used by the separate web-installer format. File-plan tests use the pinned builder's blockmap generator, not a hand-authored embedded-map field.
|
||||
|
||||
|
||||
@@ -59,9 +59,9 @@ pnpm run package:desktop:mac:arm64
|
||||
pnpm run upload:mac:arm64
|
||||
```
|
||||
|
||||
内测打包在目标 `.env` 中显式设置 `DSH_DESKTOP_AUTO_UPDATE_ENV=test` 和 `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`;上传使用 `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` 及独立测试凭据。test 和 production 都使用固定 Nightly 通道,部署选择不提供通道切换。
|
||||
内测打包在目标 `.env` 中显式设置 `DSH_DESKTOP_AUTO_UPDATE_ENV=test` 和 `DOWNLOAD_TEST_ORIGIN=https://download-test.deepseek.com`;上传使用 `DOWNLOAD_TEST_COS_BUCKET=bj-toc-download-test-1320056602` 及独立测试凭据。部署与更新渠道相互独立;参见[安装包渠道选择](../packaging/README.zh.md#release-versions)。
|
||||
|
||||
前期内测包使用 `test` 部署。只有正式发布才显式选择 `production`;更换上传凭据不会改变已有安装包的更新目标。打包不需要 COS 凭据,会禁用 electron-builder 发布、移除子进程的 COS 凭据,并且仅在签名与公证成功后记录完成状态。上传在读取凭据前验证该记录、部署、目标、共同版本号、文件名、大小与 SHA-512。安装包和 blockmap 先于 YAML 上传;历史对象继续保留。每个版本发布 `nightly.yml` 或 `nightly-mac.yml`;稳定版本还发布指向相同产物的 `latest.yml` 或 `latest-mac.yml`。发布的 YAML 使用安装包绝对 URL。上传器不设置 Cache-Control,包括 COS SDK 否则会添加的空头部:缓存策略由部署基础设施负责,清单不缓存,安装包缓存单独配置。同一目标应串行发布,并在发布验收前验证公网产物与清单内容。
|
||||
前期内测包使用 `test` 部署。只有正式发布才显式选择 `production`;更换上传凭据不会改变已有安装包的更新目标。打包不需要 COS 凭据,会禁用 electron-builder 发布、移除子进程的 COS 凭据,并且仅在签名与公证成功后记录完成状态。上传在读取凭据前验证该记录、部署、目标、共同版本号、文件名、大小与 SHA-512。安装包和 blockmap 先于 YAML 上传;历史对象继续保留。每个包仅发布自身选定渠道的清单:`nightly.yml` / `nightly-mac.yml` 或 `stable.yml` / `stable-mac.yml`。发布的 YAML 使用安装包绝对 URL。上传器不设置 Cache-Control,包括 COS SDK 否则会添加的空头部:缓存策略由部署基础设施负责,清单不缓存,安装包缓存单独配置。同一目标应串行发布,并在发布验收前验证公网产物与清单内容。
|
||||
|
||||
Windows NSIS 上传要求安装包旁存在生成的非空 `.exe.blockmap`。blockmap 先于通道 YAML 上传;NSIS 安装包元数据不要求另一种 web-installer 格式使用的内嵌 `blockMapSize`。文件清单测试使用固定版本构建器的 blockmap 生成器,而不是手工编造内嵌映射字段。
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
en: 0f43f2e193e95043
|
||||
zh: 1c1efb062c06f7b8
|
||||
/desktop-ci-publication/package-and-publish:
|
||||
en: f7c12d343891e547
|
||||
zh: da596ee1366c2046
|
||||
en: ee8d79ac75482502
|
||||
zh: 1d986a76860ed262
|
||||
/desktop-ci-publication/recovery-and-records:
|
||||
en: b62a7484929f9941
|
||||
zh: c9dd06c77098e6ef
|
||||
en: 4ff6f0673d057ca8
|
||||
zh: 7171d060dd86db9a
|
||||
/desktop-ci-publication/restore-a-channel:
|
||||
en: d82e2ccf70cce397
|
||||
zh: d93dcb77cc4b463e
|
||||
|
||||
@@ -70,7 +70,7 @@ Environment approval gates this workflow, not arbitrary code running under the s
|
||||
|
||||
For publication to either test or production, choose Windows `signing=signed` or leave macOS **Skip notarization** unchecked. Unsigned and unnotarized packages are only for internal debugging; they cannot publish or provide automatic updates. Before installing dependencies, the hosted routing job rejects debugging artifacts identified by the selected run and attempt. Missing artifact metadata does not establish eligibility: the original machine still validates its retained manifest and signatures. A debugging-package rejection requires a new signed/notarized package and its run ID / attempt; retrying publication cannot qualify the existing bytes.
|
||||
|
||||
Select the platform's `Package Desktop` workflow, preset, source, channel and version mode. `source_ref` accepts repository branch names (for example `feature/desktop-update`) or `dsh-v*` tags for test; production accepts only `master` or `dsh-v*` tags. Names matching `dsh-v` followed by a digit and letters, digits, dots or hyphens are treated as tags, even if a branch has that name. Tags must match the selected product version. We recommend dispatching the packaging workflow from `master` and enter the source separately; raw commit SHAs, PR refs and fork repositories are not supported. The selected source must contain the CI adapters, so older sources fail before dependency installation. Only select trusted branches: their build code runs on the signing machine. This is an operating convention, not enforced workflow-origin or credential isolation. Publication uses the recorded commit and requires it to remain in the selected branch history; normal branch advances are allowed, deletion or a diverging force-push rejects. Tag publication requires the tag to still resolve to the recorded commit. Dispatch the publisher from `master`; its hosted routing job rejects other refs before scheduling the original machine. The packaged source commit is recorded separately. `version_mode=default` selects product for production and auto for test; product preserves the selected checkout version, auto appends date/run/attempt, and manual requires `build_version`. A latest package requires a stable version, so test/latest needs product or a stable manual version. The optional `test_release_id` is the test URL prefix: blank retains the machine default, successive upgrade tests on that platform need the same value, and defaults should be rotated periodically. It is not a GitHub run ID. See [packaging configuration](../../packaging/README.md#configuration-and-local-builds) for its validation and rotation limits.
|
||||
Select the platform's `Package Desktop` workflow, preset, source, channel and version mode. `source_ref` accepts repository branch names (for example `feature/desktop-update`) or `dsh-v*` tags for test; production accepts only `master` or `dsh-v*` tags. Names matching `dsh-v` followed by a digit and letters, digits, dots or hyphens are treated as tags, even if a branch has that name. Tags must match the selected product version. We recommend dispatching the packaging workflow from `master` and enter the source separately; raw commit SHAs, PR refs and fork repositories are not supported. The selected source must contain the CI adapters, so older sources fail before dependency installation. Only select trusted branches: their build code runs on the signing machine. This is an operating convention, not enforced workflow-origin or credential isolation. Publication uses the recorded commit and requires it to remain in the selected branch history; normal branch advances are allowed, deletion or a diverging force-push rejects. Tag publication requires the tag to still resolve to the recorded commit. Dispatch the publisher from `master`; its hosted routing job rejects other refs before scheduling the original machine. The packaged source commit is recorded separately. `version_mode=default` selects product for production and auto for test; product preserves the selected checkout version, auto appends date/run/attempt, and manual requires `build_version`. A stable-channel package requires a stable version, so test/stable needs product or a stable manual version. The optional `test_release_id` is the test URL prefix: blank retains the machine default, successive upgrade tests on that platform need the same value, and defaults should be rotated periodically. It is not a GitHub run ID. See [packaging configuration](../../packaging/README.md#configuration-and-local-builds) for its validation and rotation limits.
|
||||
|
||||
Packaging only retains files. After success, open **Publish Desktop · COS → Run workflow** on `master`, enter `package_run_id` from the packaging run URL, and optionally enter `package_run_attempt`. Blank selects the latest attempt, which must have succeeded; it never falls back to an older success. An explicit attempt selects that exact successful build. Windows must be signed; macOS must finish notarization and stapling. GitHub job evidence selects the original runner; its local manifest supplies the preset, version, source and channel before approval. No installer or request artifact is downloaded. Retained packages with the old `publishRequested=false` flag remain eligible; missing or changed local files stop publication.
|
||||
|
||||
@@ -80,7 +80,7 @@ Before installing dependencies on the original runner, preparation and the appro
|
||||
|
||||
## Recovery and records
|
||||
|
||||
After resolving a failure, re-run the failed publishing job to keep its prepared package attempt, or start a new publication with the explicit original package run and attempt. Re-running all jobs with a blank attempt resolves the latest package attempt again and requires a new approval. Do not rebuild solely to retry an upload. Missing objects are uploaded, identical objects are skipped, and conflicting same-name bytes stop the operation. Unknown write outcomes are reconciled by reading remote bytes. A newer channel version prevents a stale upload from replacing it. Each package updates only its embedded channel: nightly writes `nightly.yml` / `nightly-mac.yml`, latest writes `latest.yml` / `latest-mac.yml`. Latest binaries use a `latest/` subdirectory under the target binary prefix, separate from existing nightly objects. Publishing the same source/version to both channels requires two packages; an existing release tag is reused only for the same source commit.
|
||||
After resolving a failure, re-run the failed publishing job to keep its prepared package attempt, or start a new publication with the explicit original package run and attempt. Re-running all jobs with a blank attempt resolves the latest package attempt again and requires a new approval. Do not rebuild solely to retry an upload. Missing objects are uploaded, identical objects are skipped, and conflicting same-name bytes stop the operation. Unknown write outcomes are reconciled by reading remote bytes. A newer channel version prevents a stale upload from replacing it. Each package updates only its embedded channel: nightly writes `nightly.yml` / `nightly-mac.yml`, stable writes `stable.yml` / `stable-mac.yml`. Stable-channel binaries use a `stable/` subdirectory under the target binary prefix, separate from existing nightly objects. Publishing the same source/version to both channels requires two packages; an existing release tag is reused only for the same source commit.
|
||||
|
||||
The sibling `upload-records/` directory retains an operation identity, newline-delimited events and terminal result. Feed-write intents retain both previous and requested YAML. Failures record their stage, safe error classification and diagnostic; COS failures also identify `GetObject` or `PutObject`, plus a validated HTTP status and request ID when available. Access-denied reports name the permission to inspect. Raw provider responses and credentials are excluded. Audit initialization becomes visible only after its files are complete; abandoned `.initializing-*` directories are not scanned for completed publication. Writes and readback are recorded before proceeding; missing terminal results mean interruption, not success. Actions also keeps its operation record as a diagnostic artifact for 30 days. These local records are not tamper-proof. COS readback verifies storage bytes, not public CDN behavior; public download acceptance is separate.
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
发布到 test 或 production 都必须在 Windows 选择 `signing=signed`,或在 macOS 不勾选**跳过公证**。无签名、无公证的包仅限内部调试,不能发布或用于自动更新。托管路由 job 会在安装依赖前,依据所选 run 和 attempt 对应的 artifact 名称拒绝已标明的调试包。缺少 artifact 元数据不代表允许发布:原机器仍会验证留存清单和签名。遇到调试包拒绝提示时,需重新完成签名或公证,再使用新包的 run ID / attempt 发布;重跑发布不能使现有产物获得发布资格。
|
||||
|
||||
选择对应平台的 `Package Desktop` 工作流、preset、源码、通道和版本模式。test 的 `source_ref` 接受仓库内分支名(如 `feature/desktop-update`)或 `dsh-v*` tag;production 仅接受 `master` 或 `dsh-v*` tag。符合 `dsh-v` 后接数字、其余为字母、数字、点或连字符的名称按 tag 处理,即使存在同名分支。tag 必须与所选产品版本一致。建议从 `master` 启动打包工作流,再单独填写打包源码;不支持直接填写 commit SHA、PR ref 或 fork 仓库。源码必须包含 CI 适配脚本,旧源码会在安装依赖前失败。仅选择可信分支:其构建代码将在签名机器上执行。这是操作约定,不提供工作流来源强制限制或凭据隔离。发布使用记录的 commit,并要求该提交仍在所选分支历史中;正常追加提交不影响发布,删除分支或导致历史分叉的强推会拒绝。tag 发布要求 tag 仍指向记录的提交。从 `master` 发起发布;托管路由 job 会在派发到原机器前拒绝其他 ref。实际打包源码提交单独记录。`version_mode=default` 对 production 选择 product,对 test 选择 auto;product 保留所选源码版本,auto 追加日期/run/attempt,manual 要求填写 `build_version`。latest 包要求稳定版本,因此 test/latest 需选择 product 或稳定的手填版本。可选 `test_release_id` 是测试 URL 前缀:留空沿用机器默认值,同一平台连续升级测试需保持相同值,并应定期轮换默认值。它不是 GitHub run ID。格式校验和轮换限制见[打包配置](../../packaging/README.zh.md#configuration-and-local-builds)。
|
||||
选择对应平台的 `Package Desktop` 工作流、preset、源码、通道和版本模式。test 的 `source_ref` 接受仓库内分支名(如 `feature/desktop-update`)或 `dsh-v*` tag;production 仅接受 `master` 或 `dsh-v*` tag。符合 `dsh-v` 后接数字、其余为字母、数字、点或连字符的名称按 tag 处理,即使存在同名分支。tag 必须与所选产品版本一致。建议从 `master` 启动打包工作流,再单独填写打包源码;不支持直接填写 commit SHA、PR ref 或 fork 仓库。源码必须包含 CI 适配脚本,旧源码会在安装依赖前失败。仅选择可信分支:其构建代码将在签名机器上执行。这是操作约定,不提供工作流来源强制限制或凭据隔离。发布使用记录的 commit,并要求该提交仍在所选分支历史中;正常追加提交不影响发布,删除分支或导致历史分叉的强推会拒绝。tag 发布要求 tag 仍指向记录的提交。从 `master` 发起发布;托管路由 job 会在派发到原机器前拒绝其他 ref。实际打包源码提交单独记录。`version_mode=default` 对 production 选择 product,对 test 选择 auto;product 保留所选源码版本,auto 追加日期/run/attempt,manual 要求填写 `build_version`。stable 包要求稳定版本,因此 test/stable 需选择 product 或稳定的手填版本。可选 `test_release_id` 是测试 URL 前缀:留空沿用机器默认值,同一平台连续升级测试需保持相同值,并应定期轮换默认值。它不是 GitHub run ID。格式校验和轮换限制见[打包配置](../../packaging/README.zh.md#configuration-and-local-builds)。
|
||||
|
||||
打包只保留文件。成功后,在 `master` 上打开 **Publish Desktop · COS → Run workflow**,填写打包运行 URL 中的 `package_run_id`,并可填写 `package_run_attempt`。留空选择最新尝试,且该尝试必须成功,不会回退到较早的成功尝试;显式填写则选择对应的成功构建。Windows 必须完成签名,macOS 必须完成公证和 stapling。GitHub job 证据确定原 runner,本机清单在审批前提供 preset、版本、源码和通道。不会下载安装包或请求 artifact。旧清单的 `publishRequested=false` 不影响发布资格;本机文件缺失或变化会停止发布。
|
||||
|
||||
@@ -80,7 +80,7 @@ Environment 审批限制此工作流,不限制同一机器账户运行的任
|
||||
|
||||
## 恢复与记录
|
||||
|
||||
解决故障后重跑失败的发布 job,以保持准备阶段选定的打包尝试;也可显式填写原打包运行和尝试,发起新的发布。尝试留空时重跑所有 job 会重新解析最新打包尝试,并需要重新审批。不要仅为重试上传而重新打包。缺失对象会上传,相同对象跳过,同名但字节不同的对象会停止操作。写入结果不明时通过读取远端字节核对。频道已有更高版本时,旧上传不能替换它。每个包只更新自身内置通道:nightly 写入 `nightly.yml` / `nightly-mac.yml`,latest 写入 `latest.yml` / `latest-mac.yml`。latest 二进制位于目标二进制前缀下的 `latest/` 子目录,与已有 nightly 对象分离。同一源码和版本发布到两个通道需要分别打包;已有发布 tag 仅在源码提交相同时复用。
|
||||
解决故障后重跑失败的发布 job,以保持准备阶段选定的打包尝试;也可显式填写原打包运行和尝试,发起新的发布。尝试留空时重跑所有 job 会重新解析最新打包尝试,并需要重新审批。不要仅为重试上传而重新打包。缺失对象会上传,相同对象跳过,同名但字节不同的对象会停止操作。写入结果不明时通过读取远端字节核对。频道已有更高版本时,旧上传不能替换它。每个包只更新自身内置通道:nightly 写入 `nightly.yml` / `nightly-mac.yml`,stable 写入 `stable.yml` / `stable-mac.yml`。stable 二进制位于目标二进制前缀下的 `stable/` 子目录,与已有 nightly 对象分离。同一源码和版本发布到两个通道需要分别打包;已有发布 tag 仅在源码提交相同时复用。
|
||||
|
||||
同级 `upload-records/` 目录保留操作标识、逐行事件和最终结果。频道写入意图同时记录先前和目标 YAML。失败记录阶段、安全错误分类与诊断;COS 失败还标明 `GetObject` 或 `PutObject`,并在可用时记录经过校验的 HTTP 状态码和 request ID。访问拒绝提示会指出需检查的权限。不保存原始服务响应或凭据。审计初始化在文件完整后才可见;遗留的 `.initializing-*` 目录不参与已完成发布的扫描。写入和回读在继续操作前落盘;没有最终结果代表中断,不代表成功。Actions 还将操作记录保存为诊断 artifact,保留 30 天。这些本地记录不防篡改。COS 回读验证存储字节,不代表公开 CDN 行为;公开下载需单独验收。
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ function render(view) {
|
||||
const { locale: { id, messages }, policy, update, confirmation, navigation } = view
|
||||
const confirming = confirmation !== undefined
|
||||
const failed = update.phase === 'error'
|
||||
const configurationError = failed && update.failedOperation === 'configuration'
|
||||
const ready = update.phase === 'ready' || (failed && update.failedOperation === 'install')
|
||||
const downloadable = update.phase === 'available' || (failed && update.failedOperation === 'download')
|
||||
const authenticationRequired = policy.error === 'authentication-required'
|
||||
@@ -45,13 +46,14 @@ function render(view) {
|
||||
detail = view.restart === 'stopping-tasks' ? messages.mandatoryStopping : messages.mandatoryRestarting
|
||||
status = ''; primary = ''; action = ''
|
||||
}
|
||||
const error = localError ?? view.error ?? (authenticationRequired ? messages.policyLoginRequired : failed
|
||||
const error = localError ?? view.error ?? (configurationError ? messages.updateConfigurationRead
|
||||
: authenticationRequired ? messages.policyLoginRequired : failed
|
||||
? update.failedOperation === 'download' ? messages.mandatoryDownloadFailed
|
||||
: update.failedOperation === 'install'
|
||||
? preparationMessages[update.preparationFailure] ?? messages.mandatoryInstallFailed
|
||||
: messages.mandatoryUnavailable
|
||||
: update.phase === 'idle' && !policy.checking ? messages.mandatoryNoRelease : undefined)
|
||||
const technicalDetails = failed ? update.technicalDetails ?? update.message ?? '' : ''
|
||||
const technicalDetails = failed && !configurationError ? update.technicalDetails ?? update.message ?? '' : ''
|
||||
document.documentElement.lang = id
|
||||
document.title = messages.mandatoryTitle
|
||||
element('title').textContent = title
|
||||
@@ -73,7 +75,8 @@ function render(view) {
|
||||
element('update').disabled = busy && !confirming
|
||||
element('later').textContent = messages.updateLater
|
||||
element('later').hidden = !confirmation?.active
|
||||
element('refresh').textContent = authenticationRequired ? messages.policyLogin : messages.mandatoryRefresh
|
||||
element('refresh').textContent = configurationError ? messages.updateConfigurationDetails
|
||||
: authenticationRequired ? messages.policyLogin : messages.mandatoryRefresh
|
||||
element('refresh').hidden = confirming || view.restart !== undefined || (!authenticationRequired && (!!primary || !fallback))
|
||||
element('refresh').disabled = busy || policy.checking
|
||||
element('page').textContent = navigation ? messages.mandatoryReopen : messages.mandatoryPage
|
||||
|
||||
@@ -35,3 +35,8 @@ body.update-dialog.visible { opacity: 1; }
|
||||
.primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
|
||||
.secondary:hover:not(:disabled), .close:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
||||
}
|
||||
|
||||
button[aria-busy="true"] { opacity: 1; display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||||
button[aria-busy="true"]::before { content: ""; width: 14px; height: 14px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: reload-spin 800ms linear infinite; }
|
||||
@keyframes reload-spin { to { transform: rotate(360deg); } }
|
||||
@media (prefers-reduced-motion: reduce) { button[aria-busy="true"]::before { animation: none; } }
|
||||
|
||||
@@ -3,7 +3,7 @@ const api = window.dshUpdateDialog
|
||||
let view
|
||||
let responding = false
|
||||
function respond(index) {
|
||||
if (responding || view === undefined) return
|
||||
if (responding || view === undefined || view.busyButton !== undefined) return
|
||||
responding = true
|
||||
void api.respond(view.revision, index).catch(() => { responding = false })
|
||||
}
|
||||
@@ -11,7 +11,8 @@ document.getElementById('close').addEventListener('click', () => { respond(view.
|
||||
document.addEventListener('keydown', event => {
|
||||
if (event.key === 'Escape' && view !== undefined) { event.preventDefault(); respond(view.cancelId) }
|
||||
if (event.key !== 'Tab') return
|
||||
const controls = [...document.querySelectorAll('button, details:not([hidden]) > summary, details[open]:not([hidden]) > pre')]
|
||||
const controls = [...document.querySelectorAll('button:not(:disabled), details:not([hidden]) > summary, details[open]:not([hidden]) > pre')]
|
||||
if (controls.length === 0) { event.preventDefault(); return }
|
||||
const current = controls.indexOf(document.activeElement)
|
||||
const next = current < 0 ? (event.shiftKey ? controls.length - 1 : 0)
|
||||
: (current + (event.shiftKey ? -1 : 1) + controls.length) % controls.length
|
||||
@@ -32,6 +33,7 @@ function render(state) {
|
||||
document.getElementById('title').textContent = state.message
|
||||
document.getElementById('detail').textContent = state.detail
|
||||
document.getElementById('detail').hidden = state.detail === ''
|
||||
document.getElementById('close').disabled = state.busyButton !== undefined
|
||||
document.getElementById('close').setAttribute('aria-label', state.closeLabel)
|
||||
document.getElementById('technical-details').hidden = state.technicalDetails === ''
|
||||
document.getElementById('technical-details-label').textContent = state.technicalDetailsLabel
|
||||
@@ -42,6 +44,8 @@ function render(state) {
|
||||
const button = document.createElement('button')
|
||||
button.type = 'button'
|
||||
button.textContent = label
|
||||
button.disabled = state.busyButton !== undefined
|
||||
if (state.busyButton === index) button.setAttribute('aria-busy', 'true')
|
||||
button.className = index === 0 ? 'primary' : 'secondary'
|
||||
button.addEventListener('click', () => { respond(index) })
|
||||
document.getElementById('actions').append(button)
|
||||
|
||||
@@ -17,7 +17,7 @@ export function desktopLatestInstallerFilename(target: DesktopAutoUpdateTarget):
|
||||
/** Public updater URL and object prefixes for one release target. */
|
||||
export interface DesktopAutoUpdateConfig {
|
||||
readonly environment: DesktopAutoUpdateEnvironment
|
||||
readonly channel: 'nightly' | 'latest'
|
||||
readonly channel: 'nightly' | 'stable'
|
||||
readonly target: DesktopAutoUpdateTarget
|
||||
readonly origin: string
|
||||
readonly publicUrl: string
|
||||
@@ -68,7 +68,7 @@ export function desktopBuildRecordFilename(target: DesktopAutoUpdateTarget): str
|
||||
export function desktopUpdateMetadataFilename(
|
||||
version: string,
|
||||
platform: NodeJS.Platform,
|
||||
channel?: 'nightly' | 'latest',
|
||||
channel?: 'nightly' | 'stable',
|
||||
): string
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,7 @@ export function resolveDesktopUploadConfig(
|
||||
* @param env Packaging settings.
|
||||
* @returns Validated channel.
|
||||
*/
|
||||
export function resolveDesktopUpdateChannel(env: NodeJS.ProcessEnv): 'nightly' | 'latest'
|
||||
export function resolveDesktopUpdateChannel(env: NodeJS.ProcessEnv): 'nightly' | 'stable'
|
||||
|
||||
/**
|
||||
* Read a required nonempty deployment setting.
|
||||
|
||||
@@ -8,11 +8,11 @@ export const DESKTOP_AUTO_UPDATE_ENV = 'DSH_DESKTOP_AUTO_UPDATE_ENV'
|
||||
/**
|
||||
* Resolve the channel embedded in the installer; omitted local settings retain nightly.
|
||||
* @param {NodeJS.ProcessEnv} env Packaging or upload settings.
|
||||
* @returns {'nightly' | 'latest'} Validated channel.
|
||||
* @returns {'nightly' | 'stable'} Validated channel.
|
||||
*/
|
||||
export function resolveDesktopUpdateChannel(env) {
|
||||
const channel = env.DSH_DESKTOP_UPDATE_CHANNEL || 'nightly'
|
||||
if (channel !== 'nightly' && channel !== 'latest') throw new Error('desktop auto-update: invalid update channel')
|
||||
if (channel !== 'nightly' && channel !== 'stable') throw new Error('desktop auto-update: invalid update channel')
|
||||
return channel
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export function desktopBuildRecordFilename(target) {
|
||||
* Return the electron-builder channel metadata filename for an application version.
|
||||
* @param {string} version - Desktop semantic version.
|
||||
* @param {NodeJS.Platform} platform - Target platform.
|
||||
* @param {'nightly' | 'latest'} channel - Selected channel; omitted means nightly.
|
||||
* @param {'nightly' | 'stable'} channel - Selected channel; omitted means nightly.
|
||||
* @returns {string} Channel metadata filename emitted for the target.
|
||||
*/
|
||||
export function desktopUpdateMetadataFilename(version, platform, channel = 'nightly') {
|
||||
@@ -148,7 +148,7 @@ function httpsOrigin(value, name) {
|
||||
* @param {NodeJS.ProcessEnv} env - Packaging or upload environment.
|
||||
* @param {NodeJS.Platform} platform - Target Node.js platform.
|
||||
* @param {string} arch - Target Node.js architecture.
|
||||
* @returns {{ environment: 'test' | 'production', channel: 'nightly' | 'latest', target: 'mac-arm64' | 'mac-x64' | 'win-x64', origin: string, publicUrl: string, keyPrefix: string, binaryKeyPrefix: string }} Resolved updater configuration.
|
||||
* @returns {{ environment: 'test' | 'production', channel: 'nightly' | 'stable', target: 'mac-arm64' | 'mac-x64' | 'win-x64', origin: string, publicUrl: string, keyPrefix: string, binaryKeyPrefix: string }} Resolved updater configuration.
|
||||
* @throws {Error} When the test deployment lacks a valid HTTPS origin or a 32-character lowercase hexadecimal release ID.
|
||||
*/
|
||||
export function resolveDesktopAutoUpdateConfig(env, platform, arch) {
|
||||
@@ -177,7 +177,7 @@ export function resolveDesktopAutoUpdateConfig(env, platform, arch) {
|
||||
target,
|
||||
origin,
|
||||
keyPrefix,
|
||||
binaryKeyPrefix: `${releasePrefix}/bin/${target}${channel === 'latest' ? '/latest' : ''}`,
|
||||
binaryKeyPrefix: `${releasePrefix}/bin/${target}${channel === 'stable' ? '/stable' : ''}`,
|
||||
publicUrl: `${origin}/${keyPrefix}/`,
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,7 @@ export function resolveDesktopAutoUpdateConfig(env, platform, arch) {
|
||||
* @param {NodeJS.ProcessEnv} env - Upload environment.
|
||||
* @param {NodeJS.Platform} platform - Target Node.js platform.
|
||||
* @param {string} arch - Target Node.js architecture.
|
||||
* @returns {{ environment: 'test' | 'production', channel: 'nightly' | 'latest', target: 'mac-arm64' | 'mac-x64' | 'win-x64', origin: string, publicUrl: string, keyPrefix: string, binaryKeyPrefix: string, bucket: string, secretIdEnvName: string, secretKeyEnvName: string }} Resolved upload configuration.
|
||||
* @returns {{ environment: 'test' | 'production', channel: 'nightly' | 'stable', target: 'mac-arm64' | 'mac-x64' | 'win-x64', origin: string, publicUrl: string, keyPrefix: string, binaryKeyPrefix: string, bucket: string, secretIdEnvName: string, secretKeyEnvName: string }} Resolved upload configuration.
|
||||
* @throws {Error} When the selected deployment lacks a bucket or valid updater configuration.
|
||||
*/
|
||||
export function resolveDesktopUploadConfig(env, platform, arch) {
|
||||
|
||||
@@ -24,7 +24,7 @@ export function desktopCiVersion(environment, productVersion, now = new Date())
|
||||
}
|
||||
candidate = `${desktopBuildVersionPrefix(productVersion)}${date}.${environment.GITHUB_RUN_NUMBER}.${environment.GITHUB_RUN_ATTEMPT}`
|
||||
}
|
||||
if (environment.DSH_DESKTOP_UPDATE_CHANNEL === 'latest' && candidate.includes('-')) throw new Error('Latest channel requires a stable version; select product or manual stable version')
|
||||
if (environment.DSH_DESKTOP_UPDATE_CHANNEL === 'stable' && candidate.includes('-')) throw new Error('Stable channel requires a stable version; select product or manual stable version')
|
||||
try {
|
||||
const resolved = validateDesktopBuildVersion(candidate, productVersion)
|
||||
return resolved
|
||||
|
||||
@@ -12,7 +12,7 @@ const runSchema = z.object({
|
||||
const requestSchema = z.object({ source: z.object({
|
||||
repository: z.string(), runId: z.string(), attempt: z.string(), commit: z.string().regex(/^[a-f0-9]{40}$/u),
|
||||
workflowCommit: z.string(), sourceRef: z.string().min(1),
|
||||
channel: z.enum(['nightly', 'latest']),
|
||||
channel: z.enum(['nightly', 'stable']),
|
||||
runnerName: z.string(), target: z.enum(['mac-arm64', 'mac-x64', 'win-x64']), preset: z.enum(['test', 'production']),
|
||||
qualification: z.enum(['signed', 'notarized', 'unsigned', 'unnotarized']),
|
||||
}).refine(source => source.preset === 'test' || /^(?:master|dsh-v[0-9][0-9A-Za-z.-]*)$/u.test(source.sourceRef),
|
||||
@@ -25,7 +25,7 @@ export interface DesktopPublishRoute {
|
||||
readonly attempt: string
|
||||
readonly commit: string
|
||||
readonly sourceRef: string
|
||||
readonly channel: 'nightly' | 'latest'
|
||||
readonly channel: 'nightly' | 'stable'
|
||||
readonly runnerName: string
|
||||
readonly runnerLabel: string
|
||||
readonly target: 'mac-arm64' | 'mac-x64' | 'win-x64'
|
||||
|
||||
@@ -14,7 +14,7 @@ const sourceSchema = z.strictObject({
|
||||
commit: z.string().regex(/^[a-f0-9]{40}$/u),
|
||||
workflowCommit: z.string().regex(/^[a-f0-9]{40}$/u),
|
||||
sourceRef: z.string().min(1),
|
||||
channel: z.enum(['nightly', 'latest']),
|
||||
channel: z.enum(['nightly', 'stable']),
|
||||
runnerName: z.string().min(1),
|
||||
// Storage accepts every Desktop target; CI routing separately limits currently provisioned platforms.
|
||||
target: z.enum(['mac-arm64', 'mac-x64', 'win-x64']),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user