feat: add searxng web-search provider, openrouter cost balance UI, offline scripts; update source-launch and docs
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Some checks failed
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / windows node 24 / wine blocking (push) Has been skipped
CI / wine apt cache (push) Successful in 58s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 2m46s
Deploy documentation / deploy (push) Has been skipped
E2E (real DeepSeek API) / e2e (push) Failing after 1m18s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m18s
Landlock Run / Matrix (push) Successful in 13s
Release (vendor) / Pack npm tarballs (push) Failing after 3m43s
Release (dsh) / Pack npm tarballs (push) Failing after 1m53s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m51s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-08-19-searxng-web-search-provider.md
|
||||
2026-08-19-searxng-web-search-provider.md: 377dba2af133677b4fd1fd591cb3929ac2acda33
|
||||
2026-08-19-searxng-web-search-provider.zh.md: 29d406831ea947c383263dd5cdba840fa9e448a8
|
||||
@@ -0,0 +1,38 @@
|
||||
# Agent Note: SearXNG web search provider
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-19-searxng-web-search-provider.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
The harness [web capability seam](2026-06-24-web-capability-seam.md) ships three search providers — Exa, Perplexity, and native DeepSeek web search — all of which are hosted API services. None fits a deployment that wants to self-host its own metasearch. SearXNG is a self-hostable metasearch engine with a JSON API, so it is the natural backend for such deployments. It differs from the hosted providers in two ways that shape the adapter: there is no single canonical SearXNG endpoint (each instance is its own server, so `baseURL` cannot have a sensible public default), and an engine does not expose a per-request result-count control (page size is instance configuration).
|
||||
|
||||
## Decision
|
||||
|
||||
Add `@deepseek-ai/dsh-web-search-searxng` (`packages/web/web-search-searxng`), a `WebSearchProvider` that registers under the stable id `searxng` and calls a SearXNG instance's JSON API (`GET {base}/search?q=...&format=json`).
|
||||
|
||||
- `baseURL` has **no default**; the schema keeps it optional so an omitted or unparseable value surfaces as an unavailable provider (`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`) rather than a boot error. The product requires evidence before an unsupported default endpoint, and no single public SearXNG instance is authoritative.
|
||||
- Config flows through the `web-search-searxng` Settings namespace (layered over the cordis entry), and the provider projects the resolved section **per search**, so a settings edit applies to the next call without re-registration — the same projection topology as `dsh-web-search-deepseek`.
|
||||
- The provider carries **no API key and no `Authorization` header**: it targets open instances, so no credential can leak to a redirect target. HTTP redirects are still rejected with `redirect: 'error'`, mapping to `WEB_PROVIDER_ERROR`, matching the seam's redirect hygiene.
|
||||
- `language` (SearXNG `language`, default `auto`) and `timeRange` (`day`|`week`|`month`|`year`) are exposed; a request carries no result count because the API cannot bound one, so the seam's `maxResults` truncation is the sole bound. `content` is omitted (no generated answer).
|
||||
- Results map to `WebSearchSource`: `url` ← `url`, `title` ← `title`, `snippet` ← `content`. Unlike Exa, a result **without a snippet is kept** — URL and title are still useful — so sources are not dropped for missing snippets.
|
||||
|
||||
The package follows the provider topology of `dsh-web-search-exa`: a function/namespace plugin (`inject: ['web']`) that registers into `ctx.web`, `inject`-less in the sense that it registers a backend rather than owning the `ctx.web` key, with package-owned mapping and error translation and an empty package invariant companion.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Give `baseURL` a public-instance default
|
||||
|
||||
Rejected. A default like `https://searx.be` would pick an arbitrary, possibly unreachable or rate-limited public instance and silently send traffic to a third party without the operator choosing it. Requiring an explicit `baseURL` makes the provider unavailable until configured, which is the honest misconfiguration signal the seam already names (`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`).
|
||||
|
||||
### Route SearXNG through an OpenAI-compatible gateway instead of a dedicated provider
|
||||
|
||||
Rejected. The harness search seam consumes `WebSearchProvider` backends, not arbitrary OpenAI-compatible endpoints; the LLM provider configuration surface does not feed `ctx.web`. A dedicated SearXNG `WebSearchProvider` is the only path that reaches the web search tools without a bespoke glue service.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A deployment gains a self-hosted search backend whose query goes to the operator's own instance, with optional language and recency filters but no per-query result-count control — single-page results are truncated by the seam.
|
||||
- The provider set grows to four search backends; `searchProvider` (or `$DSH_WEB_SEARCH_PROVIDER`) names `searxng` to select it, or auto-selection picks it when it is the only usable provider.
|
||||
- Redirect rejection means a misconfigured `baseURL` that answers 3xx surfaces as `WEB_PROVIDER_ERROR` rather than silently following; with no credentials on the request this is hygiene, not secret protection.
|
||||
- Snippet-less results are retained, so `tool-web` renders fallback URL/hostname labels for them rather than dropping the source.
|
||||
@@ -0,0 +1,38 @@
|
||||
# Agent Note: SearXNG web search provider
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-19-searxng-web-search-provider.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
harness [web 能力 seam](2026-06-24-web-capability-seam.md) 自带了三个搜索提供方——Exa、Perplexity 与 DeepSeek 原生 web 搜索——它们都是托管 API 服务。它们都不适合希望自托管元搜索的部署场景。SearXNG 是具备 JSON API 的可自托管元搜索引擎,因此是这类部署的自然后端。它在这两个方面与托管提供方不同,进而塑造了适配器:不存在统一的 SearXNG 端点(每个实例都是独立服务器,因此 `baseURL` 无法有合理的公共默认值),且引擎不提供按请求控制结果数量的方式(页面大小属于实例配置)。
|
||||
|
||||
## 决策
|
||||
|
||||
新增 `@deepseek-ai/dsh-web-search-searxng`(`packages/web/web-search-searxng`),一个以稳定 id `searxng` 注册、调用 SearXNG 实例 JSON API(`GET {base}/search?q=...&format=json`)的 `WebSearchProvider`。
|
||||
|
||||
- `baseURL` **无默认值**;schema 保持其为可选,因此省略或无法解析的值呈现为不可用提供方(`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`),而非启动错误。产品要求先有证据再采用未经验证的默认端点,且不存在权威的公共 SearXNG 实例。
|
||||
- 配置经由 `web-search-searxng` Settings 命名空间流入(叠加在 cordis 条目之上),提供方**按搜索**投影解析后的分节,因此设置变更在下次调用即生效,无需重新注册——与 `dsh-web-search-deepseek` 的投影拓扑一致。
|
||||
- 提供方**不携带 API 密钥或 `Authorization` 头**:它面向开放实例,因此不会有凭据泄露给重定向目标。HTTP 重定向仍以 `redirect: 'error'` 拒绝,映射为 `WEB_PROVIDER_ERROR`,与 seam 的重定向卫生一致。
|
||||
- 公开 `language`(SearXNG `language`,默认 `auto`)与 `timeRange`(`day`|`week`|`month`|`year`);请求不携带结果数量,因为 API 无法限制数量,因此 seam 的 `maxResults` 截断是唯一上限。省略 `content`(无生成答案)。
|
||||
- 结果映射为 `WebSearchSource`:`url` ← `url`、`title` ← `title`、`snippet` ← `content`。与 Exa 不同,**没有 snippet 的结果会被保留**——URL 与标题仍然有用——因此不会因缺少 snippet 而丢弃来源。
|
||||
|
||||
该包遵循 `dsh-web-search-exa` 的提供方拓扑:函数/命名空间插件(`inject: ['web']`)向 `ctx.web` 注册后端而非拥有 `ctx.web` 键,拥有包级映射与错误转换,并带一个空的包级 invariant 伴生插件。
|
||||
|
||||
## 备选方案
|
||||
|
||||
### 为 `baseURL` 提供公共实例默认值
|
||||
|
||||
拒绝。类似 `https://searx.be` 的默认值会挑选任意、可能不可达或被限速的公共实例,并在未由操作员选择的情况下把流量悄悄发给第三方。要求显式 `baseURL` 会使提供方在配置前不可用,这正是 seam 已命名的诚实误配置信号(`WEB_PROVIDER_CONFIGURED_UNAVAILABLE`)。
|
||||
|
||||
### 通过 OpenAI 兼容网关路由 SearXNG,而非专用提供方
|
||||
|
||||
拒绝。harness 搜索 seam 消费的是 `WebSearchProvider` 后端,而非任意 OpenAI 兼容端点;LLM 提供方配置面并不供给 `ctx.web`。专用 SearXNG `WebSearchProvider` 是不借助自制胶水服务而触达 web 搜索工具的唯一路径。
|
||||
|
||||
## 后果
|
||||
|
||||
- 部署获得自托管搜索后端,其查询发往操作员自己的实例,支持可选的语言与时效过滤器,但没有按查询的结果数量控制——单页结果由 seam 截断。
|
||||
- 提供方集合增至四个搜索后端;`searchProvider`(或 `$DSH_WEB_SEARCH_PROVIDER`)指定 `searxng` 以选择它,或当其是唯一可用提供方时自动选择。
|
||||
- 拒绝重定向意味着返回 3xx 的误配置 `baseURL` 呈现为 `WEB_PROVIDER_ERROR` 而非静默跟随;由于请求无凭据,这是卫生而非密钥保护。
|
||||
- 无 snippet 的结果被保留,因此 `tool-web` 为它们渲染回退的 URL/主机名标签,而非丢弃来源。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-20-openrouter-cost-balance-readouts.md
|
||||
2026-08-20-openrouter-cost-balance-readouts.md: 339acc4140eb6c34d1c3910a93babbeda9a28cac
|
||||
2026-08-20-openrouter-cost-balance-readouts.zh.md: 16606ae58b346a1eeb10f703eae748665f64ea83
|
||||
@@ -0,0 +1,90 @@
|
||||
# Agent Note: OpenRouter cost and balance readouts in the web GUI
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-20-openrouter-cost-balance-readouts.zh.md)
|
||||
|
||||
> Scope: two new packages — `@deepseek-ai/dsh-openrouter-usage` (host gateway +
|
||||
> projection) and `@deepseek-ai/dsh-client-ui-openrouter-usage` (web readouts) —
|
||||
> composed into the web profile. The gateway resolves the same
|
||||
> `OPENROUTER_API_KEY` credential that OpenRouter LLM routing uses.
|
||||
|
||||
## Problem
|
||||
|
||||
A user running the web GUI with LLM calls routed through OpenRouter has no
|
||||
visibility into what those calls cost or how much account credit remains.
|
||||
Token counts exist in the session log (`assistant/message.usage`, `assistant/chunk`
|
||||
usage chunks) and the token-meter projection, but there is no monetary figure
|
||||
anywhere in the repository, and no durable record of OpenRouter's per-model
|
||||
pricing or account balance. The harness deliberately discards the provider's
|
||||
own cost metadata (the pi-ai adapter zeroes `usage.cost`), so cost had to be
|
||||
reconstructed from token counts against a pricing table fetched from
|
||||
OpenRouter's public API.
|
||||
|
||||
## Decision
|
||||
|
||||
**Cost is a client-facing read-model projection, never a logged or model-visible
|
||||
fact.** The gateway fetches OpenRouter's model pricing table (`GET /models`,
|
||||
`pricing.prompt`/`completion` USD per token, flat `request` fee, disclosed
|
||||
cache rates) and folds each session's logged usage against it in the
|
||||
`openRouterCost` `SessionProjectionMap` entry. Balance comes from OpenRouter's
|
||||
canonical balance endpoint `GET /credits` (available credits = `total_credits`
|
||||
minus the spent `total_usage`, the figure the dashboard surfaces), merged with
|
||||
`GET /auth/key` for the key label and the monthly token budget
|
||||
(`usage`/`limit`); a standard `sk-or-v1` key returns **no** `credits` field on
|
||||
`/auth/key`, so that endpoint alone would always read a null balance. Both
|
||||
fetches go through plain `globalThis.fetch` with `redirect: 'error'`, the host
|
||||
standard for credential-bearing outbound calls.
|
||||
|
||||
**The web surface is two slot entries.** A per-session cost readout registers
|
||||
into `conversation.composer.dock` (the shipped stats line's dock), reading the
|
||||
`openRouterCost` projection through the `useProjection` framework seat; a live
|
||||
balance badge registers into `sidebar.footer.action`, polling the balance
|
||||
Remote on a 60s client-side interval. Both hide themselves when their data is
|
||||
absent: cost hides before any priced step, balance renders an empty dashes
|
||||
marker when the snapshot has no value yet.
|
||||
|
||||
**The API key is a credential reference, seeded identically to routing.**
|
||||
`apiKeyEnv` (default `OPENROUTER_API_KEY`) resolves through the credentials
|
||||
seam with an environment fallback exactly like `dsh-web-search-deepseek`,
|
||||
captured per refresh in a thunk so a credentials or settings change applies on
|
||||
the next tick without a rebuild.
|
||||
|
||||
**No key → no fetch, and the plugin stays inert.** No key produces an empty
|
||||
pricing table (every step stays unpriced), an all-`null` balance, and the
|
||||
client reads null-ish values it renders as absent — the same dormant posture
|
||||
as the LLM adapters, not a load-time failure.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Persisting per-request cost as a session event** — cost is derived from
|
||||
events already logged (`assistant/message` usage, `assistant/chunk` usage);
|
||||
an event would duplicate accounting, be priced at the wrong time (pricing
|
||||
changes with `pricingRefreshMs`), and leak a client-facing figure into the
|
||||
durable log. The projection stays the single read-model home.
|
||||
- **Balance via `/auth/key` alone** — the plan's original shape; the real
|
||||
endpoint response for a standard `sk-or-v1` key carries no `credits` field,
|
||||
verified against OpenRouter's live API, so the snapshot would always read
|
||||
null. `/credits` is the authoritative balance endpoint and `/auth/key` only
|
||||
supplements label + monthly budget.
|
||||
- **Reading `usage.cost` from pi-ai** — the harness deliberately discards
|
||||
provider cost metadata (the adapter pins `NO_COST`); reconstructing from
|
||||
tokens × table is the only available path and is documented as an estimate.
|
||||
- **A settings card and a dedicated credentials UI** — deferred: the readouts
|
||||
work with the existing credential the routing already uses; a card adds
|
||||
surface without changing behavior.
|
||||
|
||||
## Consequences
|
||||
|
||||
`test:gui` stays green (3800 tests) and both packages carry unit tests plus a
|
||||
real-composition loader test that boots the shipped YAML shape through the
|
||||
vendored Loader, resolves the key from a credentials document, and asserts
|
||||
the mocked `/models` and `/credits` fetches populate the projection and the
|
||||
snapshot. The numbers are an estimate, not itemized billing: the projection
|
||||
prices `tokens × pricing` per step, pricing is applied as of fold time
|
||||
(refreshing pricing affects only cells folded afterward), cache rates fall
|
||||
back to the prompt rate when undisclosed, and free-tier / promotional pricing
|
||||
may differ from the model table — all documented under Known Limitations.
|
||||
Nothing is model-visible: the agent loop, session log format, and SDKs are
|
||||
untouched. Deferred: a settings card, and any per-generation billing fidelity
|
||||
that would require logging OpenRouter generation ids.
|
||||
@@ -0,0 +1,32 @@
|
||||
# Agent Note: OpenRouter cost and balance readouts in the web GUI
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-20-openrouter-cost-balance-readouts.md) | 中文
|
||||
|
||||
> 范围:两个新包 —— `@deepseek-ai/dsh-openrouter-usage`(主机端网关 + 投影)与 `@deepseek-ai/dsh-client-ui-openrouter-usage`(Web 读数)—— 组合进 web profile。网关解析与 OpenRouter LLM 路由相同的 `OPENROUTER_API_KEY` 凭证。
|
||||
|
||||
## Problem
|
||||
|
||||
使用 Web GUI 并通过 OpenRouter 路由 LLM 调用的用户,无法看到这些调用花费了多少、账户还剩多少额度。token 计数存在于会话日志(`assistant/message.usage`、`assistant/chunk` 用量块)和 token-meter 投影中,但仓库里没有任何货币数字,也没有 OpenRouter 按模型定价或账户余额的持久记录。harness 有意丢弃 provider 自身的成本元数据(pi-ai 适配器将 `usage.cost` 清零),因此成本必须根据 token 计数与从 OpenRouter 公共 API 拉取的定价表重建。
|
||||
|
||||
## Decision
|
||||
|
||||
**费用是面向客户端的只读模型投影,绝不是被记录或模型可见的事实。** 网关拉取 OpenRouter 的模型定价表(`GET /models`,`pricing.prompt`/`completion` USD 每 token、flat 的 `request` 费用、披露的缓存费率),并在 `openRouterCost` 的 `SessionProjectionMap` 条目中按各会话已记录的用量折叠它。余额来自 OpenRouter 权威余额端点 `GET /credits`(可用余额 = `total_credits` 减去已花费的 `total_usage`,即仪表盘展示的数字),并与 `GET /auth/key` 合并以获取 key 标签与月度 token 预算(`usage`/`limit`);标准 `sk-or-v1` key 在 `/auth/key` 上**不**返回 `credits` 字段,因此仅靠该端点总会读到空余额。两个请求都使用 `redirect: 'error'` 的 `global.fetch` —— 携带凭证的出站调用的主机标准。
|
||||
|
||||
**Web 表面是两个 slot 条目。**每会话费用读数注册进 `conversation.composer.dock`(随附统计行的 dock),通过 `useProjection` 框架座位读取 `openRouterCost` 投影;实时余额徽章注册进 `sidebar.footer.action`,以客户端 60s 间隔轮询余额 Remote。两者在数据缺失时自我隐藏:费用在任何已计价步骤前隐藏,余额在快照尚无值时渲染空的破折号标记。
|
||||
|
||||
**API key 是凭证引用,与路由的初始化方式一致。**`apiKeyEnv`(默认 `OPENROUTER_API_KEY`)与 `dsh-web-search-deepseek` 完全一样,通过凭证接缝加环境回退解析,按刷新在 thunk 中捕获,使凭证或设置变更无需重建即可在下一周期生效。
|
||||
|
||||
**无 key → 不发请求,插件保持惰性。**无 key 产生空定价表(每个 step 都保持未计价)、全 `null` 余额,客户端把 null 渲染为缺失 —— 与 LLM 适配器相同的休眠姿态,而非加载期失败。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **将会话事件持久化每次请求费用** —— 费用派生自已记录的事件(`assistant/message` 用量、`assistant/chunk` 用量);事件会重复记账、在错误时间计价(`pricingRefreshMs` 变更定价),并使客户端数字泄漏进持久日志。投影保持唯一的读数模型归属。
|
||||
- **仅用 `/auth/key` 读取余额** —— 计划的初始形态;真实端点响应中标准 `sk-or-v1` key 不携带 `credits` 字段(已对照 OpenRouter 线上 API 验证),因此快照总会是 null。`/credits` 是权威余额端点,`/auth/key` 只是补充标签与月度预算。
|
||||
- **读 pi-ai 的 `usage.cost`** —— harness 刻意丢弃 provider 成本元数据(适配器钉定 `NO_COST`);从 token × 定价表重建是唯一可行路径,并记录为估算。
|
||||
- **提供设置卡片与专用凭证 UI** —— 推迟:读数可以用路由已使用的既有凭证工作;卡片只是界面,不改变行为。
|
||||
|
||||
## Consequences
|
||||
|
||||
`test:gui` 保持全绿(3800 个测试),两个包都带单元测试加真实组合 loader 测试:把带变形的 YAML 形状(session + 投影注册表 + 凭证 + openrouter-usage)跑进 vendored Loader,从凭证文档解析 key,并断言被 mock 的 `/models` 与 `/credits` 请求填充投影与快照。数字是估算而非逐项账单:投影按 step 以 `tokens × pricing` 计价,计价格次折叠时生效(刷新定价只影响其后折叠的单元格),未披露时缓存费率回退到 prompt 费率,免费档位/促销价格可能与模型表不同 —— 全部记录在 Known Limitations。没有任何内容对模型可见:agent 循环、会话日志格式与 SDK 均未改动。推迟:设置卡片、以及需要记录 OpenRouter generation id 的逐代账单保真度。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-19-offline-pnpm-prefetch-scripts.md
|
||||
2026-08-19-offline-pnpm-prefetch-scripts.md: c00eda84455b582f1ee006617d3031473b14f9e4
|
||||
2026-08-19-offline-pnpm-prefetch-scripts.zh.md: 6314726a7a30cfa79b188d2a98eefa3f2525437a
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Offline pnpm prefetch scripts
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-19-offline-pnpm-prefetch-scripts.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
A source checkout cannot boot `dsh web` until a supported Node runtime, `pnpm install`, and `pnpm run build` have materialized `node_modules`, package `lib/` output, and `apps/web/dist`. Install reaches the npm registry (and Corepack reaches it for `pnpm@11.7.0`); the Node distro comes from `nodejs.org/dist`. Node's fetch client cannot use `socks5h://` proxies, so a machine whose only ambient proxy is SOCKS fails `pnpm install` even while curl through the same proxy succeeds. An air-gapped follow-up install therefore needs a same-OS prefetch of the official Node archive, the lockfile store, and a Corepack home that does not call the registry.
|
||||
|
||||
## Decision
|
||||
|
||||
[`scripts/offline/download-deps`](../../../../scripts/offline/download-deps.ps1) (POSIX twin [`.sh`](../../../../scripts/offline/download-deps.sh)) runs while `nodejs.org` and the registry are reachable. It downloads the official Node zip/tar for the current OS and CPU from `DSH_NODE_DIST_BASE` (default `https://nodejs.org/dist`), verifies `SHASUMS256.txt`, unpacks into ignored [`.offline-cache/node/runtime`](../../../../.gitignore), and writes `.offline-cache/node/runtime.json`. The Node version is `DSH_NODE_VERSION` when set, otherwise the running Node when it satisfies `engines.node` (`^22.19.0 || >=24.0.0`), otherwise the newest `v24.x` from `index.json` that publishes this platform's archive. It then sets `COREPACK_HOME` to `.offline-cache/corepack`, `corepack prepare`s the root `package.json` `packageManager` pin using that Node, and `pnpm fetch --frozen-lockfile --store-dir .offline-store`. [`scripts/offline/install-deps`](../../../../scripts/offline/install-deps.ps1) (POSIX twin [`.sh`](../../../../scripts/offline/install-deps.sh)) unpacks the cached Node if needed, prepends it to `PATH`, sets `COREPACK_ENABLE_NETWORK=0`, and runs `pnpm install --offline --frozen-lockfile` against that store. Optional `-Build` / `--build` runs `npm run build:lib` then `pnpm --dir apps/web run build`, avoiding the nested `pnpm --filter` invocation inside `npm run build:web` that can pick a different Corepack pnpm than `packageManager`.
|
||||
|
||||
`DSH_NPM_HTTP_PROXY`, when set, is an HTTP overlay applied only around `corepack prepare` and `pnpm fetch`, then the process proxy variables are restored so a caller SOCKS session is unchanged. curl uses the inherited `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` (SOCKS included) and falls back to `DSH_NPM_HTTP_PROXY` only when none of those are set. A `socks5` or `socks5h` value for Node/pnpm (from `DSH_NPM_HTTP_PROXY`, or from the inherited proxies when no HTTP overlay is set) fails before Corepack runs, because that scheme is not a supported pnpm transport.
|
||||
|
||||
The scripts prefetch Node and the lockfile for the host OS and CPU they run on. They do not download LLM weights, Playwright browsers, or a local inference server. Copy the checkout including `.offline-store` and `.offline-cache` to the offline machine.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Tell contributors to copy a finished `node_modules` tree and skip a second install.** A copied isolated pnpm layout is host-specific and breaks when the store path or package content-address links do not match; `pnpm install --offline` against a fetched store is the documented pnpm air-gap path.
|
||||
|
||||
**Check `supportedArchitectures` into the download script so one store serves win32 and linux.** Rejected for the default: optional native packages (esbuild, koffi, node-pty) must match the install host, and a mixed store still needs a same-OS install to unpack the right binaries. Cross-OS prefetch remains an explicit local `pnpm fetch` config, not the script default.
|
||||
|
||||
**Add root `package.json` scripts that call these files.** Nested `pnpm` under `npm run` is what already mismatches Corepack versions on `build:web`; the wrappers stay invoked as files so they control `COREPACK_HOME` and `--store-dir`.
|
||||
|
||||
**Overwrite the caller's `HTTP_PROXY` for the whole download script.** Rejected: `.\download-deps.ps1` runs in the current PowerShell session, so a session-wide overwrite would replace a working SOCKS proxy for later commands. curl can use SOCKS; only Node/pnpm need the HTTP overlay, and only for those child processes.
|
||||
|
||||
**Ship the Node MSI or pkg installer.** Rejected: those installers need administrator rights and a global install; the official zip/tar is relocatable, and install only unpacks it and prepends `PATH`.
|
||||
|
||||
**Hardcode one Node version in the script.** Rejected: a pinned patch goes stale against `engines.node` and CI (`22.19`, `24`, `26`). Matching the running Node keeps optional native packages aligned with the prefetch host; `index.json` is the fallback when Node is not on `PATH`.
|
||||
|
||||
**Vendor the pnpm store in git.** The store is hundreds of megabytes of registry tarballs; `.gitignore` keeps `.offline-store/` and `.offline-cache/` untracked.
|
||||
|
||||
## Consequences
|
||||
|
||||
Air-gap preparation is a two-command, same-OS pair and does not change the online `pnpm install` path. The offline machine does not need a preinstalled Node: install unpacks the cached official distro. `dsh plugin … add` of a registry or git spec still needs a network on the machine that runs it. Local chat still needs a separately installed OpenAI-compatible server; these scripts do not substitute for that.
|
||||
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Offline pnpm prefetch scripts
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-19-offline-pnpm-prefetch-scripts.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
源码检出在受支持的 Node 运行时、`pnpm install` 与 `pnpm run build` 物化 `node_modules`、各包 `lib/` 以及 `apps/web/dist` 之前无法启动 `dsh web`。安装会访问 npm registry(Corepack 为 `pnpm@11.7.0` 也会访问);Node 发行包来自 `nodejs.org/dist`。Node 的 fetch 客户端不能使用 `socks5h://` 代理,因此环境里只有 SOCKS 代理的机器即使 curl 经同一代理成功,`pnpm install` 仍会失败。后续在隔离网络中安装,需要按相同 OS 预取官方 Node 归档、lockfile store,以及一个不再访问 registry 的 Corepack home。
|
||||
|
||||
## Decision
|
||||
|
||||
[`scripts/offline/download-deps`](../../../../scripts/offline/download-deps.ps1)(POSIX 对侧 [`.sh`](../../../../scripts/offline/download-deps.sh))在能访问 `nodejs.org` 与 registry 时运行。它从 `DSH_NODE_DIST_BASE`(默认 `https://nodejs.org/dist`)下载当前 OS 与 CPU 的官方 Node zip/tar,校验 `SHASUMS256.txt`,解压到已忽略的 [`.offline-cache/node/runtime`](../../../../.gitignore),并写入 `.offline-cache/node/runtime.json`。Node 版本在设置了 `DSH_NODE_VERSION` 时用它,否则在运行中的 Node 满足 `engines.node`(`^22.19.0 || >=24.0.0`)时用该版本,否则用 `index.json` 中发布了当前平台归档的最新 `v24.x`。随后把 `COREPACK_HOME` 设为 `.offline-cache/corepack`,用该 Node 按根目录 `package.json` 的 `packageManager` 引脚执行 `corepack prepare`,并运行 `pnpm fetch --frozen-lockfile --store-dir .offline-store`。[`scripts/offline/install-deps`](../../../../scripts/offline/install-deps.ps1)(POSIX 对侧 [`.sh`](../../../../scripts/offline/install-deps.sh))在需要时解压缓存的 Node,把它前置到 `PATH`,设置 `COREPACK_ENABLE_NETWORK=0`,并对该 store 执行 `pnpm install --offline --frozen-lockfile`。可选的 `-Build` / `--build` 先运行 `npm run build:lib`,再运行 `pnpm --dir apps/web run build`,从而避开 `npm run build:web` 内部嵌套的 `pnpm --filter`——它可能选出与 `packageManager` 不同的 Corepack pnpm。
|
||||
|
||||
若设置了 `DSH_NPM_HTTP_PROXY`,它只作为 HTTP 覆盖层套在 `corepack prepare` 与 `pnpm fetch` 周围,随后恢复进程内的代理变量,因此调用方会话里的 SOCKS 代理保持不变。curl 使用继承的 `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY`(包括 SOCKS),仅在这些变量都未设置时才回退到 `DSH_NPM_HTTP_PROXY`。供 Node/pnpm 使用的值(来自 `DSH_NPM_HTTP_PROXY`,或在未设置 HTTP 覆盖层时来自继承的代理变量)若为 `socks5` 或 `socks5h`,会在 Corepack 运行前失败,因为该 scheme 不是 pnpm 支持的传输。
|
||||
|
||||
脚本只为运行时所在主机的 OS 与 CPU 预取 Node 与 lockfile。它们不下载 LLM 权重、Playwright 浏览器或本地推理服务器。把包含 `.offline-store` 与 `.offline-cache` 的检出复制到离线机器。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**让贡献者复制已完成的 `node_modules` 树并跳过第二次安装。** 复制出的 isolated pnpm 布局与主机相关,在 store 路径或包 content-address 链接不匹配时会损坏;针对已 fetch 的 store 运行 `pnpm install --offline` 才是 pnpm 文档中的隔离网络路径。
|
||||
|
||||
**把 `supportedArchitectures` 写进 download 脚本,使一份 store 同时服务 win32 与 linux。** 拒绝作为默认:可选原生包(esbuild、koffi、node-pty)必须匹配安装主机,混合 store 仍需在相同 OS 上安装才能解出正确二进制。跨 OS 预取仍是显式的本地 `pnpm fetch` 配置,不是脚本默认。
|
||||
|
||||
**在根 `package.json` 增加调用这些文件的 scripts。** `npm run` 下嵌套 `pnpm` 正是 `build:web` 上 Corepack 版本错配的原因;包装器保持按文件调用,以便自行控制 `COREPACK_HOME` 与 `--store-dir`。
|
||||
|
||||
**在整个 download 脚本期间覆盖调用方的 `HTTP_PROXY`。** 拒绝:`.\download-deps.ps1` 在当前 PowerShell 会话中运行,会话级覆盖会把后续命令仍可用的 SOCKS 代理换掉。curl 可以使用 SOCKS;只有 Node/pnpm 需要 HTTP 覆盖层,且仅针对这些子进程。
|
||||
|
||||
**随脚本分发 Node 的 MSI 或 pkg 安装包。** 拒绝:那些安装器需要管理员权限和全局安装;官方 zip/tar 可重定位,install 只需解压并前置 `PATH`。
|
||||
|
||||
**在脚本中写死某一个 Node 版本。** 拒绝:钉死的 patch 会相对 `engines.node` 与 CI(`22.19`、`24`、`26`)过时。与正在运行的 Node 对齐,可使可选原生包与预取主机一致;`PATH` 上没有 Node 时回退到 `index.json`。
|
||||
|
||||
**把 pnpm store 纳入 git。** store 是数百 MB 的 registry tarball;`.gitignore` 使 `.offline-store/` 与 `.offline-cache/` 保持未跟踪。
|
||||
|
||||
## Consequences
|
||||
|
||||
隔离网络准备是一对相同 OS 上的两条命令,不改变在线 `pnpm install` 路径。离线机器不需要预先安装 Node:install 会解压缓存的官方发行包。在运行 `dsh plugin … add` 安装 registry 或 git spec 的机器上仍然需要网络。本地对话仍需要另行安装的 OpenAI-compatible 服务器;这些脚本不能替代它。
|
||||
@@ -0,0 +1,6 @@
|
||||
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-19-source-dsh-http-proxy-overlay.md
|
||||
2026-08-19-source-dsh-http-proxy-overlay.md: 814633aeda22d1afb306e9f42c26a0701e62b746
|
||||
2026-08-19-source-dsh-http-proxy-overlay.zh.md: 878a6024027bbc6081cc08b5891d7da6dc243895
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Source dsh HTTP proxy overlay
|
||||
|
||||
Status: implemented
|
||||
|
||||
English | [中文](2026-08-19-source-dsh-http-proxy-overlay.zh.md)
|
||||
|
||||
## Problem
|
||||
|
||||
Machines whose ambient proxy is SOCKS can reach OpenRouter with curl and still fail `pnpm dsh` HTTPS fetches: Node, undici, and pi-ai accept `http://` / `https://` proxy URLs and reject `socks5` / `socks5h`. Setting `HTTPS_PROXY` in the calling PowerShell or bash session also routes later shell commands through that HTTP proxy. Product `.env` files cannot supply `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, or `NO_PROXY` because those names are bootstrap-only. Assigning those names on `process.env` after Node has started does not rebind `fetch`: undici reads `NODE_USE_ENV_PROXY` and `HTTPS_PROXY` at process start. Node `--env-file-if-exists` also leaves inherited names in place, so a user-level `HTTPS_PROXY=socks5h://…` still wins.
|
||||
|
||||
## Decision
|
||||
|
||||
The root `dsh` script runs [`scripts/run-source-dsh.ts`](../../../../scripts/run-source-dsh.ts) as `node --import tsx/esm scripts/run-source-dsh.ts`. [`applySourceDshHttpProxy`](../../../../scripts/apply-source-dsh-http-proxy.ts) writes HTTP proxy variables onto a copy of `process.env`. When gitignored [`.dsh-http-proxy.env`](../../../../.gitignore) or `DSH_HTTP_PROXY` supplies an overlay and `DSH_SOURCE_HTTP_PROXY_APPLIED` is unset, the wrapper respawns the same Node argv with that environment so `NODE_USE_ENV_PROXY=1` and the HTTP proxy URLs exist before `fetch` initializes. The marker prevents a second respawn. The calling shell is unchanged.
|
||||
|
||||
`DSH_HTTP_PROXY`, when set to a non-empty HTTP(S) URL, overwrites `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY` (both cases) and sets `NODE_USE_ENV_PROXY=1`. When `NO_PROXY` and `no_proxy` are both unset, it sets them to `localhost,127.0.0.1,::1`. Otherwise the overlay file's parsed assignments are applied, then any remaining SOCKS value on those six names is replaced by the file's HTTP URL so an omitted `ALL_PROXY` cannot keep `socks5h://`. A `socks5` or `socks5h` winning overlay fails before the CLI boots. Absent both sources, the inherited environment is unchanged and there is no respawn. The wrapper prints the applied HTTP URL once to stderr before respawn.
|
||||
|
||||
The installed `apps/cli/lib/bin.js` path is unchanged: it has no checkout overlay file.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Tell the user to export `HTTPS_PROXY` in the shell before `pnpm dsh`.** That reaches OpenRouter, but the same variables remain on later commands in that session, including tools that should keep a SOCKS proxy.
|
||||
|
||||
**Put proxy names in the invoking `.env` or `$DSH_HOME/.env`.** Rejected by the [configuration-source-ownership decision](../architecture/2026-08-04-configuration-source-ownership.md): bootstrap network variables may come only from the inherited process environment.
|
||||
|
||||
**Assign `NODE_USE_ENV_PROXY` only inside the wrapper after Node has started.** OpenRouter traffic uses `fetch`; Node binds the env-proxy dispatcher at process start, so a later `process.env` write does not change it.
|
||||
|
||||
**Use Node `--env-file-if-exists` without respawning.** That file does not replace names the parent already set, so an inherited `socks5h://` `HTTPS_PROXY` still reaches `fetch`.
|
||||
|
||||
## Consequences
|
||||
|
||||
Source `pnpm dsh` can use an HTTP overlay without mutating the calling shell. An inherited SOCKS `HTTPS_PROXY` is replaced in the respawned Node process, not in the shell. Contributors without `DSH_HTTP_PROXY` or `.dsh-http-proxy.env` see the previous inheritance behavior and no extra process. A SOCKS-only overlay fails loud instead of timing out against OpenRouter. The published bin still requires the caller to pass HTTP proxy variables when the host needs a proxy. `--use-env-proxy` is not on the command: Node 22.19 in CI rejects the flag.
|
||||
|
||||
## Testing
|
||||
|
||||
`scripts/apply-source-dsh-http-proxy.spec.ts` covers no-overlay, `DSH_HTTP_PROXY` overwrite of SOCKS, file parse, file-versus-`DSH_HTTP_PROXY` precedence, SOCKS leftover on omitted names, SOCKS rejection, and the one-shot respawn marker. `apps/cli/tests/source-launch.compat.spec.ts` pins the root `dsh` command to `scripts/run-source-dsh.ts` and smokes that vector.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Agent Note: Source dsh HTTP proxy overlay
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-08-19-source-dsh-http-proxy-overlay.md) | 中文
|
||||
|
||||
## 问题
|
||||
|
||||
环境中只有 SOCKS 代理的机器可以用 curl 访问 OpenRouter,但 `pnpm dsh` 的 HTTPS fetch 仍会失败:Node、undici 和 pi-ai 接受 `http://` / `https://` 代理 URL,并拒绝 `socks5` / `socks5h`。在调用方的 PowerShell 或 bash 会话里设置 `HTTPS_PROXY` 也会让该会话中后续命令走这个 HTTP 代理。产品 `.env` 文件不能提供 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 或 `NO_PROXY`,因为这些名称只能来自进程启动环境。在 Node 启动之后再往 `process.env` 写入这些名称不会重新绑定 `fetch`:undici 在进程启动时读取 `NODE_USE_ENV_PROXY` 和 `HTTPS_PROXY`。Node `--env-file-if-exists` 同样不会替换已继承的名称,因此用户级的 `HTTPS_PROXY=socks5h://…` 仍然生效。
|
||||
|
||||
## 决策
|
||||
|
||||
根目录的 `dsh` 脚本以 `node --import tsx/esm scripts/run-source-dsh.ts` 运行 [`scripts/run-source-dsh.ts`](../../../../scripts/run-source-dsh.ts)。[`applySourceDshHttpProxy`](../../../../scripts/apply-source-dsh-http-proxy.ts) 把 HTTP 代理变量写入 `process.env` 的副本。当被 gitignore 的 [`.dsh-http-proxy.env`](../../../../.gitignore) 或 `DSH_HTTP_PROXY` 提供覆盖层且未设置 `DSH_SOURCE_HTTP_PROXY_APPLIED` 时,包装层用该环境按相同的 Node argv 再拉起一次进程,以便在 `fetch` 初始化之前就存在 `NODE_USE_ENV_PROXY=1` 和 HTTP 代理 URL。该标记防止第二次再拉起。调用方 shell 不变。
|
||||
|
||||
当 `DSH_HTTP_PROXY` 设为非空的 HTTP(S) URL 时,它会覆盖 `HTTP_PROXY` / `HTTPS_PROXY` / `ALL_PROXY`(两种大小写)并设置 `NODE_USE_ENV_PROXY=1`。当 `NO_PROXY` 与 `no_proxy` 都未设置时,它将它们设为 `localhost,127.0.0.1,::1`。否则先应用覆盖文件中解析出的赋值,再把这六个名称上残留的 SOCKS 值换成文件中的 HTTP URL,以免漏写的 `ALL_PROXY` 仍指向 `socks5h://`。最终生效的覆盖层若为 `socks5` 或 `socks5h`,会在 CLI 启动前失败。两种来源都不存在时,继承的环境保持不变,也不会再拉起进程。包装层在再拉起之前会把生效的 HTTP URL 打印到 stderr 一次。
|
||||
|
||||
已安装的 `apps/cli/lib/bin.js` 路径不变:它没有检出级覆盖文件。
|
||||
|
||||
## 考虑过的备选方案
|
||||
|
||||
**让用户在运行 `pnpm dsh` 之前于 shell 中导出 `HTTPS_PROXY`。**这样可以访问 OpenRouter,但这些变量会留在该会话的后续命令上,包括本应继续使用 SOCKS 代理的工具。
|
||||
|
||||
**把代理名称放进调用目录的 `.env` 或 `$DSH_HOME/.env`。**被[配置来源归属决策](../architecture/2026-08-04-configuration-source-ownership.md)拒绝:网络启动变量只能来自继承的进程环境。
|
||||
|
||||
**仅在包装层内、Node 启动之后赋值 `NODE_USE_ENV_PROXY`。** OpenRouter 流量走 `fetch`;Node 在进程启动时绑定 env-proxy dispatcher,随后再写 `process.env` 不会改变它。
|
||||
|
||||
**使用 Node `--env-file-if-exists` 且不再拉起进程。**该文件不会替换父进程已经设置的名称,因此继承来的 `socks5h://` `HTTPS_PROXY` 仍会到达 `fetch`。
|
||||
|
||||
## 影响
|
||||
|
||||
源码路径的 `pnpm dsh` 可以使用 HTTP 覆盖层,而不修改调用方 shell。继承来的 SOCKS `HTTPS_PROXY` 会在再拉起的 Node 进程中被替换,而不是在 shell 中。没有 `DSH_HTTP_PROXY` 或 `.dsh-http-proxy.env` 的贡献者仍看到原来的继承行为,也不会多一个进程。仅有 SOCKS 的覆盖层会立即失败,而不是在访问 OpenRouter 时超时。已发布的 bin 在宿主需要代理时,仍要求调用方传入 HTTP 代理变量。命令行不加 `--use-env-proxy`:CI 中的 Node 22.19 会拒绝该 flag。
|
||||
|
||||
## 测试
|
||||
|
||||
`scripts/apply-source-dsh-http-proxy.spec.ts` 覆盖无覆盖层、`DSH_HTTP_PROXY` 覆盖 SOCKS、文件解析、文件与 `DSH_HTTP_PROXY` 的优先级、文件未写出的名称上残留的 SOCKS、SOCKS 拒绝,以及一次性再拉起标记。`apps/cli/tests/source-launch.compat.spec.ts` 将根目录 `dsh` 命令钉在 `scripts/run-source-dsh.ts` 上,并对该启动向量做冒烟测试。
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-10-source-run-without-managed-installer.md
|
||||
2026-08-10-source-run-without-managed-installer.md: ce618c88327fc11ad0cad9042e171800a5492663
|
||||
2026-08-10-source-run-without-managed-installer.zh.md: e54ec93b8bd13a36b7e8b6813899d5a1e27b788c
|
||||
2026-08-10-source-run-without-managed-installer.md: 3deb66367cc869bdef04527ca13300f0558d40d6
|
||||
2026-08-10-source-run-without-managed-installer.zh.md: 70b9d3fb604dc472841e44977317dc46d8e352d4
|
||||
|
||||
@@ -12,7 +12,7 @@ That lifecycle is not required to run or develop DeepSeek Harness from a source
|
||||
|
||||
## Decision
|
||||
|
||||
The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` launches `apps/cli/src/bin.ts` directly through `node --import tsx/esm`; artifact generation is the separate `pnpm run build` operation defined by the [source-launch/build separation decision](2026-08-12-separate-source-launch-from-build.md). The package script forwards arguments and inherits the caller's environment, including `NODE_USE_ENV_PROXY=1` when a supporting Node version must honor `HTTP_PROXY` and `HTTPS_PROXY`. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
|
||||
The repository supports source execution through its root `pnpm` scripts. The `dsh` entry in `package.json` launches [`scripts/run-source-dsh.ts`](../../../../scripts/run-source-dsh.ts) through `node --import tsx/esm`; that wrapper overlays HTTP proxy variables for this Node process ([source HTTP proxy overlay](../process/2026-08-19-source-dsh-http-proxy-overlay.md)) and then dispatches `apps/cli/src/bin.ts`. Artifact generation is the separate `pnpm run build` operation defined by the [source-launch/build separation decision](2026-08-12-separate-source-launch-from-build.md). The package script forwards arguments and inherits the caller's environment; the overlay can replace inherited SOCKS proxy variables so Node fetch can reach providers such as OpenRouter. Users select Web with `pnpm dsh web` and headless execution with `pnpm dsh --profile headless "task"`. The independent ACP example remains available through `pnpm run demo:acp`.
|
||||
|
||||
The repository does not distribute a source installer, an installer test suite, or skills that assume a managed `current` symlink and timestamped staging worktrees. Users own source checkout placement, Git updates, and any launcher they create outside the repository.
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ Status: implemented
|
||||
|
||||
## 决策
|
||||
|
||||
仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh` 项通过 `node --import tsx/esm` 直接启动 `apps/cli/src/bin.ts`;产物生成是独立的 `pnpm run build` 操作,由[源码启动与构建分离决策](2026-08-12-separate-source-launch-from-build.md)规定。该包脚本会转发参数并继承调用方环境;当支持环境代理的 Node 版本必须遵循 `HTTP_PROXY` 和 `HTTPS_PROXY` 时,调用方可设置 `NODE_USE_ENV_PROXY=1`。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACP(Agent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
|
||||
仓库通过根目录的 `pnpm` 脚本支持从源码运行。`package.json` 中的 `dsh` 项通过 `node --import tsx/esm` 启动 [`scripts/run-source-dsh.ts`](../../../../scripts/run-source-dsh.ts);该包装层仅为本 Node 进程套用 HTTP 代理变量([源码 HTTP 代理覆盖层](../process/2026-08-19-source-dsh-http-proxy-overlay.md)),然后分派 `apps/cli/src/bin.ts`。产物生成是独立的 `pnpm run build` 操作,由[源码启动与构建分离决策](2026-08-12-separate-source-launch-from-build.md)规定。该包脚本会转发参数并继承调用方环境;该覆盖层可以替换继承来的 SOCKS 代理变量,以便 Node fetch 能够访问 OpenRouter 等提供方。用户使用 `pnpm dsh web` 选择 Web,使用 `pnpm dsh --profile headless "task"` 选择无头执行。独立的 ACP(Agent Client Protocol)示例仍可通过 `pnpm run demo:acp` 运行。
|
||||
|
||||
仓库不分发源码安装器、安装器测试套件,也不分发依赖受管理的 `current` 符号链接和带时间戳 staging worktree 的 skill。源码检出的存放位置、Git 更新,以及用户在仓库外创建的任何启动器均由用户负责。
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-12-separate-source-launch-from-build.md
|
||||
2026-08-12-separate-source-launch-from-build.md: 639e9de0ce13a571e7f83929d9c76eac2664daab
|
||||
2026-08-12-separate-source-launch-from-build.zh.md: bbb55123860f2bba0209e95e803fa7f49328bdef
|
||||
2026-08-12-separate-source-launch-from-build.md: 0fba024bee8b3099be625a5727c6d1a53248162e
|
||||
2026-08-12-separate-source-launch-from-build.zh.md: 6ce95330bb2a4c9f63bb0e3d969b07eb43941036
|
||||
|
||||
@@ -12,7 +12,7 @@ Source modules reached through tsx and browser modules reached through built bun
|
||||
|
||||
## Decision
|
||||
|
||||
The root `dsh` script only runs `node --import tsx/esm apps/cli/src/bin.ts`. `pnpm run build` remains the separate operation that generates package and frontend artifacts. Source users run the build before the first production-like launch and whenever frontend or client-plugin artifacts need refreshing.
|
||||
The root `dsh` script runs `node --import tsx/esm scripts/run-source-dsh.ts`, which overlays HTTP proxy variables for that Node process and then dispatches `apps/cli/src/bin.ts`. `pnpm run build` remains the separate operation that generates package and frontend artifacts. Source users run the build before the first production-like launch and whenever frontend or client-plugin artifacts need refreshing.
|
||||
|
||||
Missing Typert host artifacts fail profile boot through module-resolution errors without a build instruction. Once those host artifacts exist, missing frontend and client-plugin artifacts fail at startup with diagnostics that direct the user to `pnpm run build`. The launcher does not validate artifact freshness: existing stale frontend or client-plugin bundles are accepted and can run older browser code until the next build. After package Node halves have been built once, `pnpm run dev:web` rebuilds only packages that declare `dsh.client`; it keeps client-plugin bundles current and activates their hot-reload path, but does not rebuild the frontend shell.
|
||||
|
||||
@@ -30,7 +30,7 @@ This decision owns build scheduling only. The [tsx ESM source-launch decision](.
|
||||
|
||||
- Repeated source launches do not wait for a complete repository build, and build output is not mixed with CLI output.
|
||||
- Source users own artifact freshness. Missing artifacts stop startup, but only frontend and client-plugin failures direct users to `pnpm run build`; existing stale frontend and client-plugin bundles can silently serve older browser code.
|
||||
- TUI, Web, and headless selection, argument forwarding, environment inheritance, and the tsx ESM launch vector remain unchanged.
|
||||
- TUI, Web, and headless selection, argument forwarding, and the tsx ESM launch vector remain unchanged. HTTP proxy overlay for the source Node process is owned by the [source HTTP proxy overlay decision](../process/2026-08-19-source-dsh-http-proxy-overlay.md).
|
||||
- The root onboarding and CLI reference show build and launch as separate commands and document the stale-artifact behavior.
|
||||
|
||||
## Verification
|
||||
|
||||
@@ -12,7 +12,7 @@ TypeScript 源码启动器无需在每次调用前完成整个仓库的构建。
|
||||
|
||||
## 决策
|
||||
|
||||
根目录的 `dsh` 脚本只运行 `node --import tsx/esm apps/cli/src/bin.ts`。`pnpm run build` 仍是生成包与前端产物的独立操作。源码用户在首次进行类生产启动前运行构建,并在前端或 Client plugin 产物需要刷新时再次运行。
|
||||
根目录的 `dsh` 脚本运行 `node --import tsx/esm scripts/run-source-dsh.ts`,该包装层仅为本 Node 进程套用 HTTP 代理变量,然后分派 `apps/cli/src/bin.ts`。`pnpm run build` 仍是生成包与前端产物的独立操作。源码用户在首次进行类生产启动前运行构建,并在前端或 Client plugin 产物需要刷新时再次运行。
|
||||
|
||||
Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解析错误而失败。这些 Host 产物存在后,如果前端或 Client plugin 产物缺失,启动会失败,诊断信息会指示用户运行 `pnpm run build`。启动器不会验证产物是否为最新:已有的陈旧前端或 Client plugin 组合包仍会被接受,并可能继续运行旧版浏览器代码,直至下次构建。各包的 Node 半侧至少构建过一次后,`pnpm run dev:web` 只重建声明了 `dsh.client` 的包;它会保持 Client plugin 组合包为最新状态并启用其热重载路径,但不会重建前端 shell。
|
||||
|
||||
@@ -30,7 +30,7 @@ Typert Host 产物缺失时,profile 启动会因不含构建指引的模块解
|
||||
|
||||
- 重复的源码启动无需等待完整的仓库构建,构建输出也不会与 CLI 输出混在一起。
|
||||
- 源码用户负责产物新鲜度。产物缺失会阻止启动,但只有前端与 Client plugin 产物缺失的错误会指示用户运行 `pnpm run build`;已有的过期前端与 Client plugin 组合包可能静默提供旧版浏览器代码。
|
||||
- TUI、Web 与无头模式选择、参数转发、环境继承,以及 tsx ESM 启动方式保持不变。
|
||||
- TUI、Web 与无头模式选择、参数转发,以及 tsx ESM 启动方式保持不变。源码 Node 进程的 HTTP 代理覆盖层由[源码 HTTP 代理覆盖层决策](../process/2026-08-19-source-dsh-http-proxy-overlay.md)规定。
|
||||
- 根目录上手指南与 CLI 参考将构建和启动列为独立命令,并说明过期产物行为。
|
||||
|
||||
## 验证
|
||||
|
||||
Reference in New Issue
Block a user