Merge commit '2f717c0467d7174d7fe0011c4a787450f7328a78' into codex/product-subagent-one-shot-background

This commit is contained in:
pku-xht
2026-08-13 18:54:56 +08:00
324 changed files with 1894 additions and 66686 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-deepseek-onboarding-credential-setup.md
2026-07-30-deepseek-onboarding-credential-setup.md: ebf5c66cffa8a7db90328dfcdd04e4134a78e3b1 2026-07-30-deepseek-onboarding-credential-setup.md: 823d10a723af70ec4ff51018b8b86198db0f5c29
2026-07-30-deepseek-onboarding-credential-setup.zh.md: 3df63c59fd23acf32c16dac8f4ca373409ab2968 2026-07-30-deepseek-onboarding-credential-setup.zh.md: 7e8d79c23c4b1489bfd818c90558f36509635486

View File

@@ -12,9 +12,9 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
**One readiness projection owns both Models and onboarding facts.** `ui-settings-models` keeps a single store that joins `llm.providers({})`, redacted `settings.describe({})`, and batched `credentials.describe({refs})`. The onboarding projection selects the `deepseek-official` configurable-provider entry owned by the `llm-deepseek` namespace and empty settings path, reads the effective `apiKeyEnv`, and evaluates the matching credential descriptor. A live route with the same provider id but no matching configurable-provider declaration is adapter-absent for onboarding. A configured process-environment credential is ready and remains read-only. **One readiness projection owns both Models and onboarding facts.** `ui-settings-models` keeps a single store that joins `llm.providers({})`, redacted `settings.describe({})`, and batched `credentials.describe({refs})`. The onboarding projection selects the `deepseek-official` configurable-provider entry owned by the `llm-deepseek` namespace and empty settings path, reads the effective `apiKeyEnv`, and evaluates the matching credential descriptor. A live route with the same provider id but no matching configurable-provider declaration is adapter-absent for onboarding. A configured process-environment credential is ready and remains read-only.
**The settings shell contributes ordering and navigation, not provider policy.** `ui-settings` declares a root-scoped `settings.onboarding` list slot and mounts one ordered step at a time while the current surface is the empty Hero. The active registrant receives `complete()` and a private `openSection(id)` callback; completion transfers ownership to the next entry. `ui-settings-models` registers the DeepSeek step and its Models section through `slots.inject()`, so each contribution follows its declaration lifetime without making plugin load order a contract, and independently contributed dialogs cannot stack. The product-wide welcome step that precedes it is owned separately by [the versioned welcome decision](2026-07-30-versioned-gui-welcome-onboarding.md). **The settings shell contributes ordering, not provider policy.** `ui-settings` declares a root-scoped `settings.onboarding` list slot and mounts one ordered step at a time while the current surface is the empty Hero. The active registrant receives `complete()` and a private `openSection(id)` callback; completion transfers ownership to the next entry. `ui-settings-models` registers the DeepSeek step, the preceding welcome notice, and its Models section through `slots.inject()`, so every contribution follows one client Cordis plugin's lifecycle and the dialogs cannot stack. Their common presentation is owned by the [shared-modal onboarding decision](2026-08-13-shared-modal-product-onboarding.md).
**The prompt routes to the one credential editor.** A mounted, active adapter with a resolved, writable, unconfigured reference presents one action that opens Settings on Models. The existing DeepSeek setup card there exclusively owns the password input, `credentials.set({ref, value})`, write failures, and post-write refresh; the onboarding overlay never holds or submits a secret. An unavailable settings or credential capability keeps its deployment diagnostic and routes to the same page, while an absent adapter remains skipped because navigation cannot mount a Cordis plugin. **The prompt renders the existing credential editor inline.** A mounted, active adapter with a resolved, writable, unconfigured reference renders `ProviderEditor` in credential-only mode inside the shared onboarding modal. The same component owns the password input, validation, `credentials.set({ref, value})`, write failures, and post-write refresh; credential-only mode emits no provider-settings mutation. Configure later completes only this coordinator pass. An absent adapter remains skipped because the browser cannot mount a missing Cordis plugin.
**Unavailable states do not capture the product.** An absent configurable-provider entry, inactive route, failed initial join, read-only deployment, or unresolved settings or credential capability completes the step without rendering because the onboarding action cannot repair that state. The Models page remains the deployment diagnostic and retry surface. Configure later completes a missing-credential step for the current mounted coordinator pass and writes no completion fact. Settings, credential, provider-topology, and connection invalidations all refresh the shared join, so an external credential update completes an open step without a reload. **Unavailable states do not capture the product.** An absent configurable-provider entry, inactive route, failed initial join, read-only deployment, or unresolved settings or credential capability completes the step without rendering because the onboarding action cannot repair that state. The Models page remains the deployment diagnostic and retry surface. Configure later completes a missing-credential step for the current mounted coordinator pass and writes no completion fact. Settings, credential, provider-topology, and connection invalidations all refresh the shared join, so an external credential update completes an open step without a reload.
@@ -22,7 +22,7 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
**A separate onboarding store and readiness RPC sequence** — rejected because it would create a second client-side interpretation of provider identity, settings paths, secret sidecars, credential references, and invalidation ordering beside the Models page. **A separate onboarding store and readiness RPC sequence** — rejected because it would create a second client-side interpretation of provider identity, settings paths, secret sidecars, credential references, and invalidation ordering beside the Models page.
**A second API-key editor inside onboarding** — rejected because the Models page already renders its DeepSeek setup card for exactly this state. Duplicating its secret draft, write errors, and configured-state convergence would add a second security-sensitive UI without another user capability. **A standalone API-key implementation inside onboarding** — rejected because it would duplicate the Models editor's secret draft, validation, errors, and configured-state convergence. The modal renders the existing `ProviderEditor` in a constrained mode instead.
**Writing the API key into provider settings** — rejected because a literal secret would enter the settings mutation path and whole-section replacement cannot safely reconstruct redacted values. Credential storage is already the product seam and supplies immediate invalidation. **Writing the API key into provider settings** — rejected because a literal secret would enter the settings mutation path and whole-section replacement cannot safely reconstruct redacted values. Credential storage is already the product seam and supplies immediate invalidation.
@@ -30,4 +30,4 @@ The [web configuration plane](../architecture/2026-07-30-web-config-plane.md) ma
## Consequences ## Consequences
The ordered flow leads from the product notice to the shipped adapter's existing editor without restarting: a keyless browser test boots the real Web composition under an isolated harness home, acknowledges the notice, follows the DeepSeek page to Models, stores a generated key through that page into the home's `.credentials.yaml`, verifies no key reaches DOM, ARIA, or browser console output, and confirms the running page reports configured. The full keyless Web replay lane also pins that a non-configurable replay route with the same provider id does not block unrelated journeys. Pure readiness and React tests pin managed-file and process-environment credentials, missing providers and capabilities, navigation, cancellation, external invalidation, and coordinator transfer. The flow deliberately inherits the configuration plane's documented base limitations rather than adding local secret storage, redaction, or settings replacement workarounds. The ordered flow leads from the product notice to an inline key form without restarting: a keyless browser test boots the real Web composition under an isolated harness home, acknowledges the notice, stores a generated key from the shared modal into the home's `.credentials.yaml`, verifies no key reaches DOM, ARIA, or browser console output, and confirms the ordinary Models page reports configured. The full keyless Web replay lane also pins that a non-configurable replay route with the same provider id does not block unrelated journeys. Pure readiness and React tests pin managed-file and process-environment credentials, missing providers and capabilities, cancellation, external invalidation, and coordinator transfer. The flow deliberately inherits the configuration plane's documented base limitations rather than adding local secret storage, redaction, or settings replacement workarounds.

View File

@@ -12,9 +12,9 @@ Status: implemented
**Models 与首次使用引导共享同一个就绪状态投影。**`ui-settings-models` 维护一个 store,把 `llm.providers({})`、脱敏后的 `settings.describe({})` 和批量调用的 `credentials.describe({refs})` 联接为同一份状态。首次使用投影选取由 `llm-deepseek` namespace 与空 settings path 持有的 `deepseek-official` 可配置提供方条目,读取生效的 `apiKeyEnv`,并检查对应的凭据描述符。同 provider id 但没有匹配可配置提供方声明的存活路由,在首次使用引导中视为适配器缺失。通过进程环境提供的凭据若已配置,则判定为就绪并保持只读。 **Models 与首次使用引导共享同一个就绪状态投影。**`ui-settings-models` 维护一个 store,把 `llm.providers({})`、脱敏后的 `settings.describe({})` 和批量调用的 `credentials.describe({refs})` 联接为同一份状态。首次使用投影选取由 `llm-deepseek` namespace 与空 settings path 持有的 `deepseek-official` 可配置提供方条目,读取生效的 `apiKeyEnv`,并检查对应的凭据描述符。同 provider id 但没有匹配可配置提供方声明的存活路由,在首次使用引导中视为适配器缺失。通过进程环境提供的凭据若已配置,则判定为就绪并保持只读。
**设置外壳只贡献排序与导航,不持有提供方策略。** `ui-settings` 声明一个根作用域的 `settings.onboarding` list slot,并在当前界面为空白 Hero 时,每次只挂载一个有序步骤。当前注册方会收到 `complete()` 和私有 `openSection(id)` 回调;完成当前步骤后,所有权转交给下一项。`ui-settings-models` 通过 `slots.inject()` 注册 DeepSeek 步骤及其 Models 分区,使每项贡献都跟随自身的声明生命周期,不让插件加载顺序成为约定;独立贡献的对话框也无法堆叠。排在它之前的产品级欢迎步骤由[版本化欢迎决策](2026-07-30-versioned-gui-welcome-onboarding.md)单独持有。 **设置外壳只贡献排序,不持有提供方策略。** `ui-settings` 声明一个根作用域的 `settings.onboarding` list slot,并在当前界面为空白 Hero 时,每次只挂载一个有序步骤。当前注册方会收到 `complete()` 和私有 `openSection(id)` 回调;完成当前步骤后,所有权转交给下一项。`ui-settings-models` 通过 `slots.inject()` 注册 DeepSeek 步骤、排在它之前的欢迎声明及 Models 分区,因此所有贡献都跟随同一个 client Cordis 插件的生命周期,两个弹窗也无法堆叠。它们的共用展示由[共用弹窗引导决策](2026-08-13-shared-modal-product-onboarding.md)持有。
**首次使用页面只负责跳转到唯一的凭据编辑器。**适配器已挂载且处于活跃状态,其引用可解析、可写但尚未配置时,界面会显示一个操作按钮,用于打开「设置」的 Models 分区。该分区已有的 DeepSeek 设置卡片全权负责密码输入框、`credentials.set({ref, value})`、写入失败处理和写入后刷新;首次使用页面绝不持有或提交 secret。 **首次使用弹窗行内渲染既有凭据编辑器。** 适配器已挂载且处于活跃状态,其引用可解析、可写但尚未配置时,`ProviderEditor` 会以仅凭据模式渲染在共用引导弹窗中。同一个组件全权负责密码输入框、校验、`credentials.set({ref, value})`、写入失败处理和写入后刷新;仅凭据模式不会发出提供方 settings 变更。「稍后配置」只完成协调器当前这一轮。适配器缺失时仍跳过,因为浏览器不能挂载缺失的 Cordis 插件。
**不可用状态不会占住产品。** 可配置提供方条目缺失、路由不活跃、初始联接失败、部署只读或设置/凭据能力无法解析时,都会直接完成而不渲染该步骤,因为首次使用引导无法修复这些状态。Models 页仍是部署诊断与重试界面。「稍后配置」只会完成协调器当前这一次缺少凭据的步骤,不写入任何完成状态。设置、凭据、提供方拓扑和连接失效事件都会刷新共享联接,因此外部凭据更新无需重新加载页面即可完成已打开的步骤。 **不可用状态不会占住产品。** 可配置提供方条目缺失、路由不活跃、初始联接失败、部署只读或设置/凭据能力无法解析时,都会直接完成而不渲染该步骤,因为首次使用引导无法修复这些状态。Models 页仍是部署诊断与重试界面。「稍后配置」只会完成协调器当前这一次缺少凭据的步骤,不写入任何完成状态。设置、凭据、提供方拓扑和连接失效事件都会刷新共享联接,因此外部凭据更新无需重新加载页面即可完成已打开的步骤。
@@ -22,7 +22,7 @@ Status: implemented
**为首次使用引导单设 store 与就绪状态 RPC 调用序列**:不予采用,因为这会在 Models 页之外,再建立一套客户端解释,用于判定提供方身份、设置路径、secret 槽位的伴随信息、凭据引用及失效事件顺序。 **为首次使用引导单设 store 与就绪状态 RPC 调用序列**:不予采用,因为这会在 Models 页之外,再建立一套客户端解释,用于判定提供方身份、设置路径、secret 槽位的伴随信息、凭据引用及失效事件顺序。
**在首次使用引导中增设第二个 API key 编辑器**:不予采用,因为 Models 页已为这一状态渲染 DeepSeek 设置卡片。复制其中的 secret 草稿、写入错误处理和已配置状态收敛会增加第二个安全敏感的 UI,却不会带来新的用户能力。 **在首次使用引导中单独实现 API key 表单**:不予采用,因为这会复制 Models 编辑器的 secret 草稿、校验、错误和已配置状态收敛。弹窗改为以受限模式渲染既有 `ProviderEditor`。
**把 API key 写入提供方设置**:不予采用,因为字面量 secret 会进入设置变更路径,而整个分节替换无法安全重建脱敏值。凭据存储已经是产品 seam,并能立即发出失效事件。 **把 API key 写入提供方设置**:不予采用,因为字面量 secret 会进入设置变更路径,而整个分节替换无法安全重建脱敏值。凭据存储已经是产品 seam,并能立即发出失效事件。
@@ -30,4 +30,4 @@ Status: implemented
## 后果 ## 后果
有序流程从产品声明页开始,无需重启即可引导用户前往随产品提供的适配器已有的编辑器:无密钥浏览器测试在隔离的 harness 家目录下启动真实 Web 组合,确认声明后依照 DeepSeek 页面前往 Models,通过该页面把生成的密钥存入该目录的 `.credentials.yaml`,验证密钥未进入 DOM、ARIA 或浏览器控制台输出,并确认运行中的页面报告已配置。完整的无密钥 Web 回放也固定了同 id 的不可配置回放路由不会阻塞无关流程。纯就绪状态测试与 React 测试固化了受管文件凭据与进程环境凭据、提供方与能力缺失、导航、取消、外部失效和协调器移交。该流程直接继承配置平面已记录的基础限制,不会另加局部的机密存储、脱敏或设置替换变通方案。 有序流程从产品声明页开始,无需重启即可进入行内密钥表单:无密钥浏览器测试在隔离的 harness 家目录下启动真实 Web 组合,确认声明后从共用弹窗把生成的密钥存入该目录的 `.credentials.yaml`,验证密钥未进入 DOM、ARIA 或浏览器控制台输出,并确认普通 Models 页面报告已配置。完整的无密钥 Web 回放也固定了同 id 的不可配置回放路由不会阻塞无关流程。纯就绪状态测试与 React 测试固化了受管文件凭据与进程环境凭据、提供方与能力缺失、取消、外部失效和协调器移交。该流程直接继承配置平面已记录的基础限制,不会另加局部的机密存储、脱敏或设置替换变通方案。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-07-30-versioned-gui-welcome-onboarding.md
2026-07-30-versioned-gui-welcome-onboarding.md: 7a8c5c5b9ba1ec62ad8dd0f5bdf67c516ba0ddec 2026-07-30-versioned-gui-welcome-onboarding.md: 9c8684c0510c50c28e8ece53fd794844c771e496
2026-07-30-versioned-gui-welcome-onboarding.zh.md: 646080b893bb6264a3a92f49400cb9207e29755b 2026-07-30-versioned-gui-welcome-onboarding.zh.md: 52c35bf46e40afca1248bcbb94f38d7154f138a4

View File

@@ -10,13 +10,13 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Decision ## Decision
**The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. `ui-settings-models` registers the conditional DeepSeek readiness and credential-routing step at order `0`, the only shipped occupant since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). **The Settings shell coordinates ordered steps.** `settings.onboarding` remains a root-scoped list, but `ui-settings` projects its entry ids and order into one coordinator and mounts only the first incomplete step. The active registrant receives `complete()` and `openSection(id)`; no later step mounts until ownership transfers. `ui-settings-models` now registers the restored welcome notice at order `-100` and the conditional DeepSeek credential step at order `0`; their current shared presentation is owned by the [shared-modal onboarding decision](2026-08-13-shared-modal-product-onboarding.md).
**The product welcome step is removed.** The versioned notice, its copy owner, and its acknowledgement store shipped from this decision until the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md), which owns the removal rationale; `ui-settings-general` seats no onboarding step. **The product welcome step is versioned and feature-owned.** The notice was historically removed by the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md) and is now restored in `ui-settings-models` with new testing-stage copy. `ui-settings-general` still seats no onboarding step; the plugin that owns both current steps also owns the copy, store, and shared modal.
**The durable `ui-onboarding` section outlives the notice.** The Host half registers it in the user-settings seam under the active `$DSH_HOME/settings.yaml`; its `welcomeNoticeVersion` field keeps stored acknowledgements valid and has no reader. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations. **The durable `ui-onboarding` section owns acknowledgement.** The Host half registers it in the user-settings seam under the active `$DSH_HOME/settings.yaml`; the current welcome store reads and writes `welcomeNoticeVersion` through the existing public settings API. The connection plugin publishes whether the current page uses a loopback authority as `ctx.connection.isLoopback`; hostname classification remains internal to the connection package, and other client plugins consume the service state instead of importing its implementation. The API proxy exposes this one product namespace through a closed allowlist beside configurable-provider namespaces, without treating its changes as model-catalog invalidations.
**Onboarding temporarily owns the viewport as one continuous stage.** A solid product surface replaces the complete application view through a body-level portal and marks the underlying app root inert; the exact required mask remains mounted behind that surface with `position:absolute`, zero left/right/bottom offsets, `top:80px`, `rgba(0, 0, 0, 0.24)`, and `backdrop-filter: blur(2px)`. Onboarding steps render as successive pages in this stage instead of independent modals, reusing the Web UI's black `BrandWordmark`; the conditional credential setup is the only shipped page. **Visible onboarding uses one shared modal contract.** Both current steps render through the same body-portaled `OnboardingModal`, and the underlying app root stays inert only while a dialog is visible. The shell renders no wrapper while a step loads its private facts. Explicit actions transfer coordinator ownership; Escape and mask clicks do not acknowledge or skip a step.
## Alternatives considered ## Alternatives considered
@@ -30,4 +30,4 @@ The GUI's credential onboarding begins with a DeepSeek-specific readiness check,
## Consequences ## Consequences
A fresh profile proceeds directly to provider-specific onboarding: the conditional DeepSeek step mounts when its credential is missing, and an already configured credential shows no onboarding page at all. Focused store and React tests pin coordinator ordering, conditional DeepSeek transfer, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies the exact mask geometry and computed styles while the credential step owns the viewport, continues into missing-credential setup, and checks the browser console. A fresh profile sees the current testing-stage notice, then the conditional DeepSeek key dialog when no provider is usable. Focused store and React tests pin exact-version acknowledgement, coordinator ordering, conditional transfer, shared modal behavior, and HMR cleanup. The real Chromium scenario boots the shipped Web composition with an isolated harness home, verifies both dialogs, writes the key through the existing credential boundary, and checks that no secret reaches the DOM, ARIA, or browser console.

View File

@@ -10,13 +10,13 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 决策 ## 决策
**设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。`ui-settings-models` 注册顺序为 `0` 的 DeepSeek 条件式就绪状态与凭据跳转步骤,自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,它是当前唯一的注册方。 **设置外壳协调有序步骤。** `settings.onboarding` 仍是根作用域 list,但 `ui-settings` 会把其中各条目的 id 和顺序投影到一个协调器中,并且只挂载第一个未完成的步骤。当前注册方会收到 `complete()` 和 `openSection(id)`;所有权转移前,不会挂载后续步骤。`ui-settings-models` 现在以顺序 `-100` 注册恢复后的欢迎声明,以顺序 `0` 注册 DeepSeek 条件式凭据步骤;两者当前的共用展示由[共用弹窗引导决策](2026-08-13-shared-modal-product-onboarding.md)持有。
**产品欢迎步骤已移除。** 版本化通知、其文案所有者文件和确认 store 自本决策起随产品发布,直至[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md);移除理由由该 note 持有。`ui-settings-general` 不再注册任何引导步骤。 **产品欢迎步骤按版本管理并归功能插件所有。** 该声明曾由[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)历史决策移除,现在以新的测试阶段文案恢复在 `ui-settings-models` 中。`ui-settings-general` 仍不注册任何引导步骤;持有当前两个步骤的插件也持有文案、store 和共用弹窗。
**持久化的 `ui-onboarding` 分节在通知移除后继续存在。** 宿主端在 user-settings seam 中注册它,存入当前 `$DSH_HOME/settings.yaml`;其中的 `welcomeNoticeVersion` 字段让已存储的确认记录保持有效,没有读取方。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。API Proxy 在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。 **持久化的 `ui-onboarding` 分节持有确认状态。** 宿主端在 user-settings seam 中注册它,存入当前 `$DSH_HOME/settings.yaml`;当前欢迎 store 通过既有公开 settings API 读写其中的 `welcomeNoticeVersion`。connection 插件通过 `ctx.connection.isLoopback` 统一发布当前页面是否使用 loopback authority;hostname 判定留在 connection 包内,其他客户端插件只消费服务状态,而不导入其实现。API Proxy 在可配置提供方 namespace 之外,通过封闭的允许列表暴露这一个产品 namespace,同时不会把它的变更视为模型目录失效事件。
**引导流程会暂时接管视口,形成一个连续阶段。** 纯色产品界面通过挂载到 `body` 的 portal 取代完整的应用视图,并将底层应用根节点标记为 inert;严格符合要求的遮罩仍挂载在该界面后方,并保留 `position:absolute`、left/right/bottom 偏移量为零、`top:80px`、`rgba(0, 0, 0, 0.24)` 和 `backdrop-filter: blur(2px)`。引导步骤在这一阶段中依次呈现,而不是各自作为独立的模态窗口,并复用 Web UI 的黑色 `BrandWordmark`;按条件显示的凭据设置页是当前唯一的页面。 **可见引导使用同一个弹窗契约。** 当前两个步骤都通过 body portal 的同一个 `OnboardingModal` 渲染,且只在弹窗可见期间把下层应用根节点设为 inert。步骤加载私有事实时,外壳不渲染任何包装。明确操作会移交协调器所有权;Escape 和点击遮罩都不会确认或跳过步骤。
## 曾考虑的替代方案 ## 曾考虑的替代方案
@@ -30,4 +30,4 @@ GUI 的凭据引导从 DeepSeek 专用的就绪状态检查开始,但内部测
## 后果 ## 后果
全新 profile 直接进入提供方专用引导:DeepSeek 步骤仅在其凭据缺失时挂载,凭据已配置时不会出现任何引导页面。针对性的 store 与 React 测试固化了协调器顺序、按条件移交 DeepSeek 步骤和 HMR(热模块替换)清理行为。真实 Chromium 场景会使用隔离的 harness 家目录启动随产品提供的 Web 组合,在凭据步骤占据视口时验证遮罩的精确几何尺寸和计算样式,继续进入凭据缺失设置流程,并检查浏览器控制台。 全新 profile 会先看到当前测试阶段声明;当没有任何可用提供方时,再看到条件式 DeepSeek 密钥弹窗。定向 store 与 React 测试固定精确版本确认、协调器顺序、条件式移交、共用弹窗行为与 HMR 清理。真实 Chromium 场景会在隔离的 harness 家目录下启动已发布 Web 组合,验证两个弹窗,通过既有凭据边界写入密钥,并检查 secret 未进入 DOM、ARIA 或浏览器控制台。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-10-telemetry-default-off.md
2026-08-10-telemetry-default-off.md: b70fdbc64074936ae910cba305a59111d6f9b572 2026-08-10-telemetry-default-off.md: 3f56817c9c23ec55f2173b66fa915ab05646b2a7
2026-08-10-telemetry-default-off.zh.md: abb5cafa9f877659dca1d53c0be9df9414799cf9 2026-08-10-telemetry-default-off.zh.md: aa7b640f292713aa4e598f5ea1194f806dd6ae0d

View File

@@ -14,7 +14,7 @@ Both feeds use `DSH_TELEMETRY_MODE` as their positive consent setting. Unset and
The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md). The dsh-sdk launcher reads the same variable without parsing `cordis.yml` or booting Cordis. `FULL` permits reporting; `FEEDBACK_ONLY`, `DISABLED`, unset, and empty values deny it. Consent is frozen from the launching environment before the command runs, because `dsh-sdk start` loads a project `.env` and project code can mutate `process.env`: resolving afterwards would let a project grant reporting of its own configuration, which the [configuration source ownership decision](../architecture/2026-08-04-configuration-source-ownership.md) denies for the whole `DSH_*` namespace. An unsupported mode denies rather than throwing at that boundary, since telemetry may never change a command's result. This rule superseded the default-on launcher consent before the launcher and its proposal were deleted by the [SDK project toolchain removal](../simplification/2026-08-11-remove-sdk-project-toolchain.md).
The [CLI reference README](../../../../apps/cli/reference/README.md) documents the deployment stance: Session Log upload is off by default, `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` are the two opt-in choices, and explicitly enabled exports can contain complete session content. The product itself presents no prompt about enabling telemetry since the [first-run beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md). The [CLI reference README](../../../../apps/cli/reference/README.md) documents the deployment stance: Session Log upload is off by default, `DSH_TELEMETRY_MODE=FEEDBACK_ONLY` and `DSH_TELEMETRY_MODE=FULL` are the two opt-in choices, and explicitly enabled exports can contain complete session content. The restored [testing-stage onboarding notice](2026-08-13-shared-modal-product-onboarding.md) contains no telemetry copy, so the product still presents no prompt about enabling upload.
## Alternatives considered ## Alternatives considered

View File

@@ -14,7 +14,7 @@ DeepSeek Harness 有两路出站遥测数据流。在内测阶段,共享基础
dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。 dsh-sdk 启动器读取同一变量,不解析 `cordis.yml`,也不启动 Cordis。`FULL` 允许上报;`FEEDBACK_ONLY`、`DISABLED`、未设置和空值都会拒绝。授权在命令执行前从启动环境冻结:`dsh-sdk start` 会加载项目 `.env`,项目代码也能修改 `process.env`,若在执行后解析,项目便能自行授权上报其自身配置,而[配置来源所有权决策](../architecture/2026-08-04-configuration-source-ownership.md)对整个 `DSH_*` 命名空间禁止这种行为。在该边界上,不受支持的模式按拒绝处理而非抛出,因为遥测不得改变命令结果。此规则在启动器及其提案被[SDK 项目工具链移除决策](../simplification/2026-08-11-remove-sdk-project-toolchain.md)删除之前,仅取代了启动器默认允许上报的规则。
[CLI reference README](../../../../apps/cli/reference/README.md) 记录了这一部署口径:会话日志上传默认关闭,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 是两种显式启用选项,显式开启后的导出可能包含完整会话内容。自[移除首次启动内测声明](../simplification/2026-08-13-remove-first-run-beta-notice.md)起,产品本身不提供任何关于开启遥测的提示。 [CLI reference README](../../../../apps/cli/reference/README.md) 记录了这一部署口径:会话日志上传默认关闭,`DSH_TELEMETRY_MODE=FEEDBACK_ONLY` 和 `DSH_TELEMETRY_MODE=FULL` 是两种显式启用选项,显式开启后的导出可能包含完整会话内容。恢复后的[测试阶段引导声明](2026-08-13-shared-modal-product-onboarding.md)不包含遥测文案,因此产品仍不提供任何关于开启上传的提示。
## 考虑过的替代方案 ## 考虑过的替代方案

View File

@@ -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-13-shared-modal-product-onboarding.md
2026-08-13-shared-modal-product-onboarding.md: 771b6675ea170c5001fca7c880f6c246e19fe7f1
2026-08-13-shared-modal-product-onboarding.zh.md: ebbcb73344b531e90a6d27641f25fa9961d572ac

View File

@@ -0,0 +1,33 @@
# Agent Note: Shared-modal product onboarding
Status: implemented
English | [中文](2026-08-13-shared-modal-product-onboarding.zh.md)
## Problem
First-run onboarding mixed two interaction models: a viewport takeover for product context and a credential prompt that redirected users into Settings before they could enter a key. That made a short, ordered flow feel like two unrelated surfaces and left onboarding UI ownership split across packages. The product still needs a versioned testing-stage notice before provider setup, but restoring it must not add a second independent overlay or change the Host settings and credential boundaries.
## Decision
**One existing client Cordis plugin owns both shipped steps.** `ui-settings-models` registers `welcome-notice` at order `-100` and `deepseek-official` at order `0` in `settings.onboarding`. The shell continues to mount only the first incomplete entry, so the dialogs cannot stack. No additional client package or plugin row is introduced.
**Both steps share one modal component.** `OnboardingModal` wraps the existing ui-primitives `Modal`, supplies the common title and content geometry, and owns `#root` inert for exactly the visible lifetime. Escape and mask clicks do not silently complete mandatory onboarding; each step exposes only its explicit actions. A step still loading private facts returns `null`, so it paints and blocks nothing.
**The welcome notice reuses the existing durable field.** Its exact copy and version live in `onboarding-copy.ts`. Loopback clients compare and write `ui-onboarding.welcomeNoticeVersion` through the existing settings API, and only Continue acknowledges the current version. Remote clients retain the existing process-local fallback because the settings namespace is loopback-only. No Host schema, API-proxy allowlist, or persistence implementation changes.
**The credential dialog reuses the existing editor and write boundary.** The Models join still decides whether any provider is usable. When the official DeepSeek reference is writable and missing, `ProviderEditor` renders in credential-only mode inside the shared modal. It validates the key and calls the existing `credentials.set`; it does not mutate provider settings. Save and continue waits for the write and refreshed readiness, while Configure later completes only the current coordinator pass.
## Alternatives considered
**Separate client plugins for the notice and credential steps.** Rejected because the product asks for one client Cordis plugin and the two surfaces share copy, ordering, modal chrome, and invalidation ownership.
**Move acknowledgement or credential logic into a new Host API.** Rejected because both backend contracts already express the required state and writes. A new endpoint would widen scope without changing user capability.
**Keep the credential step as navigation into Models.** Rejected because the key is the only required first-run field, and the existing editor can expose that write safely without sending the user through a second dialog.
**Keep the former full-viewport stage.** Rejected because the requested onboarding is a pair of dialogs over the current app, and the common ui-primitives modal already provides the appropriate portal, mask, and accessibility contract.
## Consequences
A fresh loopback profile sees the specified internal-testing notice, then an inline DeepSeek key dialog only when no provider is usable. Acknowledgement remains versioned in `settings.yaml`, secrets remain write-only in `.credentials.yaml`, and already-ready or unsupported deployments render no onboarding chrome while readiness loads. The Models package now owns product-onboarding presentation as well as provider configuration; its README and browser coverage make that broader responsibility explicit. This decision restores a concise testing-stage notice after the historical [full-viewport beta notice removal](../simplification/2026-08-13-remove-first-run-beta-notice.md) without restoring that notice's telemetry copy or takeover layout.

View File

@@ -0,0 +1,33 @@
# Agent Note: 共用弹窗的产品引导
Status: implemented
[English](2026-08-13-shared-modal-product-onboarding.md) | 中文
## 问题
首次使用引导混用了两种交互:产品背景说明占满整个视口,凭据提示则先把用户带进「设置」,之后才能输入密钥。一个很短的有序流程因此像两个互不相关的界面,引导 UI 的归属也分散在多个包中。产品仍需要在提供方配置之前显示版本化的测试阶段声明,但恢复它不能增加第二个独立浮层,也不能改变 Host 的设置与凭据边界。
## 决策
**由同一个既有 client Cordis 插件持有两个已发布步骤。** `ui-settings-models` 在 `settings.onboarding` 中以顺序 `-100` 注册 `welcome-notice`,以顺序 `0` 注册 `deepseek-official`。外壳仍然只挂载第一个未完成条目,因此两个弹窗不会堆叠。不新增 client 包或插件配置行。
**两个步骤共用同一个弹窗组件。** `OnboardingModal` 包装既有 ui-primitives `Modal`,提供统一的标题和内容布局,并只在可见期间持有 `#root` 的 inert 状态。Escape 和遮罩点击不会静默完成强制引导;每个步骤只暴露自己的明确操作。步骤仍在加载私有事实时返回 `null`,因此不会绘制或阻塞界面。
**欢迎声明复用既有持久化字段。** 完整文案与版本由 `onboarding-copy.ts` 持有。回环客户端通过既有 settings API 比较和写入 `ui-onboarding.welcomeNoticeVersion`,且只有点击「继续」才确认当前版本。远程客户端继续使用既有的进程内回退,因为该 settings namespace 仅限回环访问。不改变 Host schema、API Proxy 允许列表或持久化实现。
**凭据弹窗复用既有编辑器与写入边界。** Models 联接仍负责判断是否已有任意可用提供方。当 DeepSeek 官方引用可写但缺失时,`ProviderEditor` 以仅凭据模式渲染在共用弹窗中。它校验密钥并调用既有 `credentials.set`,不会修改提供方设置。「保存并继续」会等待写入与就绪状态刷新;「稍后配置」只完成协调器当前这一轮。
## 曾考虑的替代方案
**让声明与凭据步骤分别成为 client 插件。** 不采用:产品要求只使用一个 client Cordis 插件,且两个界面共享文案、顺序、弹窗框架与失效刷新归属。
**把确认或凭据逻辑移入新的 Host API。** 不采用:两个既有后端契约已经能表达所需状态与写入;新增 endpoint 只会扩大范围,不会增加用户能力。
**继续从凭据步骤跳转到 Models。** 不采用:首次使用唯一必填的是密钥,既有编辑器可以安全暴露这项写入,无需再把用户送进第二个对话框。
**保留此前占满视口的展示层。** 不采用:本次需要的是叠加在当前应用上的两个弹窗,既有 ui-primitives modal 已提供合适的 portal、遮罩与无障碍契约。
## 后果
新的回环 profile 会先看到指定的内测声明;仅当没有任何可用提供方时,之后才会出现行内 DeepSeek 密钥弹窗。确认仍按版本写入 `settings.yaml`,secret 仍以只写方式存入 `.credentials.yaml`,已就绪或无法修复的部署在加载判定期间不会渲染任何引导框架。Models 包现在同时持有产品引导展示与提供方配置;README 和浏览器覆盖明确记录了这项扩展后的职责。本决策在历史上的[全屏内测声明移除](../simplification/2026-08-13-remove-first-run-beta-notice.md)之后恢复简洁的测试阶段声明,但不会恢复那份声明中的遥测文案或接管式布局。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-22-evidence-based-larger-hosted-runners.md
2026-07-22-evidence-based-larger-hosted-runners.md: 84c951809891b4936549a2f429dc7efc99833c1b 2026-07-22-evidence-based-larger-hosted-runners.md: e0d919851d99eac6539a25c63c9baeb49f76335f
2026-07-22-evidence-based-larger-hosted-runners.zh.md: bb5b585da26ffd15b5a1096cd40293f64fb69c79 2026-07-22-evidence-based-larger-hosted-runners.zh.md: 673bd7643506f022b640d14918dd7c883fb60e36

View File

@@ -52,7 +52,7 @@ The process-bound coverage project contains exactly five suite files. Thirty-two
Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the enterprise required path plus standard-hosted compatibility jobs, while other larger-runner sizes run only by manual dispatch. Complete serial Linux, macOS, and Windows references run only when `master` moves. Pull requests use the enterprise required path plus standard-hosted compatibility jobs, while other larger-runner sizes run only by manual dispatch.
An additional serial Linux reference runs on the in-house self-hosted pool (`vm-backup` label: a 64-core VM with six always-on systemd-managed runner instances) on every `master` push. It is a hot-standby drill, not a required check: each run re-proves that the persistent VM can execute the complete unsharded aggregate. The actual switch is pre-wired: the three required Linux jobs resolve their pool through the writer-manageable `DSH_CI_FAILOVER` repository variable, so an outage response is setting one variable and re-running — no merge, which would be deadlocked behind the failing checks themselves ([runbook](2026-07-26-ci-failover-runbook.md)). The standby lane is push-triggered, so it always executes the base branch's workflow definition. Under failover, however, `pull_request` jobs do reach these runners with the PR merge ref's own workflow definition — the trust boundary is repository membership (the repository is private with forking disabled, and the selectors exclude Dependabot), as the [failover runbook](2026-07-26-ci-failover-runbook.md) records. An additional serial Linux reference runs on the in-house self-hosted pool (`vm-backup` label: a 64-core VM with six always-on systemd-managed runner instances) on every `master` push. It is a hot-standby drill, not a required check: each run re-proves that the persistent VM can execute the complete unsharded aggregate. The actual switch is pre-wired: the three required Linux jobs resolve their pool through the writer-manageable `DSH_CI_FAILOVER_LINUX` repository variable, so an outage response is setting one variable and re-running — no merge, which would be deadlocked behind the failing checks themselves ([runbook](2026-07-26-ci-failover-runbook.md)). The standby lane is push-triggered, so it always executes the base branch's workflow definition. Under failover, however, `pull_request` jobs do reach these runners with the PR merge ref's own workflow definition — the trust boundary is repository membership (the repository is private with forking disabled, and the selectors exclude Dependabot), as the [failover runbook](2026-07-26-ci-failover-runbook.md) records.
## Alternatives considered ## Alternatives considered

View File

@@ -52,7 +52,7 @@ Windows 仓库工作在超过 16 核后收益很小,但 32 核池可以让完
只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用企业级运行器必需路径和标准托管兼容性作业,其他大型运行器规格仅通过手动触发运行。 只有在 `master` 移动时,才运行完整的 Linux、macOS 和 Windows 串行参考。拉取请求使用企业级运行器必需路径和标准托管兼容性作业,其他大型运行器规格仅通过手动触发运行。
另有一条串行 Linux 参考在每次 `master` 推送时运行于公司自有的自托管池(`vm-backup` 标签:一台 64 核虚拟机,运行 6 个常驻的 systemd 管理运行器实例)。它是热备演练而非必需检查:每次运行都重新证明这台持久化虚拟机能够执行完整的未分片聚合流程。实际切换机制已预先布线:三个必需 Linux 作业通过写入权限持有者可管理的仓库变量 `DSH_CI_FAILOVER` 解析运行器池,因此故障响应就是设置一个变量并重跑——无需合并(合并本身会被正在失败的检查阻塞,形成死锁)([切换手册](2026-07-26-ci-failover-runbook.md))。该热备通道由 push 触发,执行的始终是基础分支自身的工作流定义。但需要注意:故障切换期间,`pull_request` 作业确实会带着 PR merge 引用自带的工作流定义到达这些运行器——信任边界是仓库成员资格(仓库为私有且禁用 fork,选择器排除 Dependabot),详见[故障切换手册](2026-07-26-ci-failover-runbook.md)的记录。 另有一条串行 Linux 参考在每次 `master` 推送时运行于公司自有的自托管池(`vm-backup` 标签:一台 64 核虚拟机,运行 6 个常驻的 systemd 管理运行器实例)。它是热备演练而非必需检查:每次运行都重新证明这台持久化虚拟机能够执行完整的未分片聚合流程。实际切换机制已预先布线:三个必需 Linux 作业通过写入权限持有者可管理的仓库变量 `DSH_CI_FAILOVER_LINUX` 解析运行器池,因此故障响应就是设置一个变量并重跑——无需合并(合并本身会被正在失败的检查阻塞,形成死锁)([切换手册](2026-07-26-ci-failover-runbook.md))。该热备通道由 push 触发,执行的始终是基础分支自身的工作流定义。但需要注意:故障切换期间,`pull_request` 作业确实会带着 PR merge 引用自带的工作流定义到达这些运行器——信任边界是仓库成员资格(仓库为私有且禁用 fork,选择器排除 Dependabot),详见[故障切换手册](2026-07-26-ci-failover-runbook.md)的记录。
## 曾考虑的替代方案 ## 曾考虑的替代方案

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md
2026-07-26-ci-failover-runbook.md: 90ef2905bec86911697e552c0c1eac96ea0d3a18 2026-07-26-ci-failover-runbook.md: b4522e623ffb76f3fd33d242b21c2d1d9ff2eadf
2026-07-26-ci-failover-runbook.zh.md: 5c8fd0e90ae31e3622ae6b49217591a54b84c351 2026-07-26-ci-failover-runbook.zh.md: 58ba7ffee013d38f06afe097362f5c23f04b8121

View File

@@ -6,11 +6,11 @@ English | [中文](2026-07-26-ci-failover-runbook.zh.md)
## Problem ## Problem
The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`; the independent native Windows job (`windows node 24 / native complete`) runs on the hosted `dsh-windows-2025-16core` larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: this switch recovers an enterprise Linux-pool outage AND a hosted Windows-pool outage.** The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything. The three required Linux worker jobs in [CI](../../../../.github/workflows/ci.yml) (`node 24 / static`, `node 24 / coverage`, `node 24 / snapshots and artifacts`) run on the hosted enterprise 32-core pools; the required verdict job that aggregates them (`all checks passed`) runs on standard `ubuntu-latest`; the independent native Windows job (`windows node 24 / native complete`) runs on the hosted `dsh-windows-2025-16core` larger runner. When the enterprise pools degrade — jobs queue indefinitely or the enterprise labels vanish — every open pull request becomes unmergeable, and the ordinary recovery of merging a fix is itself deadlocked behind the very required checks that cannot run. **Scope: two independent switches, one per platform.** `DSH_CI_FAILOVER_LINUX` recovers an enterprise Linux-pool outage (the three required Linux workers plus the `all checks passed` verdict); `DSH_CI_FAILOVER_WINDOWS` recovers a hosted Windows-pool outage (the native Windows job). A Linux-pool outage need not retarget the native Windows job and vice versa. The verdict's other required dependencies (`node-compat`, `python-sdk`, `windows`) stay on standard hosted runners by design (the portable boundary); in a broader GitHub-hosted capacity failure that also takes out the standard pools, those dependencies still block `all checks passed`. An outage therefore needs a switch any responder with repository write access can throw without merging anything.
## Decision ## Decision
Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through the `DSH_CI_FAILOVER` repository variable. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, all five retarget onto the in-house self-hosted pools: the Linux jobs and verdict onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; the native Windows job onto the `dsh-win-ci` pool. The switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push. Each of the three required Linux worker jobs, the independent native Windows job, and the `all checks passed` verdict job — which would otherwise stay queued on the failed pool even after every worker passed — resolves its runner pool through a repository variable, and the switch is split by platform so an outage on one platform does not retarget the other. The three Linux workers and the `all checks passed` verdict (whose `needs` are the required Linux workers and which runs on the `vm-backup` pool) resolve through `DSH_CI_FAILOVER_LINUX`; the native Windows job resolves through `DSH_CI_FAILOVER_WINDOWS`. Unset (normal), they run on the hosted enterprise pools. Set to `selfhosted` by any repository writer, the corresponding jobs retarget onto the in-house self-hosted pool: under `DSH_CI_FAILOVER_LINUX`, the Linux jobs and verdict move onto the `vm-backup` pool, coverage and snapshot concurrency drop to shared-VM bounds, and the hosted-path pnpm cache restores are skipped; under `DSH_CI_FAILOVER_WINDOWS`, the native Windows job moves onto the `dsh-win-ci` pool. Each switch is writer-manageable repository state, not a merge, so it works while every check is red. The in-house pools' readiness is continuously re-proven by the `serial / linux (self-hosted standby)` and `serial / windows (self-hosted standby)` lanes, which run the complete unsharded aggregates on every master push.
`ci.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check. `ci.yml` exempts exactly one event from `cancel-in-progress` (`${{ github.event_name != 'push' }}`), so one master push does not cancel the drill still running from the previous one. Each drill runs its complete unsharded aggregate with one gate worker, which takes longer than the interval between master merges; under unconditional cancellation a drill is superseded before reaching a verdict and the lane yields no readiness evidence for a responder to check.
@@ -28,13 +28,15 @@ The decision belongs at workflow level because cancellation applies to the whole
### Switch (any repository writer, ~1 minute, no merge) ### Switch (any repository writer, ~1 minute, no merge)
1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER`, value `selfhosted`. The two switches are independent: flip only the one whose platform is degraded.
1. Repository **Settings → Secrets and variables → Actions → Variables → New repository variable**: name `DSH_CI_FAILOVER_LINUX` (Linux pool outage) or `DSH_CI_FAILOVER_WINDOWS` (Windows pool outage), value `selfhosted`.
2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued. 2. Retrigger the required jobs so they re-resolve their pool. Jobs already **queued** for the hosted labels do not retarget and cannot be re-run in place, so for the documented indefinite-queue outage, cancel the stuck run and re-run all jobs, or push a new commit; "Re-run failed jobs" only helps once a job has actually failed rather than queued.
3. That is the entire switch. Under failover the workflow also, automatically: drops `DSH_COVERAGE_MAX_WORKERS` to 8 and `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 (sized for six always-on instances: worst case 6 × 8 = 48 coverage workers on the 64-core VM) (shared-VM contention bounds), and skips the hosted-path pnpm cache restores (the VM's persistent store serves warm installs). 3. That is the entire switch. Under Linux failover the workflow also, automatically: drops `DSH_COVERAGE_MAX_WORKERS` to 8 and `DSH_SNAPSHOT_MAX_CONCURRENCY` to 12 (sized for six always-on instances: worst case 6 × 8 = 48 coverage workers on the 64-core VM) (shared-VM contention bounds), and skips the hosted-path pnpm cache restores (the VM's persistent store serves warm installs). The Windows switch has no such concurrency or cache branches; it only retargets the native Windows job's pool.
#**Dependabot exception.** All five selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VMs. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers. #**Dependabot exception.** Both switches' selectors deliberately exclude `dependabot[bot]`: under failover, Dependabot PRs stay queued for the hosted pool rather than executing dependency-supplied code on the persistent VMs. A Dependabot PR that remains queued during an outage is expected behavior, not a failed switch; it completes when the hosted pool recovers.
**Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so the switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner groups admit all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VMs by pushing a branch workflow. The boundary against untrusted code is repository membership; the variable only routes work for members. **Who can flip the variable.** GitHub's API lets any collaborator with write access manage repository variables, so each switch is writer-level, not strictly admin-only. In this repository's trust model that is not an escalation: the runner groups admit all workflows of this private, fork-disabled repository (a deliberate trade to make PR-ref failover possible at all), so any writer could already reach the VMs by pushing a branch workflow. The boundary against untrusted code is repository membership; the variables only route work for members.
## Capacity during failover ## Capacity during failover
@@ -43,18 +45,18 @@ Six always-on instances absorb normal PR traffic (the pool's steady-state load i
### Switch back ### Switch back
Delete the `DSH_CI_FAILOVER` variable (or set it to anything other than `selfhosted`). New runs resolve back to the hosted enterprise pools. Remove any extra instances that were registered during the incident. Delete the `DSH_CI_FAILOVER_LINUX` or `DSH_CI_FAILOVER_WINDOWS` variable (or set it to anything other than `selfhosted`). New runs resolve back to the hosted enterprise pools. Remove any extra instances that were registered during the incident.
### Trust boundary ### Trust boundary
The variable is writer-manageable repository state; a pull request event itself can neither set it nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Note on runner-group policy: pinning the runner group to the master-ref workflow is **incompatible** with this failover — the four failover jobs are `pull_request` runs evaluated from PR merge refs, and a master-pinned group leaves them queued (observed live on 2026-07-27; the group was widened to all workflows of this repository to unblock the switch). A stricter runner-side policy therefore costs PR failover; the shipped posture accepts repository-scoped, all-workflow group access. The variables are writer-manageable repository state; a pull request event itself can neither set them nor read a different value into effect, and the selector expressions live in workflow definitions. Note that under failover, `pull_request` runs execute the PR merge ref's own workflow definition — the boundary against untrusted code is repository membership (private, forking disabled, Dependabot excluded by the selectors), not the variable. Note on runner-group policy: pinning the runner group to the master-ref workflow is **incompatible** with this failover — the five failover jobs are `pull_request` runs evaluated from PR merge refs, and a master-pinned group leaves them queued (observed live on 2026-07-27; the group was widened to all workflows of this repository to unblock the switch). A stricter runner-side policy therefore costs PR failover; the shipped posture accepts repository-scoped, all-workflow group access.
## Alternatives considered ## Alternatives considered
**Merge a workflow change to switch pools.** Rejected because the outage that motivates the switch is exactly the state in which no PR can merge: the required checks are the ones failing. A repository variable is writer-manageable state that takes effect on re-run without a merge. **Merge a workflow change to switch pools.** Rejected because the outage that motivates the switch is exactly the state in which no PR can merge: the required checks are the ones failing. A repository variable is writer-manageable state that takes effect on re-run without a merge.
**Keep the self-hosted pool always in the required path.** Rejected because it trades hosted-pool availability for the in-house VM's, moving a single point of failure rather than adding a fallback. The variable keeps the hosted pools primary and the self-hosted pool a proven, one-action standby. **Keep the self-hosted pool always in the required path.** Rejected because it trades hosted-pool availability for the in-house VM's, moving a single point of failure rather than adding a fallback. The variables keep the hosted pools primary and the self-hosted pools proven, one-action standbys; splitting them by platform means an outage on one platform does not retarget the other.
## Consequences ## Consequences
Recovering from a hosted-pool outage is a single variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology to keep working: the standby lane exercises it on every master push so the failover target never goes stale, and the concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg that must stay in step with the hosted leg. Recovering from a hosted-pool outage is flipping the affected platform's variable (any writer) plus a re-run, with no merge on the critical path. The cost is a second runner topology per platform to keep working: the standby lanes exercise them on every master push so the failover targets never go stale, and the concurrency and cache-restore branches in `ci.yml` carry a `selfhosted` leg (Linux only) that must stay in step with the hosted leg. Splitting the switch by platform adds one more variable to manage but bounds the blast radius of each switch to the jobs of a single platform.

View File

@@ -6,11 +6,11 @@ Status: implemented
## 问题 ## 问题
[CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上;独立的原生 Windows 作业(`windows node 24 / native complete`)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:本切换恢复的是企业级 Linux 池故障与托管 Windows 池故障。**判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。 [CI](../../../../.github/workflows/ci.yml) 中三个必需的 Linux 工作作业(`node 24 / static`、`node 24 / coverage`、`node 24 / snapshots and artifacts`)运行在托管的企业级 32 核池上;聚合它们的必需判定作业(`all checks passed`)运行在标准 `ubuntu-latest` 上;独立的原生 Windows 作业(`windows node 24 / native complete`)运行在托管的 `dsh-windows-2025-16core` 大型运行器上。当企业池发生故障——作业无限排队或企业标签消失——所有开启的拉取请求都无法合并,而"合并一个修复"这一常规恢复手段本身正被那些无法运行的必需检查死锁。**适用范围:两个独立开关,每个平台一个。**`DSH_CI_FAILOVER_LINUX` 恢复企业级 Linux 池故障(三个必需的 Linux 工作作业加 `all checks passed` 判定作业);`DSH_CI_FAILOVER_WINDOWS` 恢复托管 Windows 池故障(原生 Windows 作业)。Linux 池故障无需重定向原生 Windows 作业,反之亦然。判定作业的其余必需依赖(`node-compat`、`python-sdk`、`windows`)按设计留在标准托管运行器上(可移植边界);若更大范围的 GitHub 托管容量故障连标准池一并击倒,这些依赖仍会阻塞 `all checks passed`。因此故障需要一个任何具备仓库写权限的响应者都能在不合并任何代码的情况下触发的开关。
## 决策 ## 决策
三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量 `DSH_CI_FAILOVER` 解析运行器池。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,五个作业全部切换到公司自有的自托管池:Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;原生 Windows 作业切到 `dsh-win-ci` 池。这个开关是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。 三个必需的 Linux 工作作业、独立的原生 Windows 作业,以及 `all checks passed` 判定作业(若不随切换,即使全部工作作业通过,它仍会滞留在故障池的队列中)——各自通过仓库变量解析运行器池,且开关按平台拆分,使一个平台的故障不会重定向另一个平台。三个 Linux 工作作业与 `all checks passed` 判定作业(其 `needs` 是必需的 Linux 工作作业,且运行在 `vm-backup` 池上)通过 `DSH_CI_FAILOVER_LINUX` 解析;原生 Windows 作业通过 `DSH_CI_FAILOVER_WINDOWS` 解析。变量不存在(正常)时它们运行在托管企业池上;由任何具备写权限的协作者设为 `selfhosted` 时,对应作业切换到公司自有的自托管池:`DSH_CI_FAILOVER_LINUX` 下,Linux 作业与判定作业切到 `vm-backup` 池,覆盖率与快照的并发降到共享虚拟机上限,并跳过托管路径的 pnpm 缓存恢复;`DSH_CI_FAILOVER_WINDOWS` 下,原生 Windows 作业切到 `dsh-win-ci` 池。每个开关都是写者可管理的仓库状态而非一次合并,因此在所有检查都是红色时仍然有效。自有池的就绪状态由 `serial / linux (self-hosted standby)` 与 `serial / windows (self-hosted standby)` 通道持续验证——每次 master 推送都在其上运行完整的未分片聚合流程。
`ci.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。 `ci.yml` 只豁免一个事件不做取消(`${{ github.event_name != 'push' }}`),因此一次 master 推送不会取消上一次推送留下的、仍在运行的演练。每次演练以单门禁工作进程执行完整的未分片聚合流程,耗时长于 master 合并的间隔;在无条件取消下,演练会在得出结论前被后续运行取代,该通道无法产出供响应者查看的就绪证据。
@@ -28,13 +28,15 @@ Status: implemented
### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并) ### 切换步骤(任何具备写权限的协作者,约 1 分钟,无需合并)
1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER`,值 `selfhosted`。 两个开关相互独立:只切换发生故障的那个平台。
1. 仓库 **Settings → Secrets and variables → Actions → Variables → New repository variable**:名称 `DSH_CI_FAILOVER_LINUX`(Linux 池故障)或 `DSH_CI_FAILOVER_WINDOWS`(Windows 池故障),值 `selfhosted`。
2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。 2. 重新触发必需作业,使其重新解析运行器池。已经为托管标签**排队**的作业不会重定向,也无法原地 re-run,因此对于本手册所述的无限排队故障,应取消卡住的运行并 re-run all jobs,或推送一个新提交;“Re-run failed jobs”只有在作业真正失败(而非仍在排队)时才有用。
3. 切换到此完成。故障切换状态下工作流还会自动:把 `DSH_COVERAGE_MAX_WORKERS` 降为 8、`DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12(按 6 个常驻实例定容:最坏情况下,6 × 8 = 48 个覆盖率工作进程运行在 64 核虚拟机上)(共享虚拟机的争抢上限),并跳过托管路径的 pnpm 缓存恢复(虚拟机的持久 store 直接提供热安装)。 3. 切换到此完成。Linux 故障切换状态下工作流还会自动:把 `DSH_COVERAGE_MAX_WORKERS` 降为 8、`DSH_SNAPSHOT_MAX_CONCURRENCY` 降为 12(按 6 个常驻实例定容:最坏情况下,6 × 8 = 48 个覆盖率工作进程运行在 64 核虚拟机上)(共享虚拟机的争抢上限),并跳过托管路径的 pnpm 缓存恢复(虚拟机的持久 store 直接提供热安装)。Windows 开关没有这类并发或缓存分支;它只重定向原生 Windows 作业的运行器池。
#**Dependabot 例外。**五个选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。 #**Dependabot 例外。**两个开关的选择器都刻意排除了 `dependabot[bot]`:故障切换期间,Dependabot 拉取请求继续在托管池排队,而不是把依赖项提供的代码放到持久化虚拟机上执行。故障期间 Dependabot PR 持续排队是预期行为而非切换失败;托管池恢复后它会自行完成。
**谁能扳动这个变量。**GitHub 的 API 允许任何具有写权限的协作者管理仓库变量,因此该开关实际是写者级而非严格的管理员级。在本仓库的信任模型下这并不构成升权:runner group 接纳本私有、禁 fork 仓库的全部工作流(这是让 PR 引用的故障切换得以成立的刻意取舍),因此任何写者本就可以通过推送分支工作流触达这台虚拟机。抵御不可信代码的边界是仓库成员资格;变量只是为成员路由工作。 **谁能扳动这个变量。**GitHub 的 API 允许任何具有写权限的协作者管理仓库变量,因此每个开关实际是写者级而非严格的管理员级。在本仓库的信任模型下这并不构成升权:runner group 接纳本私有、禁 fork 仓库的全部工作流(这是让 PR 引用的故障切换得以成立的刻意取舍),因此任何写者本就可以通过推送分支工作流触达这台虚拟机。抵御不可信代码的边界是仓库成员资格;变量只是为成员路由工作。
## 切换期间的容量 ## 切换期间的容量
@@ -43,18 +45,18 @@ Status: implemented
### 切回 ### 切回
删除 `DSH_CI_FAILOVER` 变量(或改为 `selfhosted` 以外的任何值),新的运行即解析回托管企业池。若故障期间追加注册过实例,将其移除。 删除 `DSH_CI_FAILOVER_LINUX` 或 `DSH_CI_FAILOVER_WINDOWS` 变量(或改为 `selfhosted` 以外的任何值),新的运行即解析回托管企业池。若故障期间追加注册过实例,将其移除。
### 信任边界 ### 信任边界
该变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。关于 runner group 策略的说明:把 runner group 绑定到 master 引用的工作流与本故障切换机制**不兼容**——四个故障切换作业是从 PR merge 引用求值的 `pull_request` 运行,master 绑定的组会让它们持续排队(2026-07-27 实际故障中亲历;当时将组放宽为本仓库全部工作流才疏通了切换)。更严格的运行器侧策略以牺牲 PR 故障切换为代价;当前采用的形态是仓库范围、全工作流的组访问。 这些变量是写者可管理的仓库状态;`pull_request` 事件本身既不能设置它们,也不能让不同的值生效,选择器表达式存在于工作流定义中。需要注意:故障切换期间,`pull_request` 运行执行的是 PR merge 引用自带的工作流定义——抵御不可信代码的边界是仓库成员资格(私有、禁 fork、选择器排除 Dependabot),而非该变量。关于 runner group 策略的说明:把 runner group 绑定到 master 引用的工作流与本故障切换机制**不兼容**——五个故障切换作业是从 PR merge 引用求值的 `pull_request` 运行,master 绑定的组会让它们持续排队(2026-07-27 实际故障中亲历;当时将组放宽为本仓库全部工作流才疏通了切换)。更严格的运行器侧策略以牺牲 PR 故障切换为代价;当前采用的形态是仓库范围、全工作流的组访问。
## 曾考虑的替代方案 ## 曾考虑的替代方案
**通过合并一次工作流改动来切换池。** 否决,因为触发切换的故障状态恰恰是任何 PR 都无法合并的状态:必需检查正是失败的那些。仓库变量是写者可管理的状态,重跑即生效,无需合并。 **通过合并一次工作流改动来切换池。** 否决,因为触发切换的故障状态恰恰是任何 PR 都无法合并的状态:必需检查正是失败的那些。仓库变量是写者可管理的状态,重跑即生效,无需合并。
**让自托管池长期处于必需路径中。** 否决,因为这是拿托管池的可用性去换自有虚拟机的可用性,只是搬移了单点故障而非增加回退。该变量让托管池保持主路径,自托管池作为一个经过验证、一步即可启用的热备。 **让自托管池长期处于必需路径中。** 否决,因为这是拿托管池的可用性去换自有虚拟机的可用性,只是搬移了单点故障而非增加回退。这些变量让托管池保持主路径,自托管池作为一个经过验证、一步即可启用的热备;按平台拆分意味着一个平台的故障不会重定向另一个平台。
## 后果 ## 后果
从托管池故障中恢复只需一个变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它,避免故障切换目标变得陈旧;而 `ci.yml` 中的并发与缓存恢复分支带有一条 `selfhosted` 支路,必须与托管支路保持同步。 从托管池故障中恢复只需切换受影响平台的变量(任何写者可设)加一次重跑,关键路径上没有合并。代价是每个平台都要维护第二套运行器拓扑:热备通道在每次 master 推送时都运行它们,避免故障切换目标变得陈旧;而 `ci.yml` 中的并发与缓存恢复分支带有一条 `selfhosted` 支路(仅 Linux),必须与托管支路保持同步。按平台拆分开关多了一个需要管理的变量,但把每个开关的影响范围限定在单个平台的作业上。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md # pnpm run verify-translation-pairing --write .agents/notes/implemented/simplification/2026-08-13-remove-first-run-beta-notice.md
2026-08-13-remove-first-run-beta-notice.md: 69f47be445ebfa9f10e6ba1217e4674a98c747f1 2026-08-13-remove-first-run-beta-notice.md: 21396eb9cc3e0e766238115967e354abe1032bd2
2026-08-13-remove-first-run-beta-notice.zh.md: 51c91918baaf1ae3db28434d7d86093fbd5ffb4e 2026-08-13-remove-first-run-beta-notice.zh.md: 2818343804e88e7114f5a6298393780b0fdcaf24

View File

@@ -10,7 +10,7 @@ Every GUI first launch opened with a full-viewport internal-test statement (内
## Decision ## Decision
The first-run notice is removed from the assembled product rather than reworded. `ui-settings-general` seats no `settings.onboarding` step; the notice component, its durable acknowledgement store, its copy owner, and its locale keys are deleted. The `settings.onboarding` coordinator and its takeover stage stay ([ordered onboarding](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)), and the conditional DeepSeek credential step is the only shipped occupant. The Host half still registers the `ui-onboarding` settings namespace: its `welcomeNoticeVersion` field keeps acknowledgements already stored in `$DSH_HOME/settings.yaml` valid, and nothing reads or writes it. Telemetry opt-in remains an explicit deployment environment choice documented in the [CLI reference README](../../../../apps/cli/reference/README.md); the product presents no prompt about enabling it. This decision removed the first-run notice from the assembled product rather than rewording it. `ui-settings-general` seated no `settings.onboarding` step; the notice component, acknowledgement store, copy owner, and locale keys were deleted, while the Host kept the `ui-onboarding` namespace so stored documents remained valid. The later [shared-modal product onboarding](../feature/2026-08-13-shared-modal-product-onboarding.md) restores a new concise testing-stage notice in `ui-settings-models`, reusing that field and backend contract without restoring the removed takeover layout or telemetry instructions. Telemetry opt-in remains an explicit deployment environment choice documented in the [CLI reference README](../../../../apps/cli/reference/README.md); the restored notice says nothing about enabling it.
## Alternatives considered ## Alternatives considered
@@ -22,4 +22,4 @@ The first-run notice is removed from the assembled product rather than reworded.
## Consequences ## Consequences
A fresh profile boots into the credential step when the DeepSeek credential is missing and directly into the product otherwise; no full-viewport notice precedes either. The assembled onboarding scenario starts at the credential step, the remote-notice scenario is deleted with the feature, and the goal-bar fixture keeps the settings shell disabled because the fixture API client rejects settings traffic. Restoring a first-run notice requires a new onboarding registration and a new versioned field; the retained namespace does not resurrect the old acknowledgement semantics. This removal eliminated the full-viewport notice and its telemetry copy. The later restoration is intentionally a different presentation and copy revision: a shared modal precedes the inline credential dialog, the remote scenario again covers process-local acknowledgement, and the existing `welcomeNoticeVersion` field records the new copy version. The historical telemetry prompt remains absent.

View File

@@ -10,7 +10,7 @@ GUI 每次首启都会先显示占满视口的内测声明:内部测试的定
## 决策 ## 决策
首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、其持久化确认 store、文案所有者文件和 locale 键全部删除。`settings.onboarding` 协调器及其接管式展示阶段保留([有序引导](../feature/2026-07-30-versioned-gui-welcome-onboarding.md)),按条件显示的 DeepSeek 凭据步骤是当前唯一的注册方。宿主端仍注册 `ui-onboarding` 设置 namespace:其中的 `welcomeNoticeVersion` 字段让 `$DSH_HOME/settings.yaml` 中已写入的确认记录保持有效,没有任何代码读取或写入它。遥测的开启仍是显式的部署环境变量选择,记录在 [CLI reference README](../../../../apps/cli/reference/README.md) 中;产品界面不出现任何关于开启遥测的提示。 本决策当时把首启声明从组装后的产品中整体移除,而不是改写。`ui-settings-general` 不再注册任何 `settings.onboarding` 步骤;声明组件、确认 store、文案所有者文件和 locale 键均被删除,Host 则保留 `ui-onboarding` namespace,使既有设置文档继续有效。后续的[共用弹窗产品引导](../feature/2026-08-13-shared-modal-product-onboarding.md)在 `ui-settings-models` 中恢复了一份新的简洁测试阶段声明,复用该字段与后端契约,但不会恢复已移除的接管式布局或遥测说明。遥测的开启仍是显式的部署环境变量选择,记录在 [CLI reference README](../../../../apps/cli/reference/README.md) 中;恢复后的声明不涉及如何开启遥测。
## 曾考虑的替代方案 ## 曾考虑的替代方案
@@ -22,4 +22,4 @@ GUI 每次首启都会先显示占满视口的内测声明:内部测试的定
## 后果 ## 后果
全新 profile 在缺少 DeepSeek 凭据时直接进入凭据步骤,凭据已配置时直接进入产品,两种情况都没有占满视口的声明。组装级引导场景从凭据步骤开始,远程声明场景随功能一并删除,goal-bar fixture 继续禁用设置外壳,因为 fixture API 客户端会拒绝设置请求。将来若要恢复首启声明,需要新的引导注册和新的版本化字段;保留的 namespace 不会复活旧的确认语义。 这次移除消除了占满视口的声明及其遥测文案。后续恢复有意采用不同的展示与文案版本:共用弹窗先于行内凭据弹窗出现,远程场景重新覆盖进程内确认,既有 `welcomeNoticeVersion` 字段记录新的文案版本。历史上的遥测提示仍未恢复。

2
.github/AGENTS.md vendored
View File

@@ -1,3 +1,3 @@
# AGENTS.md — GitHub Actions # AGENTS.md — GitHub Actions
Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` (or the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER=selfhosted`) but reports independently. The master `serial-windows` standby continuously validates the self-hosted failover target — see the [failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md). Run jobs on Windows runners (`windows-*` labels) under native `pwsh`. The pull-request `windows` job is the deliberate exception: it runs Windows Node under Wine on hosted Linux and blocks `all checks passed`; `windows-native` runs automatically on `windows-2025` (or the self-hosted `[self-hosted, dsh-win-ci, windows]` pool under `DSH_CI_FAILOVER_WINDOWS=selfhosted`) but reports independently. The master `serial-windows` standby continuously validates the self-hosted failover target — see the [failover runbook](../.agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md).

View File

@@ -49,8 +49,8 @@ jobs:
# build-backed consumer tail. The consumer job owns the only Linux build so # build-backed consumer tail. The consumer job owns the only Linux build so
# all three jobs enter runner allocation independently. # all three jobs enter runner allocation independently.
# #
# FAILOVER: each Linux enterprise job resolves its pool through the # FAILOVER (Linux): each Linux enterprise job resolves its pool through the
# DSH_CI_FAILOVER repository variable. Unset (normal), the expressions # DSH_CI_FAILOVER_LINUX repository variable. Unset (normal), the expressions
# pick the hosted enterprise pools below. Setting the variable to # pick the hosted enterprise pools below. Setting the variable to
# 'selfhosted' (repo Settings → Actions → Variables; writer-manageable # 'selfhosted' (repo Settings → Actions → Variables; writer-manageable
# repository state — not PR-editable, no merge required) retargets all # repository state — not PR-editable, no merge required) retargets all
@@ -58,11 +58,12 @@ jobs:
# vm-backup pool and re-running the failed jobs is the entire switch — # vm-backup pool and re-running the failed jobs is the entire switch —
# see .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md. The # see .agents/notes/implemented/process/2026-07-26-ci-failover-runbook.md. The
# in-house pool's readiness is re-proven on every master push by the # in-house pool's readiness is re-proven on every master push by the
# serial-linux-selfhosted standby lane below. # serial-linux-selfhosted standby lane below. The Windows failover switch is
# the separate DSH_CI_FAILOVER_WINDOWS variable on the windows-native job below.
node-24: node-24:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: >- runs-on: >-
${{ vars.DSH_CI_FAILOVER == 'selfhosted' ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'dsh-ubuntu-24-04-16core' }} || 'dsh-ubuntu-24-04-16core' }}
@@ -96,7 +97,7 @@ jobs:
# compression and upload on the paid latency-critical path. Skipped # compression and upload on the paid latency-critical path. Skipped
# under failover — see the coverage lane's identical rationale. # under failover — see the coverage lane's identical rationale.
- uses: actions/cache/restore@v4 - uses: actions/cache/restore@v4
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
with: with:
path: ${{ steps.pnpm-store.outputs.path }} path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
@@ -114,7 +115,7 @@ jobs:
node-24-coverage: node-24-coverage:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: >- runs-on: >-
${{ vars.DSH_CI_FAILOVER == 'selfhosted' ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'dsh-ubuntu-24-04-16core' }} || 'dsh-ubuntu-24-04-16core' }}
@@ -124,7 +125,7 @@ jobs:
# shares one 64-core VM across six always-on runner instances, so each # shares one 64-core VM across six always-on runner instances, so each
# instance may use eight while keeping the worst case at 8 × 6 = 48 # instance may use eight while keeping the worst case at 8 × 6 = 48
# workers; process-bound suites remain isolated in forks. # workers; process-bound suites remain isolated in forks.
DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '6' }} DSH_COVERAGE_MAX_WORKERS: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '8' || '6' }}
DSH_GATE_CONCURRENCY: '3' DSH_GATE_CONCURRENCY: '3'
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
@@ -151,7 +152,7 @@ jobs:
# already serves warm installs, while restoring the hosted archive # already serves warm installs, while restoring the hosted archive
# would spend ~52 s pulling ~180 MB into that populated store. # would spend ~52 s pulling ~180 MB into that populated store.
- uses: actions/cache/restore@v4 - uses: actions/cache/restore@v4
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
with: with:
path: ${{ steps.pnpm-store.outputs.path }} path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
@@ -177,7 +178,7 @@ jobs:
node-24-consumers: node-24-consumers:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: >- runs-on: >-
${{ vars.DSH_CI_FAILOVER == 'selfhosted' ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'dsh-ubuntu-24-04-16core' }} || 'dsh-ubuntu-24-04-16core' }}
@@ -188,7 +189,7 @@ jobs:
DSH_OXLINT_THREADS: '8' DSH_OXLINT_THREADS: '8'
DSH_PUBLINT_CONCURRENCY: '8' DSH_PUBLINT_CONCURRENCY: '8'
# Failover halves snapshot concurrency for the shared 64-core VM. # Failover halves snapshot concurrency for the shared 64-core VM.
DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }} DSH_SNAPSHOT_MAX_CONCURRENCY: ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' && '12' || '32' }}
steps: steps:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
with: with:
@@ -212,7 +213,7 @@ jobs:
# Skipped under failover — see the coverage lane's identical rationale. # Skipped under failover — see the coverage lane's identical rationale.
- uses: actions/cache/restore@v4 - uses: actions/cache/restore@v4
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
with: with:
path: ${{ steps.pnpm-store.outputs.path }} path: ${{ steps.pnpm-store.outputs.path }}
key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }} key: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
@@ -222,7 +223,7 @@ jobs:
# Pull requests restore the cache normally produced by serial-linux on # Pull requests restore the cache normally produced by serial-linux on
# master; they do not pay compression and upload on the required path. # master; they do not pay compression and upload on the required path.
- uses: actions/cache/restore@v4 - uses: actions/cache/restore@v4
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
with: with:
path: ~/.cache/ms-playwright path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }} key: ${{ runner.os }}-playwright-${{ hashFiles('pnpm-lock.yaml') }}
@@ -243,13 +244,13 @@ jobs:
exit "$sandbox_status" exit "$sandbox_status"
- name: Install Playwright Chromium and hosted dependencies - name: Install Playwright Chromium and hosted dependencies
if: vars.DSH_CI_FAILOVER != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX != 'selfhosted' || github.event.pull_request.user.login == 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install --with-deps chromium
# The persistent VM image owns Playwright's Linux system packages; do # The persistent VM image owns Playwright's Linux system packages; do
# not mutate the shared host with apt on every failover run. # not mutate the shared host with apt on every failover run.
- name: Install Playwright Chromium on the failover VM - name: Install Playwright Chromium on the failover VM
if: vars.DSH_CI_FAILOVER == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]' if: vars.DSH_CI_FAILOVER_LINUX == 'selfhosted' && github.event.pull_request.user.login != 'dependabot[bot]'
run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium run: pnpm --filter @deepseek-ai/dsh-web-frontend exec playwright install chromium
- name: Run compatibility, snapshot, and artifact gates - name: Run compatibility, snapshot, and artifact gates
@@ -437,13 +438,16 @@ jobs:
# its own unmasked conclusion but is deliberately absent from # its own unmasked conclusion but is deliberately absent from
# all-checks-passed.needs, so it never delays or changes that required # all-checks-passed.needs, so it never delays or changes that required
# verdict. Under normal operation it runs on the hosted larger runner; under # verdict. Under normal operation it runs on the hosted larger runner; under
# failover (DSH_CI_FAILOVER=selfhosted) it retargets onto the in-house # Windows failover (DSH_CI_FAILOVER_WINDOWS=selfhosted) it retargets onto the
# self-hosted Windows pool. Dependabot PRs are excluded from the self-hosted # in-house self-hosted Windows pool. Dependabot PRs are excluded from the
# pool and stay queued for the hosted runner — see the failover runbook. # self-hosted pool and stay queued for the hosted runner — see the failover
# runbook. This Windows switch is independent of the Linux
# DSH_CI_FAILOVER_LINUX variable that retargets the three required Linux jobs
# and the all-checks-passed verdict above.
windows-native: windows-native:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
runs-on: >- runs-on: >-
${{ vars.DSH_CI_FAILOVER == 'selfhosted' ${{ vars.DSH_CI_FAILOVER_WINDOWS == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "dsh-win-ci", "windows"]') && fromJSON('["self-hosted", "dsh-win-ci", "windows"]')
|| 'dsh-windows-2025-16core' }} || 'dsh-windows-2025-16core' }}
@@ -558,7 +562,7 @@ jobs:
# re-runs the complete unsharded aggregate on the persistent 64-core VM, # re-runs the complete unsharded aggregate on the persistent 64-core VM,
# continuously proving that environment can take over a required lane if # continuously proving that environment can take over a required lane if
# the hosted pools degrade (the switch is then setting the writer-manageable # the hosted pools degrade (the switch is then setting the writer-manageable
# DSH_CI_FAILOVER variable — see the failover runbook, no merge required). # DSH_CI_FAILOVER_LINUX variable — see the failover runbook, no merge required).
# Push-triggered, so this lane always executes the base branch's own # Push-triggered, so this lane always executes the base branch's own
# workflow definition. (Under failover, pull_request jobs do reach these # workflow definition. (Under failover, pull_request jobs do reach these
# runners with the PR merge ref's workflow — the boundary there is # runners with the PR merge ref's workflow — the boundary there is
@@ -644,11 +648,11 @@ jobs:
# move re-runs the complete unsharded Windows gate inventory on the persistent # move re-runs the complete unsharded Windows gate inventory on the persistent
# VM, continuously proving that environment can take over the required # VM, continuously proving that environment can take over the required
# `windows` lane if the hosted pool degrades (the switch is setting the # `windows` lane if the hosted pool degrades (the switch is setting the
# writer-manageable DSH_CI_FAILOVER variable — see the failover runbook, no # writer-manageable DSH_CI_FAILOVER_WINDOWS variable — see the failover
# merge required). Push-triggered, so this lane always executes the base # runbook, no merge required). Push-triggered, so this lane always executes
# branch's own workflow definition. Non-blocking for pull requests; absent # the base branch's own workflow definition. Non-blocking for pull requests;
# from all-checks-passed.needs by design — the required `windows` job owns # absent from all-checks-passed.needs by design — the required `windows` job
# the PR verdict. No cache steps because the VM's persistent pnpm store # owns the PR verdict. No cache steps because the VM's persistent pnpm store
# and tool caches make them redundant (and saving here would poison the # and tool caches make them redundant (and saving here would poison the
# hosted cache namespace with self-hosted paths). # hosted cache namespace with self-hosted paths).
serial-windows: serial-windows:
@@ -911,11 +915,13 @@ jobs:
all-checks-passed: all-checks-passed:
name: all checks passed name: all checks passed
# This bookkeeping-only verdict must not depend on custom-pool # This bookkeeping-only verdict must not depend on custom-pool
# provisioning — and under failover it follows the same selector as the # provisioning — and under Linux failover it follows the same selector as
# worker jobs it aggregates, so a standard-hosted outage cannot strand # the worker jobs it aggregates, so a standard-hosted outage cannot strand
# the branch-protection verdict either. # the branch-protection verdict either. It retargets with the Linux switch
# (DSH_CI_FAILOVER_LINUX), not the Windows one, because it aggregates the
# required Linux workers and runs on the vm-backup pool.
runs-on: >- runs-on: >-
${{ vars.DSH_CI_FAILOVER == 'selfhosted' ${{ vars.DSH_CI_FAILOVER_LINUX == 'selfhosted'
&& github.event.pull_request.user.login != 'dependabot[bot]' && github.event.pull_request.user.login != 'dependabot[bot]'
&& fromJSON('["self-hosted", "linux", "x64", "vm-backup"]') && fromJSON('["self-hosted", "linux", "x64", "vm-backup"]')
|| 'ubuntu-latest' }} || 'ubuntu-latest' }}

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write README.md # pnpm run verify-translation-pairing --write README.md
README.md: 098b514f8311de092bab18bdd560722b1e485913 README.md: 8a4bd01332a23ce4144c661784bc549e0ba72d21
README.zh.md: 639d519b6202ffe740d46e462feec2657d7e13ad README.zh.md: b7bc214bfb1fd8a76a47de3f0aa242122aeb7603

View File

@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
DeepSeek Harness (`dsh`) is an open-source agent harness developed by [DeepSeek AI](https://deepseek.com). DeepSeek Harness (`dsh`) is an open-source agent harness developed by [DeepSeek AI](https://deepseek.com).
It uses an architecture where **everything is a plugin**, and is powered by [Cordis](https://github.com/cordiverse/cordis). It uses an architecture where **everything is a plugin**, and is powered by [Cordis](https://github.com/cordiverse/cordis), whose design is described in [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper).
## Developer preview ## Developer preview

View File

@@ -4,7 +4,7 @@
DeepSeek Harness(`dsh`)是由 [DeepSeek AI](https://deepseek.com) 开发的开源 agent harness(智能体框架)。 DeepSeek Harness(`dsh`)是由 [DeepSeek AI](https://deepseek.com) 开发的开源 agent harness(智能体框架)。
它采用**一切皆插件**的架构,并由 [Cordis](https://github.com/cordiverse/cordis) 驱动。 它采用**一切皆插件**的架构,并由 [Cordis](https://github.com/cordiverse/cordis) 驱动,其设计参见论文 [_A Programming Paradigm for Spatiotemporal Composability_](https://github.com/cordiverse/paper)。
## 开发者预览 ## 开发者预览

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh", "name": "@deepseek-ai/dsh",
"description": "dsh CLI: profile boot, plugin management, and the browser UI alias", "description": "dsh CLI: profile boot, plugin management, and the browser UI alias",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/cli/reference/README.md # pnpm run verify-translation-pairing --write apps/cli/reference/README.md
README.md: 0b23db293c304807b5959f5eae9a2faf868caa62 README.md: 0be64fdfc0ad4e81d23f25a26881fa89f37565b0
README.zh.md: d812c7cd0d1efaa69b135925f3e922668539781b README.zh.md: 649bf15df814abf4875fed794c2e76c494bfcc25

View File

@@ -61,7 +61,7 @@ dsh web --dump-config
dsh web --help dsh web --help
``` ```
The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. Binding all interfaces also trusts the machine's discovered LAN IP literals; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence. The production Web runner needs built package and frontend artifacts (`pnpm run build`). It serves `http://127.0.0.1:3080` by default. The CLI intentionally does not support `--host 0.0.0.0` yet and exits with a usage error; `--trusted-host` adds named authorities accepted by the `/api` browser-trust fence.
Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed. Process shutdown gives the plugin tree up to five seconds to dispose. The first `SIGINT`/`SIGTERM` starts that graceful drain — `SIGTERM` is a supervisor's ordinary stop request and exits 0 on every surface, `SIGINT` reports 130; a second signal forces immediate exit. If one-shot normal completion is already stuck in disposal, the first `Ctrl+C` is the escalation and exits immediately instead of being swallowed.

View File

@@ -61,7 +61,7 @@ dsh web --dump-config
dsh web --help dsh web --help
``` ```
生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。绑定所有网络接口时,还会信任机器自动发现的 LAN IP 字面量;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。 生产 Web 运行器需要已构建的包和前端产物(`pnpm run build`)。默认服务地址是 `http://127.0.0.1:3080`。CLI 目前有意不支持 `--host 0.0.0.0`,并会以用法错误退出;`--trusted-host` 可添加 `/api` 浏览器信任围栏接受的具名 authority。
进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。 进程关闭时,插件树最多有 5 秒完成 dispose。首次收到 `SIGINT` 或 `SIGTERM` 时会开始优雅排空:`SIGTERM` 是监督进程发出的常规停止请求,在所有运行模式下都以 0 退出;`SIGINT` 则报告 130。第二次收到信号时会立即强制退出。如果一次性运行在正常结束时已经卡在 dispose 阶段,第一次按下 `Ctrl+C` 就会直接升级为强制退出,而不会被忽略。

View File

@@ -36,8 +36,8 @@ describe('parseDshArgs', () => {
.toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] }) .toEqual({ mode: 'profile', profile: 'tui', patches: [], args: ['--resume', 'abc'] })
expect(parse(['--profile', 'web', '-h'])) expect(parse(['--profile', 'web', '-h']))
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] }) .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['-h'] })
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev'])) expect(parse(['web', '--host', '127.0.0.1', '--port', '8080', '--dev']))
.toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '0.0.0.0', '--port', '8080', '--dev'] }) .toEqual({ mode: 'profile', profile: 'web', patches: [], args: ['--host', '127.0.0.1', '--port', '8080', '--dev'] })
expect(parse(['--profile', 'headless', 'run', 'the', 'tests'])) expect(parse(['--profile', 'headless', 'run', 'the', 'tests']))
.toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] }) .toEqual({ mode: 'profile', profile: 'headless', patches: [], args: ['run', 'the', 'tests'] })
// Launcher flags placed after that boundary belong to the app too. // Launcher flags placed after that boundary belong to the app too.

View File

@@ -339,6 +339,15 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(web.stdout).toContain('--port <port>') expect(web.stdout).toContain('--port <port>')
expect(web.stdout).not.toContain('dsh web: http://') expect(web.stdout).not.toContain('dsh web: http://')
const wildcardHost = await runBuiltBin(['web', '--host', '0.0.0.0'], {
DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1',
})
expect(wildcardHost.code).toBe(1)
expect(wildcardHost.stdout).toBe('')
expect(wildcardHost.stderr).toContain('--host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
expect(wildcardHost.stderr).not.toContain('dsh web: http://')
const headlessHelp = await runBuiltBin(['--profile', 'headless', '--help'], { const headlessHelp = await runBuiltBin(['--profile', 'headless', '--help'], {
DSH_HOME: home, DSH_HOME: home,
DSH_TELEMETRY_DISABLED: '1', DSH_TELEMETRY_DISABLED: '1',

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-web-frontend", "name": "@deepseek-ai/dsh-web-frontend",
"description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web", "description": "Web application entry: vite build over the @deepseek-ai/dsh-client-web shell library; dist/ served by apps/cli's dsh web",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write apps/web/tests/README.md # pnpm run verify-translation-pairing --write apps/web/tests/README.md
README.md: 4d7236ef69189845dbc2847289d497fc54c94ae9 README.md: acb0c300bafe221f6a92f0168908bebf965377b9
README.zh.md: 21c2f2ce7ccc8fe5314d766c06235453aa292031 README.zh.md: fd3d950a106375bb038b205415407b4a31bd2b32

View File

@@ -30,7 +30,8 @@ artifact it produces itself.
When a scenario needs a Client-owned constant or pure function, mirror it here When a scenario needs a Client-owned constant or pure function, mirror it here
instead, next to the commented-out import that names the source module. A drift instead, next to the commented-out import that names the source module. A drift
then surfaces as a missed selector or a stale mirrored value — a loud failure, then surfaces as a missed selector or a stale mirrored value — a loud failure,
never a silent pass. never a silent pass. `scaffold.ts` follows this rule for the welcome-notice
namespace, acknowledgement field, version, and asserted Chinese copy.
Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so Two kinds of Client import stand. `assembled-boot.ts` drives the shell itself, so
it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest it imports `AppWebEntry` from `@deepseek-ai/dsh-client-web` and the boot-manifest

View File

@@ -24,7 +24,7 @@ Client face,而该 face 必须等 Host tsdown 生成 `@deepseek-ai/dsh-goal/re
当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的 当某个场景需要 Client 持有的常量或纯函数时,改为在此处镜像一份,并紧挨着一条注释掉的
import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默 import 点明源模块。这样漂移会表现为选择器未命中或镜像值过期——是响亮的失败,绝不会是静默
通过。 通过。`scaffold.ts` 按此规则镜像欢迎声明的 namespace、确认字段、版本和被断言的中文文案。
有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从 有两类 Client import 是长期成立的。`assembled-boot.ts` 驱动 shell 本身,因此它从
`@deepseek-ai/dsh-client-web` import `AppWebEntry`、从 `@deepseek-ai/dsh-client-web` import `AppWebEntry`、从

View File

@@ -26,7 +26,7 @@ describe('web e2e: goal bar clear convergence', () => {
let tripwire: ReturnType<typeof watchConsole> let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => { beforeAll(async () => {
scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY }) scaffold = await launchWebScaffold({ extraOverlayPath: OVERLAY, welcomeNoticePending: true })
browser = await chromium.launch() browser = await chromium.launch()
page = await newEnglishPage(browser) page = await newEnglishPage(browser)
tripwire = watchConsole(page) tripwire = watchConsole(page)

View File

@@ -1,6 +1,7 @@
// Keyless browser e2e: the shipped DeepSeek adapter stays mounted while its // Keyless browser e2e: the shipped DeepSeek adapter stays mounted while its
// credential is absent, onboarding routes to the real Models editor, and its // credential is absent, both ordered steps share the shipped modal chrome,
// write lands in an isolated harness home without a reload or model call. // and the inline key write lands in an isolated harness home without a reload
// or model call.
import { randomBytes } from 'node:crypto' import { randomBytes } from 'node:crypto'
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
@@ -8,13 +9,17 @@ import { join } from 'node:path'
import type { Browser, Page } from 'playwright' import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright' import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { import {
acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden,
launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold, launchWebScaffold, watchConsole, webSnapshotMode, type WebScaffold,
WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_COPY, WELCOME_NOTICE_SETTINGS_NAMESPACE,
WELCOME_NOTICE_VERSION,
} from './scaffold.ts' } from './scaffold.ts'
import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts' import { ZH_BROWSER_LOCALE, connectFreshWorkspaceZh, saveFailureShot } from './support.ts'
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url)) const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/onboarding-deepseek-config', import.meta.url))
const WELCOME_EXPECTED = join(SNAPSHOT_DIR, 'welcome.expected.md')
const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md') const MISSING_EXPECTED = join(SNAPSHOT_DIR, 'missing.expected.md')
const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md') const MODELS_EXPECTED = join(SNAPSHOT_DIR, 'models.expected.md')
const MODE = webSnapshotMode() const MODE = webSnapshotMode()
@@ -27,7 +32,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
const browserConsole: string[] = [] const browserConsole: string[] = []
beforeAll(async () => { beforeAll(async () => {
scaffold = await launchWebScaffold({ deepSeekMissingCredential: true }) scaffold = await launchWebScaffold({ deepSeekMissingCredential: true, welcomeNoticePending: true })
browser = await chromium.launch() browser = await chromium.launch()
// The scenario asserts the shipped Chinese copy, so the browser asks for it. // The scenario asserts the shipped Chinese copy, so the browser asks for it.
page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE }) page = await browser.newPage({ viewport: { width: 1440, height: 960 }, locale: ZH_BROWSER_LOCALE })
@@ -44,53 +49,40 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('stores a key write-only and observes configured state without restarting', async () => { it('stores a key write-only and observes configured state without restarting', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config')) onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-deepseek-config'))
const credentialStep = page.getByRole('region', { name: '添加一个 API Key 开始使用' }) const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title })
await credentialStep.waitFor({ timeout: 15_000 }) await welcome.waitFor({ timeout: 15_000 })
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true) expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
for (const paragraph of WELCOME_NOTICE_COPY.zh.body.split('\n\n')) {
expect(await welcome.getByText(paragraph, { exact: true }).count()).toBe(1)
}
expect(await welcome.getByRole('button').allTextContents()).toEqual([
WELCOME_NOTICE_COPY.zh.continueLabel,
])
const welcomeAria = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(WELCOME_EXPECTED, welcomeAria, MODE)
const mask = page.locator('[class*="onboardingMask"]') // Observation is not acknowledgement: the exact version is persisted
expect(await mask.count()).toBe(1) // only by the explicit action, so a reload still presents this dialog.
const maskStyles = await mask.evaluate((mask) => { const firstReloadWarnings = tripwire.warnings.length
const style = getComputedStyle(mask) await page.reload({ waitUntil: 'load' })
const rect = mask.getBoundingClientRect() acknowledgeReloadConnectionLoss(tripwire, firstReloadWarnings)
return { await welcome.waitFor({ timeout: 15_000 })
position: style.position,
left: style.left,
right: style.right,
top: style.top,
bottom: style.bottom,
background: style.backgroundColor,
backdropFilter: style.backdropFilter,
rect: { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom },
}
})
expect(maskStyles).toEqual({
position: 'absolute',
left: '0px',
right: '0px',
top: '80px',
bottom: '0px',
background: 'rgba(0, 0, 0, 0.24)',
backdropFilter: 'blur(2px)',
rect: { left: 0, top: 80, right: 1440, bottom: 960 },
})
expect(await credentialStep.getByRole('textbox').count()).toBe(0) await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
const initial = await captureStableAria(page, '[role="region"]', scaffold.workspaceCwd) await welcome.waitFor({ state: 'detached', timeout: 15_000 })
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
await credentialStep.getByRole('button', { name: '前往配置' }).click() const credentialStep = page.getByRole('dialog', { name: '添加一个 API Key 开始使用' })
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 }) await credentialStep.waitFor({ timeout: 15_000 })
const settings = page.getByRole('dialog', { name: '设置' }) const keyInput = credentialStep.getByLabel('API 密钥', { exact: true })
await settings.waitFor({ timeout: 10_000 })
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
const keyInput = settings.getByLabel('API 密钥', { exact: true })
await keyInput.waitFor({ timeout: 10_000 }) await keyInput.waitFor({ timeout: 10_000 })
const initial = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(MISSING_EXPECTED, initial, MODE)
const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}` const secret = `dsh_onboarding_${randomBytes(12).toString('hex')}`
await keyInput.fill(secret) await keyInput.fill(secret)
await settings.getByRole('button', { name: '保存', exact: true }).click() await credentialStep.getByRole('button', { name: '保存并继续' }).click()
await keyInput.waitFor({ state: 'detached', timeout: 15_000 }) await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
const stored = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8') const stored = await readFile(join(scaffold.harnessHome, '.credentials.yaml'), 'utf8')
expect(stored.includes(`DEEPSEEK_API_KEY: ${secret}`)).toBe(true) expect(stored.includes(`DEEPSEEK_API_KEY: ${secret}`)).toBe(true)
@@ -98,8 +90,15 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false) expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
expect(browserConsole.some(line => line.includes(secret))).toBe(false) expect(browserConsole.some(line => line.includes(secret))).toBe(false)
// The same open Models surface reuses the refreshed join and exposes the const acknowledgedSettings = await readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8')
expect(acknowledgedSettings).toContain(`${WELCOME_NOTICE_ACK_FIELD}: ${WELCOME_NOTICE_VERSION}`)
// The ordinary Models surface reuses the refreshed join and exposes the
// configured write-only placeholder without a reload. // configured write-only placeholder without a reload.
await page.getByRole('button', { name: '设置', exact: true }).click()
const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 })
await settings.getByRole('button', { name: '模型' }).click()
const deepSeekRow = settings.getByText('DeepSeek', { exact: true }).first() const deepSeekRow = settings.getByText('DeepSeek', { exact: true }).first()
await deepSeekRow.waitFor({ timeout: 10_000 }) await deepSeekRow.waitFor({ timeout: 10_000 })
await deepSeekRow.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click() await deepSeekRow.locator('xpath=ancestor::li').getByRole('button', { name: '编辑' }).click()
@@ -114,7 +113,21 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
await page.reload({ waitUntil: 'load' }) await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings) acknowledgeReloadConnectionLoss(tripwire, secondReloadWarnings)
await page.waitForSelector('[class*="frame"]', { timeout: 15_000 }) await page.waitForSelector('[class*="frame"]', { timeout: 15_000 })
expect(await page.getByRole('region', { name: '添加一个 API Key 开始使用' }).count()).toBe(0) expect(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
// An old acknowledgement means materially revised copy: welcome returns,
// while the already-configured provider step remains complete.
await scaffold.ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: 'previous-copy-version',
}])
const thirdReloadWarnings = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, thirdReloadWarnings)
await welcome.waitFor({ timeout: 15_000 })
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
expect((await page.content()).includes(secret)).toBe(false) expect((await page.content()).includes(secret)).toBe(false)
expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false) expect((await page.locator('body').ariaSnapshot()).includes(secret)).toBe(false)
@@ -125,10 +138,9 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => { it('never paints the takeover chrome on a configured reload, even with the settings join held open', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload')) onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-configured-reload'))
// Regression pin for the reload white flash: the credential step is // Regression pin for the reload flash: both steps are satisfied, yet each
// satisfied (credential configured), yet it must LOAD its // must load private facts before deciding not to show. Dialog chrome lives
// private join before it can decide not to show. The chrome lives inside // inside each visible branch, so the deciding window paints and blocks
// the step (OnboardingSurface), so the deciding window paints and blocks
// nothing. Holding settings.describe widens that window from loopback // nothing. Holding settings.describe widens that window from loopback
// RTT scale to a deterministic hundreds of milliseconds, removing all // RTT scale to a deterministic hundreds of milliseconds, removing all
// timing dependence from the sampler assertions below. // timing dependence from the sampler assertions below.
@@ -141,7 +153,10 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
const sightings: string[] = [] const sightings: string[] = []
;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings ;(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings = sightings
setInterval(() => { setInterval(() => {
if (document.querySelector('[class*="onboardingStage"], [class*="onboardingMask"]') !== null) { if (document.querySelector(
'[role="dialog"][aria-label="内测声明"], '
+ '[role="dialog"][aria-label="添加一个 API Key 开始使用"]',
) !== null) {
sightings.push('chrome') sightings.push('chrome')
} }
if (document.getElementById('root')?.inert === true) sightings.push('inert') if (document.getElementById('root')?.inert === true) sightings.push('inert')
@@ -171,7 +186,8 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
acknowledgeReloadConnectionLoss(tripwire, warningsBefore) acknowledgeReloadConnectionLoss(tripwire, warningsBefore)
expect(await page.evaluate(() => expect(await page.evaluate(() =>
(window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([]) (window as unknown as { __takeoverSightings: string[] }).__takeoverSightings)).toEqual([])
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0) expect(await page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title }).count()).toBe(0)
expect(await page.getByRole('dialog', { name: '添加一个 API Key 开始使用' }).count()).toBe(0)
expect(tripwire.pageErrors).toEqual([]) expect(tripwire.pageErrors).toEqual([])
}, 60_000) }, 60_000)
@@ -228,7 +244,7 @@ describe.skipIf(MODE === 'record')('web e2e: first-run DeepSeek credential setup
it('keeps the fixture inventory closed', async () => { it('keeps the fixture inventory closed', async () => {
await assertFixtureInventory( await assertFixtureInventory(
SNAPSHOT_DIR, SNAPSHOT_DIR,
['missing.expected.md', 'models.expected.md'], ['welcome.expected.md', 'missing.expected.md', 'models.expected.md'],
) )
}) })
}) })

View File

@@ -44,14 +44,17 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
it('closes the setup card without discarding the add card beside it', async () => { it('closes the setup card without discarding the add card beside it', async () => {
onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel')) onTestFailed(() => saveFailureShot(page, 'web-e2e-onboarding-setup-card-cancel'))
const credentialStep = page.getByRole('region', { name: CREDENTIAL_STEP }) const credentialStep = page.getByRole('dialog', { name: CREDENTIAL_STEP })
await credentialStep.waitFor({ timeout: 15_000 }) await credentialStep.waitFor({ timeout: 15_000 })
await credentialStep.getByRole('button', { name: '前往配置' }).click() await credentialStep.getByRole('button', { name: '稍后配置' }).click()
await credentialStep.waitFor({ state: 'detached', timeout: 15_000 }) await credentialStep.waitFor({ state: 'detached', timeout: 15_000 })
await page.getByRole('button', { name: '设置', exact: true }).click()
const settings = page.getByRole('dialog', { name: '设置' }) const settings = page.getByRole('dialog', { name: '设置' })
await settings.waitFor({ timeout: 10_000 }) await settings.waitFor({ timeout: 10_000 })
// Nothing is reachable yet, so DeepSeek presents itself as its open card. // The onboarding step no longer navigates into Settings on dismissal, so
// enter the Models section explicitly before exercising its normal cards.
await settings.getByRole('button', { name: '模型' }).click()
const setupKey = settings.getByRole('textbox', { name: 'API 密钥', exact: true }) const setupKey = settings.getByRole('textbox', { name: 'API 密钥', exact: true })
await setupKey.waitFor({ timeout: 10_000 }) await setupKey.waitFor({ timeout: 10_000 })
@@ -66,15 +69,14 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
{ timeout: 10_000 }, { timeout: 10_000 },
).toBe(2) ).toBe(2)
// Cancelling the setup card is the regression: it used to leave itself open // Cancelling the setup card must not close the independent add-provider
// and close the add card, discarding that draft. // draft beside it.
await settings.getByRole('button', { name: '取消', exact: true }).first().click() await settings.getByRole('button', { name: '取消', exact: true }).first().click()
expect(await settings.getByLabel('提供方').count()).toBe(1) expect(await settings.getByLabel('提供方').count()).toBe(1)
await expect.poll( await expect.poll(
async () => settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count(), async () => settings.getByRole('textbox', { name: 'API 密钥', exact: true }).count(),
{ timeout: 10_000 }, { timeout: 10_000 },
).toBe(1) ).toBe(1)
// DeepSeek is now an ordinary row: a missing-key dot and an Edit button.
await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 }) await settings.getByRole('button', { name: '编辑 DeepSeek (deepseek-official)' }).waitFor({ timeout: 10_000 })
const dismissed = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd) const dismissed = await captureStableAria(page, '[role="dialog"]', scaffold.workspaceCwd)
await compareOrRefreshGolden(DISMISSED_EXPECTED, dismissed, MODE) await compareOrRefreshGolden(DISMISSED_EXPECTED, dismissed, MODE)
@@ -104,10 +106,9 @@ describe.skipIf(MODE === 'record')('web e2e: another usable provider ends first-
// The regression: the step read only the official route's credential, so a // The regression: the step read only the official route's credential, so a
// fully configured user was taken over on every blank session. // fully configured user was taken over on every blank session.
await expect.poll( await expect.poll(
async () => page.getByRole('region', { name: CREDENTIAL_STEP }).count(), async () => page.getByRole('dialog', { name: CREDENTIAL_STEP }).count(),
{ timeout: 10_000 }, { timeout: 10_000 },
).toBe(0) ).toBe(0)
expect(await page.locator('[class*="onboardingStage"]').count()).toBe(0)
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false) expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(false)
// The Models page agrees: DeepSeek stays a row rather than reopening its // The Models page agrees: DeepSeek stays a row rather than reopening its

View File

@@ -0,0 +1,60 @@
// Trusted non-loopback Web access cannot call the loopback-only settings API;
// the notice therefore advances for this browser process and returns on reload.
import type { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import {
acknowledgeReloadConnectionLoss, launchWebScaffold, watchConsole, webSnapshotMode,
WELCOME_NOTICE_COPY,
type WebScaffold,
} from './scaffold.ts'
import { ZH_BROWSER_LOCALE } from './support.ts'
const MODE = webSnapshotMode()
describe.skipIf(MODE === 'record')('web e2e: remote welcome notice', () => {
let scaffold: WebScaffold
let browser: Browser
let page: Page
let tripwire: ReturnType<typeof watchConsole>
beforeAll(async () => {
scaffold = await launchWebScaffold({
remoteAuthority: 'remote.localhost',
welcomeNoticePending: true,
})
browser = await chromium.launch()
page = await browser.newPage({
viewport: { width: 1440, height: 960 },
locale: ZH_BROWSER_LOCALE,
})
tripwire = watchConsole(page)
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
await page.waitForSelector('#root', { timeout: 30_000 })
}, 120_000)
afterAll(async () => {
await browser?.close()
await scaffold?.close()
})
it('advances process-locally and presents the notice again after reload', async () => {
const welcome = page.getByRole('dialog', { name: WELCOME_NOTICE_COPY.zh.title })
await welcome.waitFor({ timeout: 15_000 })
expect(await page.locator('#root').evaluate(root => (root as HTMLElement).inert)).toBe(true)
await welcome.getByRole('button', { name: WELCOME_NOTICE_COPY.zh.continueLabel }).click()
await welcome.waitFor({ state: 'detached', timeout: 15_000 })
await expect.poll(
() => page.locator('#root').evaluate(root => (root as HTMLElement).inert),
{ timeout: 15_000 },
).toBe(false)
const reloadWarnings = tripwire.warnings.length
await page.reload({ waitUntil: 'load' })
acknowledgeReloadConnectionLoss(tripwire, reloadWarnings)
await welcome.waitFor({ timeout: 15_000 })
expect(tripwire.warnings).toEqual([])
expect(tripwire.pageErrors).toEqual([])
}, 60_000)
})

View File

@@ -41,6 +41,7 @@ import {
loadOverlayPatches, loadOverlayPatches,
} from '@deepseek-ai/dsh-app-boot' } from '@deepseek-ai/dsh-app-boot'
import { dshHomePath } from '@deepseek-ai/dsh-home-paths' import { dshHomePath } from '@deepseek-ai/dsh-home-paths'
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
import { LlmAdapter } from '@deepseek-ai/dsh-llm' import { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { import type {
LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, StreamChunk,
@@ -62,6 +63,25 @@ import type {} from '@deepseek-ai/dsh-agent'
import { provideCmdline } from '@deepseek-ai/dsh-cmdline' import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
import { REPO_ROOT, requireDist } from './support.ts' import { REPO_ROOT, requireDist } from './support.ts'
// Host-side web e2e cannot import a browser package: doing so would pull that
// package's complete TS project into this graph. Mirrored from
// packages/client/ui-settings-models/src/onboarding-copy.ts; drift makes the
// default pre-acknowledgement stop suppressing the notice and fails loudly.
// import {
// WELCOME_NOTICE_ACK_FIELD, WELCOME_NOTICE_SETTINGS_NAMESPACE,
// WELCOME_NOTICE_VERSION, WELCOME_NOTICE_COPY,
// } from '@deepseek-ai/dsh-client-ui-settings-models'
export const WELCOME_NOTICE_SETTINGS_NAMESPACE = 'ui-onboarding'
export const WELCOME_NOTICE_ACK_FIELD = 'welcomeNoticeVersion'
export const WELCOME_NOTICE_VERSION = '2026-08-13.1'
export const WELCOME_NOTICE_COPY = {
zh: {
title: '内测声明',
body: 'DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。\n\n我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。',
continueLabel: '继续',
},
} as const
/** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */ /** Snapshot mode for the lane, from $DSH_SNAPSHOT (same vocabulary as the other snapshot suites). */
export type WebSnapshotMode = 'replay' | 'record' | 'refresh' export type WebSnapshotMode = 'replay' | 'record' | 'refresh'
@@ -213,6 +233,8 @@ export interface LaunchOptions {
* keyless first-run configuration lane; the default disables the adapter. * keyless first-run configuration lane; the default disables the adapter.
*/ */
deepSeekMissingCredential?: boolean deepSeekMissingCredential?: boolean
/** Leave the current welcome notice pending; ordinary scenarios pre-acknowledge it before browser boot. */
welcomeNoticePending?: boolean
/** /**
* Patch the shipped DeepSeek search row to a deterministic endpoint and * Patch the shipped DeepSeek search row to a deterministic endpoint and
* credential reference. Browser search scenarios keep the real provider and * credential reference. Browser search scenarios keep the real provider and
@@ -510,6 +532,11 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
}) })
await ctx.loader.await() await ctx.loader.await()
assertEntriesLoaded(ctx, 'web e2e scaffold') assertEntriesLoaded(ctx, 'web e2e scaffold')
if (options.welcomeNoticePending !== true) {
await ctx.settings.mutate(settingsNamespace(WELCOME_NOTICE_SETTINGS_NAMESPACE), [{
op: 'set', path: [WELCOME_NOTICE_ACK_FIELD], value: WELCOME_NOTICE_VERSION,
}])
}
const boundPort = ctx.get('webServer')?.port const boundPort = ctx.get('webServer')?.port
if (boundPort === undefined) { if (boundPort === undefined) {
throw new Error('web e2e scaffold: webServer service missing after settled boot') throw new Error('web e2e scaffold: webServer service missing after settled boot')

View File

@@ -2,6 +2,6 @@
- treeitem "Ungrouped" [expanded]: - treeitem "Ungrouped" [expanded]:
- img - img
- text: Ungrouped - text: Ungrouped
- treeitem "Use the read tool twice 1min"
- treeitem "Use the read tool twice (1) now"
- treeitem "Use the read tool twice (2) now" [selected] - treeitem "Use the read tool twice (2) now" [selected]
- treeitem "Use the read tool twice (1) now"
- treeitem "Use the read tool twice 1min"

View File

@@ -1,5 +1,8 @@
- region "添加一个 API Key 开始使用": - dialog "添加一个 API Key 开始使用":
- heading "添加一个 API Key 开始使用" [level=2] - heading "添加一个 API Key 开始使用" [level=2]
- paragraph: 配置 DeepSeek 官方模型,即可开始使用。 - paragraph: 配置 DeepSeek 官方模型,即可开始使用。
- text: API 密钥
- textbox "API 密钥":
- /placeholder: 输入 API 密钥
- button "稍后配置" - button "稍后配置"
- button "前往配置" - button "保存并继续" [disabled]

View File

@@ -0,0 +1,5 @@
- dialog "内测声明":
- heading "内测声明" [level=2]
- paragraph: DeepSeek Harness 目前的 0.1 版本仍处在面向 Harness 开发者进行测试的阶段,还有许多地方需要持续改进和打磨,希望听取广大开发者的反馈建议。预计 DeepSeek Harness 的核心插件以及基础 API 都会在接下来的一段时间内快速迭代、持续演化。
- paragraph: 我们期待与全球开发者一起,在开源、开放、可复用、可组合的基础设施之上,共同探索智能上限。欢迎全球 Harness 开发者加入 DSH 插件生态。
- button "继续"

View File

@@ -2,5 +2,5 @@
- treeitem "workspace" [expanded]: - treeitem "workspace" [expanded]:
- img - img
- text: workspace - text: workspace
- treeitem "New Session" [selected]
- treeitem "1 subagent running Delegate a background job. now" - treeitem "1 subagent running Delegate a background job. now"
- treeitem "New Session" [selected]

View File

@@ -2,5 +2,5 @@
- treeitem "workspace" [expanded]: - treeitem "workspace" [expanded]:
- img - img
- text: workspace - text: workspace
- treeitem "Ask a research subagent to now"
- treeitem "Explain event sourcing in one (1) now" [selected] - treeitem "Explain event sourcing in one (1) now" [selected]
- treeitem "Ask a research subagent to now"

File diff suppressed because one or more lines are too long

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write docs/user/develop/basic/publish.md # pnpm run verify-translation-pairing --write docs/user/develop/basic/publish.md
publish.md: 8283f9e7ff0c28580343975d67c5715d17c53074 publish.md: 9548fe2f8e4636dd160c79cd0d50f01f8bdfd3d1
publish.zh.md: 5a87901fe39aa00e94db81dc840e39a52e0cc88c publish.zh.md: 2cff05edcca55bac5fde9d2e7d24ed9c8b48a6f5

View File

@@ -4,6 +4,8 @@ English | [中文](publish.zh.md)
The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. It assumes the `dsh` CLI is installed. Complete [plugin configuration](./config.md) first. The previous tutorials loaded a local plugin through a `--patch` overlay. This tutorial packages it as an installable **bundle**, installs it into a **profile** with `dsh plugin add`, and explains the layer order that determines the composed configuration. It assumes the `dsh` CLI is installed. Complete [plugin configuration](./config.md) first.
To use a fresh source checkout instead, complete the [run-from-source section](../../../../README.md#run-from-source), keep this tutorial's `hello-plugin` directory at the repository root, and run the remaining `dsh ...` commands from there as `pnpm dsh ...`. See [source execution](../../../../apps/cli/reference/README.md#source-execution) for build and launcher behavior.
## Two concepts, two manifests ## Two concepts, two manifests
Installation is built on two concepts. Both are described by a `package.json`, but they carry different kinds of manifest under the `dsh` key, and they answer different questions: Installation is built on two concepts. Both are described by a `package.json`, but they carry different kinds of manifest under the `dsh` key, and they answer different questions:

View File

@@ -4,6 +4,8 @@
前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。本文假设 `dsh` CLI 已安装。请先完成[插件配置](./config.md)。 前几篇教程通过 `--patch` overlay 加载本地插件。本教程把它打包成可安装的**组合包**(bundle),用 `dsh plugin add` 安装进一个 **profile**,并解释决定组合后配置的层顺序。本文假设 `dsh` CLI 已安装。请先完成[插件配置](./config.md)。
如果改用全新的源码 checkout,请先按照[从源码运行章节](../../../../README.md#run-from-source)完成准备,将本教程的 `hello-plugin` 目录放在仓库根目录,并从该目录把下文的 `dsh ...` 命令改为 `pnpm dsh ...`。构建与启动器行为见[源码执行](../../../../apps/cli/reference/README.md#source-execution)。
## 两个概念,两种 manifest ## 两个概念,两种 manifest
安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同: 安装机制建立在两个概念之上。二者都由一份 `package.json` 描述,但它们在 `dsh` 键下携带的 manifest(元数据清单)种类不同,回答的问题也不同:

View File

@@ -1,6 +1,6 @@
{ {
"name": "@deepseek-ai/dsh-root", "name": "@deepseek-ai/dsh-root",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"license": "MIT", "license": "MIT",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -3,4 +3,4 @@
# after editing either side, bring the other along and re-record with: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/README.md # pnpm run verify-translation-pairing --write packages/README.md
README.md: 7904ec1fea8dbab361fcd90b2b32f1c743ce5537 README.md: 7904ec1fea8dbab361fcd90b2b32f1c743ce5537
README.zh.md: bac14113b656990cfda18a448034bfa25482c4fc README.zh.md: c39b65dad7e9c277779a85d7327982597628e007

View File

@@ -10,53 +10,53 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通
| 组 | 职责 | 发布预期 | | 组 | 职责 | 发布预期 |
|---|---|---| |---|---|---|
| [`core/`](core/README.md) | 产品 API 主干:会话、提示词、工具、agent(智能体)服务与具体循环 | 产品:稳定接口 | | [`core/`](core/README.md) | 产品 API 主干:会话、提示词、工具、agent(智能体)服务与具体循环 | 产品:稳定 API |
| [`api/`](api/README.md) | Remote BFF 装配与 Typert RPC Gateway | 产品:稳定接口 | | [`api/`](api/README.md) | Remote BFF 装配与 Typert RPC 网关 | 产品:稳定 API |
| [`typert/`](typert/README.md) | 类型图生成、产物加载与运行时注册表 | 产品:稳定接口 | | [`typert/`](typert/README.md) | 类型图生成、产物加载与运行时注册表 | 产品:稳定 API |
| [`goal/`](goal/README.md) | 同会话 goal 的持久化与生命周期 | 产品:稳定接口 | | [`goal/`](goal/README.md) | 同会话 goal 的持久化与生命周期 | 产品:稳定 API |
| [`schedule/`](schedule/README.md) | 仅限会话内的定时后续轮次 | 产品:稳定接口 | | [`schedule/`](schedule/README.md) | 仅限会话内的定时后续操作 | 产品:稳定 API |
| [`feedback/`](feedback/README.md) | 人类反馈 | 产品:稳定接口 | | [`feedback/`](feedback/README.md) | 人类反馈 | 产品:稳定 API |
| [`identity/`](identity/README.md) | 共享匿名身份 | 产品:稳定接口 | | [`identity/`](identity/README.md) | 共享匿名身份 | 产品:稳定 API |
| [`llm/`](llm/README.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 | 产品:稳定接口 | | [`llm/`](llm/README.md) | LLM(大语言模型)能力系列:抽象服务 + 提供方适配器 | 产品:稳定 API |
| [`e2b/`](e2b/README.md) | E2B 提供方 | POC | | [`e2b/`](e2b/README.md) | E2B 提供方 | POC |
| [`subprocess/`](subprocess/README.md) | 子进程能力系列:Service Definition + 本地进程树提供方 | 产品:稳定接口 | | [`subprocess/`](subprocess/README.md) | 子进程能力系列:Service Definition + 本地进程树提供方 | 产品:稳定 API |
| [`shell/`](shell/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定接口 | | [`shell/`](shell/README.md) | Bash 能力系列:执行器 seam、本地实现、面向模型的工具 | 产品:稳定 API |
| [`terminal/`](terminal/README.md) | 持久 PTY 能力系列:按所有者隔离的会话、本地实现和面向模型的工具 | 产品:稳定接口 | | [`terminal/`](terminal/README.md) | 持久 PTY 能力系列:限定所有者范围的会话、本地实现和面向模型的工具 | 产品:稳定 API |
| [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + Code Mode Consumer | 产品:稳定接口 | | [`code-runtime/`](code-runtime/README.md) | 代码执行能力系列:Service Definition + worker 线程提供方 + Code Mode Consumer | 产品:稳定 API |
| [`sandbox/`](sandbox/README.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 | 产品:稳定接口 | | [`sandbox/`](sandbox/README.md) | 进程限制 seam;bwrap/Landlock/Seatbelt 后端 | 产品:稳定 API |
| [`fs/`](fs/README.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、由 bash 支持的发现工具 | 产品:稳定接口 | | [`fs/`](fs/README.md) | 文件系统能力系列:seam、本地实现、面向模型的文件工具、由 bash 支持的发现工具 | 产品:稳定 API |
| [`lsp/`](lsp/README.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 | 产品:稳定接口 | | [`lsp/`](lsp/README.md) | LSP 能力系列:seam、通用 stdio 提供方和 `lsp` 工具 | 产品:稳定 API |
| [`skill/`](skill/README.md) | skill(技能)能力系列:提供方注册表、本地提供方和面向模型的目录/loader | 产品:稳定接口 | | [`skill/`](skill/README.md) | skill(技能)能力系列:提供方注册表、本地提供方和面向模型的目录/loader | 产品:稳定 API |
| [`compaction/`](compaction/README.md) | 压缩(compaction)能力系列:Service Definition + 基础提供方 + 命令 Consumer | 产品:稳定接口 | | [`compaction/`](compaction/README.md) | 压缩(compaction)能力系列:Service Definition + 基础提供方 + 命令 Consumer | 产品:稳定 API |
| [`context/`](context/README.md) | 模型可见请求上下文,包括 workspace 指令和时间上下文 | 产品:稳定接口 | | [`context/`](context/README.md) | 模型可见请求上下文,包括 workspace 指令和时间上下文 | 产品:稳定 API |
| [`subagent/`](subagent/README.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 | 产品:稳定接口 | | [`subagent/`](subagent/README.md) | subagent 能力系列:提供方注册表约定和面向模型的委托工具 | 产品:稳定 API |
| [`jobs/`](jobs/README.md) | 通用后台任务运行时和面向模型的 `job_*` 控制工具 | 产品:稳定接口 | | [`jobs/`](jobs/README.md) | 通用后台任务运行时和面向模型的 `job_*` 控制工具 | 产品:稳定 API |
| [`workflow/`](workflow/README.md) | 工作流 seam、worker 线程引擎和面向模型的 `workflow`/`ralph` 工具 | 产品:稳定接口 | | [`workflow/`](workflow/README.md) | 工作流 seam、worker 线程引擎和面向模型的 `workflow`/`ralph` 工具 | 产品:稳定 API |
| [`web/`](web/README.md) | Web 能力系列:seam、搜索/获取提供方实现和面向模型的 Web 工具 | 产品:稳定接口 | | [`web/`](web/README.md) | Web 能力系列:seam、搜索/获取提供方实现和面向模型的 Web 工具 | 产品:稳定 API |
| [`attachment/`](attachment/README.md) | 持久附件标识、校验、本地内容寻址存储 | 产品:稳定接口 | | [`attachment/`](attachment/README.md) | 持久附件标识、校验、本地内容寻址存储 | 产品:稳定 API |
| [`spill/`](spill/README.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 | 产品:稳定接口 | | [`spill/`](spill/README.md) | spill 能力系列:存储 seam、本地实现、工具结果 spill 策略 | 产品:稳定 API |
| [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定接口 | | [`todo/`](todo/README.md) | 面向模型的 `todo_write` 工具 | 产品:稳定 API |
| [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定接口 | | [`plan/`](plan/README.md) | Plan 协作状态,提供直接进入命令与经评审的退出 | 产品:稳定 API |
| [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定接口 | | [`preset/`](preset/README.md) | 由 preset `cordis.yml` 按会话组装 agent | 产品:稳定 API |
| [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定接口 | | [`guard/`](guard/README.md) | 循环卫生守卫:建议性重复调用提醒 + `tools/execute` 截止时间强制执行器 | 产品:稳定 API |
| [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定接口 | | [`bundle/`](bundle/README.md) | 可安装的 `dsh --profile` 补丁层 | 产品:稳定 API |
| [`extensions/`](extensions/README.md) | agent 运行时自修改:实时插件/服务检查和模型所写插件挂载/卸载([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | 产品:稳定接口 | | [`extensions/`](extensions/README.md) | agent 运行时自修改:实时插件/服务检查和模型所写插件挂载/卸载([设计](../.agents/notes/implemented/feature/2026-07-08-self-referential-cordis-toolset.md)) | 产品:稳定 API |
| [`hooks/`](hooks/README.md) | 钩子桥接 + 共享 Claude Code/Codex 协议格式库 | 产品:稳定接口 | | [`hooks/`](hooks/README.md) | 钩子桥接 + 共享的 Claude Code/Codex 线协议库 | 产品:稳定 API |
| [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、基于日志的标题、会话上报 | 产品:稳定接口 | | [`session/`](session/README.md) | 持久会话数据平面:持久化 seam + JSONL/SQLite 后端、投影 seam、基于日志的标题、会话上报 | 产品:稳定 API |
| [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定接口 | | [`session-query/`](session-query/README.md) | 会话检索系列:逻辑语料库、有界读取、血缘、事件关系、语义过滤和 SQLite 全文搜索 | 产品:稳定 API |
| [`settings/`](settings/README.md) | 用户设置 seam + 基于文件的提供方 | 产品:稳定接口 | | [`settings/`](settings/README.md) | 用户设置 seam + 基于文件的提供方 | 产品:稳定 API |
| [`credentials/`](credentials/README.md) | 凭据引用 seam + 环境变量优先于 `.env` 的提供方 | 产品:稳定接口 | | [`credentials/`](credentials/README.md) | 凭据引用 seam + 环境变量优先于 `.env` 的提供方 | 产品:稳定 API |
| [`storage/`](storage/README.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定接口 | | [`storage/`](storage/README.md) | 非会话存储中枢 + 后端 + 领域形式 | 产品:稳定 API |
| [`workspace/`](workspace/README.md) | Workspace 实体 | 产品:稳定接口 | | [`workspace/`](workspace/README.md) | Workspace 实体 | 产品:稳定 API |
| [`sdk/`](sdk/README.md) | 进程外运行时 SDK:JSON-RPC 协议、TypeScript 客户端和服务器插件 | 产品:稳定接口 | | [`sdk/`](sdk/README.md) | 进程外运行时 SDK:JSON-RPC 协议、TypeScript 客户端和服务器插件 | 产品:稳定 API |
| [`acp/`](acp/README.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 | 产品:稳定接口 | | [`acp/`](acp/README.md) | 仅面向自动化的 ACP(Agent Client Protocol)服务器 | 产品:稳定 API |
| [`interaction/`](interaction/README.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 | 产品:稳定接口 | | [`interaction/`](interaction/README.md) | 人机协作平面:批准/交互 seam、权限预设、命令、询问用户的工具 | 产品:稳定 API |
| [`boot/`](boot/README.md) | 共享的 app bin 启动粘合层 | 产品:稳定接口 | | [`boot/`](boot/README.md) | 共享的 app bin 启动粘合层 | 产品:稳定 API |
| [`host/`](host/README.md) | web GUI 宿主半侧:API 网关 + HTTP 路由服务器 | 产品:稳定接口 | | [`host/`](host/README.md) | web GUI 宿主半侧:API 网关 + HTTP 路由服务器 | 产品:稳定 API |
| [`client/`](client/README.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 | 产品:稳定接口 | | [`client/`](client/README.md) | web GUI 浏览器半侧:shell、协议层、对象服务、slot、`ui-*` 插件 | 产品:稳定 API |
| [`examples/`](examples/README.md) | 演示组合包(agent-spine + CLI(命令行界面)/ACP/JSON-RPC bin),由叶节点加载 | 支持:示例基础设施 | | [`examples/`](examples/README.md) | 演示组合包(agent-spine + CLI(命令行界面)/ACP/JSON-RPC bin),由叶节点加载 | 支持:示例基础设施 |
| [`test-support/`](test-support/README.md) | 支持基础设施(testkit、不变式、回放、Loader 冒烟测试) | 支持:兼容性预期较低 | | [`test-support/`](test-support/README.md) | 支持基础设施(testkit、不变式、回放、Loader 冒烟测试) | 支持:兼容性预期较低 |
| [`util/`](util/README.md) | 组间共享的低层零依赖工具(`Branded<B>`、Harness home/路径辅助函数、超时、保留策略) | 支持:小型、稳定、无 harness 依赖 | | [`util/`](util/README.md) | 组间共享的低层零依赖工具(`Branded<B>`、Harness home/路径辅助函数、超时、留存) | 支持:小型、稳定、无 harness 依赖 |
新包加入现有组;新组更新其 README 和此表。 新包加入现有组;新组更新其 README 和此表。
@@ -64,6 +64,6 @@ npm scope 为 `@deepseek-ai/dsh-*`;Cordis `Service` 子类和函数插件通
依赖图由工具生成:[docs/module-graph.md](../docs/module-graph.md)(`pnpm run gen-module-graph`,CI 中有新鲜度门禁)。 依赖图由工具生成:[docs/module-graph.md](../docs/module-graph.md)(`pnpm run gen-module-graph`,CI 中有新鲜度门禁)。
**扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。包括 `dsh-agent-spine-demo` 在内的组合包可以依赖主干插件。能力在 Service Definition/Service Provider/Consumer 角色需要独立演进时将其分离;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)。 **扩展插件依赖 Service Definition,绝不依赖具体提供方。** `dsh-agent-loop` 可替换;UI、钩子和工具插件使用 `dsh-agent`。包括 `dsh-agent-spine-demo` 在内的组合包可以依赖主干插件。能力会将需要独立演进的 Service Definition/Service Provider/Consumer 角色分离;详见[能力 seam](../.agents/notes/implemented/architecture/2026-06-13-capability-seams.md)。
包 README 覆盖用途、API、扩展点和[模型体验](../docs/cookbook/adding-a-package.md#4-write-the-package-readme);列入模型无关[省略允许清单](../scripts/verify-package-readme-model-experience.ts)的包除外。它们还要包含 `## Known Limitations and Deferred Work`,或列入其[允许清单](../scripts/verify-package-readme-limitations.ts)。 包 README 覆盖用途、API、扩展点和[模型体验](../docs/cookbook/adding-a-package.md#4-write-the-package-readme);列入模型无关[省略允许清单](../scripts/verify-package-readme-model-experience.ts)的包除外。它们还要包含 `## Known Limitations and Deferred Work`,或列入其[允许清单](../scripts/verify-package-readme-limitations.ts)。

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-acp", "name": "@deepseek-ai/dsh-acp",
"description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio", "description": "Automation-only Agent Client Protocol server for driving DeepSeek Harness agents over JSON-RPC stdio",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-api-gateway", "name": "@deepseek-ai/dsh-api-gateway",
"description": "Typert Remote Host dispatcher and Client API endpoint", "description": "Typert Remote Host dispatcher and Client API endpoint",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-api-remotes", "name": "@deepseek-ai/dsh-api-remotes",
"description": "Remote BFF assembly and Host Agent/Session lookup policy", "description": "Remote BFF assembly and Host Agent/Session lookup policy",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-attachment-local", "name": "@deepseek-ai/dsh-attachment-local",
"description": "Private content-addressed DSH_HOME attachment storage", "description": "Private content-addressed DSH_HOME attachment storage",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-attachment", "name": "@deepseek-ai/dsh-attachment",
"description": "Durable immutable attachment storage seam for the DeepSeek Harness", "description": "Durable immutable attachment storage seam for the DeepSeek Harness",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-app-boot", "name": "@deepseek-ai/dsh-app-boot",
"description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence", "description": "Shared boot glue for the app bins: .env loading, fail-loud Loader guards, snapshot-aware config resolution, and the Loader boot sequence",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-cmdline", "name": "@deepseek-ai/dsh-cmdline",
"description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs", "description": "Immutable command-line handoff from a dsh launcher to any app plugin that injects cmdlineArgs",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-base", "name": "@deepseek-ai/dsh-base",
"description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root", "description": "The shared dsh core as a profile bundle: every profile's first patch layer, inserting the base plugin rows over the empty profile root",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-headless", "name": "@deepseek-ai/dsh-headless",
"description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer", "description": "The dsh one-shot bundle: a direct core Agent/Session runner over dsh-base with no Host, HTTP, or browser layer",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md # pnpm run verify-translation-pairing --write packages/bundle/web-app/README.md
README.md: 3f7fbe7bbbeba2ee753ab2dfa8c945e798dd6a02 README.md: 90d1566b5a7a25f6a827079c4a8ab776e05cc7a3
README.zh.md: 357ca9946ed7fef1bf4c691c6666156bb293b5e0 README.zh.md: b7156bcab66b53964bb2f0e1c87d9e9a806e3f75

View File

@@ -2,7 +2,7 @@
English | [中文](README.zh.md) English | [中文](README.zh.md)
The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. Flag-configured rows inject that service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle. The dsh browser-surface bundle. [`cordis.patch.yml`](cordis.patch.yml) rides over [`dsh-base`](../base/README.md): it sets the coding persona, inserts the Web host rows (webserver, API gateway, workspace, projection cache, storage) and the browser plugin roster, the always-on client-plugin reload chain ([`dsh-client-hmr`](../../client/hmr/README.md), idle until a rebuild watcher rewrites client bundles), and mounts this package's `web-runtime` glue plugin (config `{printUrl, surfaceContext, trustedHosts}`). That plugin resolves the built frontend dist through `@deepseek-ai/dsh-web-frontend`'s exports, samples bind-dependent LAN trust once, provides it as `webRuntime` to the browser-trust fence and client roster, mounts the [`frontend-static`](../../host/frontend-static/README.md) fallback owner, registers the harness-source and web-surface prompt sections plus the bash-visible `DSH_WEB_URL` runtime variable when `surfaceContext` is true, and prints the `dsh web:` URL line when `printUrl` is true, after its Loader tree settles so a sibling failure cannot announce a dead app. This bundle also owns the app command line: the ordinary `web-startup` provider ([`src/startup.ts`](src/startup.ts)) injects `ctx.cmdlineArgs` ([`dsh-cmdline`](../../boot/cmdline/README.md)), parses `--host`, `--port`, repeatable `--trusted-host`, and the app's `--help`, then provides `webStartup`. It rejects `--host 0.0.0.0` before publishing that service because the CLI intentionally does not support all-interfaces binding yet. Flag-configured rows inject the service and read it directly from lazy config, so nothing binds a port before argument resolution and `dsh --profile web --help` starts no server. [`dsh-headless`](../headless/README.md) is a sibling surface over the same base and does not mount this bundle.
## Model Experience ## Model Experience

View File

@@ -2,7 +2,7 @@
[English](README.md) | 中文 [English](README.md) | 中文
dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host`、`--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。 dsh 浏览器表层组合包。[`cordis.patch.yml`](cordis.patch.yml) 叠加在 [`dsh-base`](../base/README.md) 之上:设置 coding persona,插入 Web 宿主行(webserver、API 网关、workspace、投影缓存、存储)、浏览器插件名录与始终挂载的客户端插件重载链([`dsh-client-hmr`](../../client/hmr/README.md),在重建 watcher 改写客户端 bundle 之前保持空闲),并挂载本包的 `web-runtime` 粘合插件(配置为 `{printUrl, surfaceContext, trustedHosts}`)。该插件通过 `@deepseek-ai/dsh-web-frontend` 的 exports 解析已构建的前端 dist,只采样一次依赖 bind 的 LAN 信任信息并将其作为 `webRuntime` 提供给浏览器信任栅栏和客户端名录,挂载 [`frontend-static`](../../host/frontend-static/README.md) 回退席位所有者,在 `surfaceContext` 为 true 时注册 Harness 源码与 Web 表层提示词段落,以及 bash 可见的 `DSH_WEB_URL` 运行时变量,并在 `printUrl` 为 true 时等自身的 Loader 配置树结算后再打印 `dsh web:` URL 行,避免兄弟行失败时公告一个已失效的应用。本组合包还持有应用命令行:普通 `web-startup` 提供方([`src/startup.ts`](src/startup.ts))注入 `ctx.cmdlineArgs`([`dsh-cmdline`](../../boot/cmdline/README.md)),解析 `--host`、`--port`、可重复的 `--trusted-host` 以及应用自己的 `--help`,再提供 `webStartup`。它会在发布该服务前拒绝 `--host 0.0.0.0`,因为 CLI 目前有意不支持绑定所有网络接口。由 flag 配置的行会注入该服务,并在惰性配置中直接读取它,因此参数解析完成前不会有任何东西绑定端口,`dsh --profile web --help` 也不会启动服务器。[`dsh-headless`](../headless/README.md) 是同一 base 之上的同级表层,不挂载本组合包。
## 模型体验 ## 模型体验

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-web-app", "name": "@deepseek-ai/dsh-web-app",
"description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)", "description": "The dsh browser-surface bundle: the web patch layer over dsh-base plus the runtime glue plugin (frontend dist serving, web-surface prompt, bash runtime variables, URL line)",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -45,28 +45,30 @@ function webCommand(): Command {
.name('dsh --profile web') .name('dsh --profile web')
.description('Serve the DeepSeek Harness browser UI.') .description('Serve the DeepSeek Harness browser UI.')
.helpOption('-h, --help', 'show this help') .helpOption('-h, --help', 'show this help')
.option('--host <host>', 'bind host; pass 0.0.0.0 to reach it from another machine') .option('--host <host>', 'bind host')
.option('--port <port>', 'listen port; pass 0 to let the OS pick a free one') .option('--port <port>', 'listen port; pass 0 to let the OS pick a free one')
.option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)') .option('--trusted-host <authority...>', 'extra authority the /api browser-trust fence accepts (host or host:port; repeatable)')
.addHelpText('after', ` .addHelpText('after', `
Examples: Examples:
dsh --profile web serve on the composed host and port dsh --profile web serve on the composed host and port
dsh --profile web --port 8080 serve on another port dsh --profile web --port 8080 serve on another port
dsh --profile web --host 0.0.0.0 reach it from another machine on the LAN
`) `)
} }
/** /**
* Parse and provide the Web invocation as an ordinary Cordis service. The * Parse and provide the Web invocation as an ordinary Cordis service. The
* command's action publishes the flags this invocation named; a non-numeric * command's action publishes the flags this invocation named; `--host 0.0.0.0`
* `--port` is a usage error, so on rejection (and on `--help`) nothing is * or a non-numeric `--port` is a usage error, so on rejection (and on `--help`)
* provided. * nothing is provided.
* @param ctx - plugin context carrying the command line. * @param ctx - plugin context carrying the command line.
*/ */
export function apply(ctx: Context): void { export function apply(ctx: Context): void {
const program = webCommand() const program = webCommand()
program.action(() => { program.action(() => {
const options = program.opts<WebOptions>() const options = program.opts<WebOptions>()
if (options.host === '0.0.0.0') {
program.error('error: --host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
}
if (options.port !== undefined && !/^\d+$/.test(options.port)) { if (options.port !== undefined && !/^\d+$/.test(options.port)) {
program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`) program.error(`error: --port must be a number, got ${JSON.stringify(options.port)}`)
} }

View File

@@ -88,13 +88,13 @@ export const apply = ctx => globalThis.__webStartupApply(ctx)
describe('web command-line provider', () => { describe('web command-line provider', () => {
it('publishes each flag and releases direct service expressions', async () => { it('publishes each flag and releases direct service expressions', async () => {
const { values, observed } = await bootProvider([ const { values, observed } = await bootProvider([
'--host', '0.0.0.0', '--host', '127.0.0.1',
'--port', '8080', '--port', '8080',
'--trusted-host', 'lab.internal', 'lab-2.internal', '--trusted-host', 'lab.internal', 'lab-2.internal',
'--trusted-host', '10.0.0.9', '--trusted-host', '10.0.0.9',
]) ])
expect(values).toEqual({ expect(values).toEqual({
host: '0.0.0.0', host: '127.0.0.1',
port: 8080, port: 8080,
trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'], trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'],
}) })
@@ -128,4 +128,12 @@ describe('web command-line provider', () => {
expect(observed.readerConfig).toBeUndefined() expect(observed.readerConfig).toBeUndefined()
expect(observed.exits).toEqual([1]) expect(observed.exits).toEqual([1])
}) })
it('rejects the intentionally unsupported all-interfaces host before the consumer activates', async () => {
const { values, observed } = await bootProvider(['--host', '0.0.0.0'])
expect(observed.out).toContain('--host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
expect(values).toBeUndefined()
expect(observed.readerConfig).toBeUndefined()
expect(observed.exits).toEqual([1])
})
}) })

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/connection/README.md # pnpm run verify-translation-pairing --write packages/client/connection/README.md
README.md: 3315552dd0400697d5a96639598c5f5bb2bb4f5b README.md: f0a707cc5f3a962c7852f7323c727d0a39a57b10
README.zh.md: 0dc251f1e5410bd0d74ef450c6d8df3dc9cdab81 README.zh.md: b529eebf9af93e36b6b92c19964678b7f5a04ea1

View File

@@ -6,7 +6,7 @@ Wire consumer layer: the client plugin's apply mounts `ctx.connection` (shared a
## /api browser-trust fence ## /api browser-trust fence
The node half guards every entry under `/api` before bridging or upgrading (`src/api-request-trust.ts`). Every request — browser-marked or not — must present a `Host` that is a loopback authority or matches a `trustedHosts` entry: exact on `host:port` entries, any port on port-less entries, both sides compared through WHATWG normalization (DNS-rebinding defense). There is deliberately no shortcut for unmarked HTTP requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to image and navigation reads, so an unmarked request may still be a rebound browser read with a readable response, and Host is the one header rebinding cannot forge; a browser WebSocket handshake carries `Origin` and passes the same comparison. Non-browser clients pass the same fence via loopback, the CLI-derived LAN IP literals, or a declared authority. When markers are present, an attached `Origin` must equal the Host authority, and an explicit `sec-fetch-site: cross-site` marker is refused. A `trustedHosts` entry that is not a bare, canonical `host[:port]` authority — one WHATWG parsing reads back exactly as written — fails the plugin load loudly: parsing would otherwise quietly authorize the hostname inside `harness.internal/path`, or broaden a dangling-colon or zero-padded port to an any-port grant. HTTP failures answer plain 403 before any RPC dispatch; upgrade failures reject the handshake before any event stream starts. A non-loopback (`--host 0.0.0.0`) deployment therefore needs its serving authorities trusted: the dsh CLI derives the machine's LAN IP literals itself and its `--trusted-host` flag declares named ones, so `trustedHosts` in cordis.yml is for compositions the CLI does not boot. The fence is a reachability policy, not authentication; the Web carrier provides no authentication layer. Decision record: [the api browser-trust boundary Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md). The node half guards every entry under `/api` before bridging or upgrading (`src/api-request-trust.ts`). Every request — browser-marked or not — must present a `Host` that is a loopback authority or matches a `trustedHosts` entry: exact on `host:port` entries, any port on port-less entries, both sides compared through WHATWG normalization (DNS-rebinding defense). There is deliberately no shortcut for unmarked HTTP requests: over plain HTTP a browser attaches neither `Origin` nor Fetch-Metadata to image and navigation reads, so an unmarked request may still be a rebound browser read with a readable response, and Host is the one header rebinding cannot forge; a browser WebSocket handshake carries `Origin` and passes the same comparison. Non-browser clients pass the same fence via loopback, deployment-derived LAN IP literals, or a declared authority. When markers are present, an attached `Origin` must equal the Host authority, and an explicit `sec-fetch-site: cross-site` marker is refused. A `trustedHosts` entry that is not a bare, canonical `host[:port]` authority — one WHATWG parsing reads back exactly as written — fails the plugin load loudly: parsing would otherwise quietly authorize the hostname inside `harness.internal/path`, or broaden a dangling-colon or zero-padded port to an any-port grant. HTTP failures answer plain 403 before any RPC dispatch; upgrade failures reject the handshake before any event stream starts. Non-loopback compositions must trust their serving authorities explicitly: the Web runtime derives LAN IP literals from an all-interfaces server config, while `trustedHosts` in cordis.yml and the CLI's `--trusted-host` flag declare named authorities. `dsh web --host 0.0.0.0` is intentionally unsupported until remote access has an authentication layer. The fence is a reachability policy, not authentication; the Web carrier provides no authentication layer. Decision record: [the api browser-trust boundary Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md).
## `/api` WebSocket downlinks ## `/api` WebSocket downlinks

View File

@@ -6,7 +6,7 @@
## /api 浏览器信任栅栏 ## /api 浏览器信任栅栏
node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、CLI(命令行界面)推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。因此非回环(`--host 0.0.0.0`)部署需要让自己的服务权威被信任:dsh CLI 会自行推导本机的 LAN IP 字面量,其 `--trusted-host` flag 用于声明具名权威,所以 cordis.yml 中的 `trustedHosts` 面向 CLI 不参与引导的组合。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md)。 node 半侧在桥接或 upgrade 前守卫 `/api` 下的每个入口(`src/api-request-trust.ts`)。每个请求——无论是否带浏览器标记——`Host` 都必须是回环地址权威,或与某个 `trustedHosts` 条目匹配:带端口的 `host:port` 条目精确匹配,不带端口的条目匹配任意端口,两侧均经 WHATWG 归一化后比较(DNS rebinding 防御)。刻意不为无浏览器标记的 HTTP 请求开捷径:明文 HTTP 下浏览器的图片与导航读取既不带 `Origin` 也不带 Fetch-Metadata,因此无标记请求仍可能是被重绑页面发起的、响应可被读走的读取,而 Host 是重绑唯一伪造不了的请求头;WebSocket 浏览器握手会带 `Origin` 并通过同一道比较。非浏览器客户端经由回环地址、部署推导的 LAN IP 字面量或已声明的权威通过同一道栅栏。当标记存在时,如附带 `Origin`,则它必须与 Host 权威完全一致;显式的 `sec-fetch-site: cross-site` 标记一律拒绝。不是纯的、规范形 `host[:port]` 权威的 `trustedHosts` 条目——即 WHATWG 解析读回后与原文不完全一致的——会让插件加载明确报错:否则解析会悄悄授权 `harness.internal/path` 这类笔误里的 hostname,或把悬空冒号、补零端口放大成任意端口授权。HTTP 失败在任何 RPC 分发之前以纯 403 应答,upgrade 失败在启动任何事件流前拒绝握手。非回环组合必须显式信任其服务权威:Web 运行时从全接口服务器配置推导 LAN IP 字面量,cordis.yml 中的 `trustedHosts` 与 CLI(命令行界面)的 `--trusted-host` flag 则声明具名权威。`dsh web --host 0.0.0.0` 在远程访问具备认证层之前有意不受支持。这道栅栏是可达性策略,而不是认证;Web 载体不提供认证层。决策记录:[api 浏览器信任边界 Agent Note](../../../.agents/notes/implemented/architecture/2026-07-28-api-browser-trust-boundary.md)。
## `/api` WebSocket 下行 ## `/api` WebSocket 下行

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-connection", "name": "@deepseek-ai/dsh-client-connection",
"description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api", "description": "Wire consumer layer: HTTP-up/WebSocket-down client, ConnectionController dual streams with reconnect, and fixture api",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -7,8 +7,8 @@
* nor Fetch-Metadata to reads (images and navigations — those * nor Fetch-Metadata to reads (images and navigations — those
* headers go only to trustworthy destinations), so an unmarked request may * headers go only to trustworthy destinations), so an unmarked request may
* still be a rebound browser read and Host is the one header rebinding cannot * still be a rebound browser read and Host is the one header rebinding cannot
* forge. Non-browser and remote clients pass the same fence via loopback, the * forge. Non-browser and remote clients pass the same fence via loopback,
* CLI-derived LAN IP literals, or a declared `trustedHosts` authority. * deployment-derived LAN IP literals, or a declared `trustedHosts` authority.
* Network reachability and authentication stay out of scope: binding policy * Network reachability and authentication stay out of scope: binding policy
* belongs to the webserver config, and this fence is not an auth layer. * belongs to the webserver config, and this fence is not an auth layer.
*/ */

View File

@@ -199,8 +199,8 @@ describe('connection node half', () => {
const loopback = fakeResponse() const loopback = fakeResponse()
await routes[0]!.handler(fakeRequest({ host: '127.0.0.1:3080' }), loopback.response) await routes[0]!.handler(fakeRequest({ host: '127.0.0.1:3080' }), loopback.response)
expect(loopback.state.status).toBe(404) expect(loopback.state.status).toBe(404)
// LAN authority declared as a port-less IP literal — the shape the CLI // An all-interfaces composition derives port-less LAN IP literals, which
// derives for `--host 0.0.0.0` — passes markerless curl on any port. // pass markerless curl on any port.
const lan = fakeResponse() const lan = fakeResponse()
await routes[0]!.handler(fakeRequest({ host: '192.168.1.5:3080' }), lan.response) await routes[0]!.handler(fakeRequest({ host: '192.168.1.5:3080' }), lan.response)
expect(lan.state.status).toBe(404) expect(lan.state.status).toBe(404)

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-hmr", "name": "@deepseek-ai/dsh-client-hmr",
"description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry", "description": "Dev-only hot-reload driver for script-loaded client entries: SSE rebuilt frames → invalidate/prefetch → fiber swap through the vendored Loader entry",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-locale", "name": "@deepseek-ai/dsh-client-locale",
"description": "Locale plugin: Host-backed zh/en preference, browser-derived fallback, locale snapshots, and typed namespace dictionaries", "description": "Locale plugin: Host-backed zh/en preference, browser-derived fallback, locale snapshots, and typed namespace dictionaries",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-modules", "name": "@deepseek-ai/dsh-client-modules",
"description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam", "description": "Client module system, dual-face: node half composes the __DSH_BOOT__ entry graph (incremental dsh.client scan, bundle route, index tap, webPlugins service); browser half is the lazy-CJS module table the vendored cordis Loader consumes as its internal seam",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-runtime", "name": "@deepseek-ai/dsh-client-runtime",
"description": "Client core services: SlotRegistry, SessionRuntime (scope tree + object layer)", "description": "Client core services: SlotRegistry, SessionRuntime (scope tree + object layer)",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-schema-form", "name": "@deepseek-ai/dsh-client-schema-form",
"description": "Schema/draft model layer for settings editors: rehydrates a serialized schemastery schema, validates drafts, and edits them immutably by path", "description": "Schema/draft model layer for settings editors: rehydrates a serialized schemastery schema, validates drafts, and edits them immutably by path",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-agent-preset", "name": "@deepseek-ai/dsh-client-ui-agent-preset",
"description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor", "description": "Agent-preset surfaces: the default for later sessions, this session's seat, and the composition editor",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-attachment", "name": "@deepseek-ai/dsh-client-ui-attachment",
"description": "Pure React attachment atoms for the dsh web UI: draft-image rail, message image gallery, and original-image lightbox (zero cordis)", "description": "Pure React attachment atoms for the dsh web UI: draft-image rail, message image gallery, and original-image lightbox (zero cordis)",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-commands", "name": "@deepseek-ai/dsh-client-ui-commands",
"description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry", "description": "Client command surface: global directory cache, '/' source, three command UI kinds, popupSelect registry",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-conversation", "name": "@deepseek-ai/dsh-client-ui-conversation",
"description": "Conversation domain: skeleton, ordered chat flow, composer with the Host-backed busy-Enter preference, and details host", "description": "Conversation domain: skeleton, ordered chat flow, composer with the Host-backed busy-Enter preference, and details host",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-deliverables", "name": "@deepseek-ai/dsh-client-ui-deliverables",
"description": "Produced-files turn tail and clickable final-response file references for Web", "description": "Produced-files turn tail and clickable final-response file references for Web",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-directory-picker-browse", "name": "@deepseek-ai/dsh-client-ui-directory-picker-browse",
"description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives", "description": "In-app directory browsing surface: the workspace directory-flow owner rendering the host's listing and creation primitives",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-directory-picker-native", "name": "@deepseek-ai/dsh-client-ui-directory-picker-native",
"description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser", "description": "Native directory-picker surface: the renderless workspace directory-flow occupant driving the host's OS chooser",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-goal", "name": "@deepseek-ai/dsh-client-ui-goal",
"description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection", "description": "Session goal surface: GoalBar docked above the composer, read from the goal session projection",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-input-trigger", "name": "@deepseek-ai/dsh-client-ui-input-trigger",
"description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources", "description": "Input trigger pipeline: '/' and '@' detection, candidate menu, pick routing to registered sources",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-jobs", "name": "@deepseek-ai/dsh-client-ui-jobs",
"description": "Session-header background-job list: live registry state mirrored from session/jobs frames", "description": "Session-header background-job list: live registry state mirrored from session/jobs frames",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"type": "module", "type": "module",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/types/index.d.ts", "types": "lib/types/index.d.ts",

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-layout", "name": "@deepseek-ai/dsh-client-ui-layout",
"description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)", "description": "Shell plugin: three-column AppFrame with drag handles, ctx.layout viewing-state service (navigation + panels)",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-message-feedback", "name": "@deepseek-ai/dsh-client-ui-message-feedback",
"description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote", "description": "Per-message feedback controls contributed to the assistant-message action strip, backed by the messageFeedback Host Remote",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-model-selection", "name": "@deepseek-ai/dsh-client-ui-model-selection",
"description": "Model selection: the /model popupSelect over session.models / session.selectModel", "description": "Model selection: the /model popupSelect over session.models / session.selectModel",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-permission-presets", "name": "@deepseek-ai/dsh-client-ui-permission-presets",
"description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection", "description": "Permission surfaces: a new-session default in General settings and a current-session /permission popup over the permissions projection",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-plan", "name": "@deepseek-ai/dsh-client-ui-plan",
"description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel", "description": "Plan-mode composer control: the conversation.input.plan seat over the plan projection and the /plan command channel",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-primitives", "name": "@deepseek-ai/dsh-client-ui-primitives",
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)", "description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority; # 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: # after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md # pnpm run verify-translation-pairing --write packages/client/ui-settings-general/README.md
README.md: 6669442f322ee38f14fff45e4344b59dffb3d61a README.md: c59b77617cfd5848553ba340e97bafe37b7b2a2f
README.zh.md: 53d006dd521fbc69e83c91914418bf98f0342bf9 README.zh.md: ea4896284b677a7b430cceb949b53aa6e3a5a242

View File

@@ -2,13 +2,13 @@
English | [中文](README.zh.md) English | [中文](README.zh.md)
Settings shell, ownerless-copy, and product-onboarding plugin. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted page at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, and the `settings` dictionaries. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages. Settings shell, ownerless copy, and durable product-onboarding namespace. It occupies `sidebar.settings` with the trigger chrome and modal settings panel, projects the `settings.section` ledger into the navigation and the `settings.onboarding` ledger into one mounted step at a time, and registers everything on the Settings pages that belongs to no single feature — the trigger/header/close chrome content, the local configuration-file action, the General section and its `settings.general.item` slot, and the `settings` dictionaries. The slot types it renders into belong to ui-settings, the settings domain base; only the shell's own contract types live here, because they reference ui-sidebar's slot type and the base layer must depend on no `ui-*` package. Feature-owned rows (Permission, Language, Appearance), sections (Models), and conditional onboarding steps stay with their feature packages.
The shell ships no copy of its own — all text arrives from registrants. Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency). The onboarding ledger projects in ascending order and mounts exactly one page at a time; the takeover chrome (body-level stage, mask, app-root `inert`) belongs to the step itself through ui-primitives' `OnboardingSurface`, so a mounted step still resolving its private facts renders null and neither paints nor blocks anything — the shell shows no empty stage while a step decides. The active registrant receives its id, `complete()`, and an `openSection(id)` callback; completing or skipping transfers ownership to the next entry. Registrants own durable completion, capability readiness, copy, mutations, and the surface wrap, so independently registered flows cannot stack and the shell does not become a second configuration fact source. The shell ships no onboarding copy of its own — all text arrives from registrants. Nav labels may be locale-following thunks, so the nav projection resolves them through `resolveSlotLabel` and re-renders on the section ledger bump or the locale revision (an optional `ctx.get('locale')` read; no hard locale dependency). The onboarding ledger projects in ascending order and mounts exactly one step at a time. Visible steps own their dialog chrome and app-root `inert` lifecycle; a mounted step still resolving private facts renders null, so nothing paints or blocks while it decides. The active registrant receives its id, `complete()`, and an `openSection(id)` callback; completing or skipping transfers ownership to the next entry. Registrants own durable completion, capability readiness, copy, mutations, and their visible wrapper, so independently registered flows cannot stack and the shell does not become a second configuration fact source.
A loopback browser loads the provider's `hasDocument` capability through `settings.describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action sends the pathless, loopback-only `settings.openDocument` request; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Remote browsers never register the action and never issue the privileged settings read. A loopback browser loads the provider's `hasDocument` capability through `settings.describe` and renders **Open configuration file** only when the Host confirms that a provider-owned local document can be prepared. The action sends the pathless, loopback-only `settings.openDocument` request; the Host resolves the provider path again, materializes an absent document, and hands it to a native text editor (`open -t` on macOS, bypassing a browser file association; the desktop file association on Linux and Windows; Windows association after `wslpath -w` translation on WSL). Open failures keep the action available and render a localized error. Reopening the dialog or reconnecting refreshes availability after a transient read failure or Host topology change. Remote browsers never register the action and never issue the privileged settings read.
The Host half registers `ui-onboarding` in the user-settings seam. Its `welcomeNoticeVersion` field keeps acknowledgements already present in stored settings documents valid and has no reader; no shipped step writes it. The Host half registers `ui-onboarding` in the user-settings seam. The welcome step contributed by `ui-settings-models` reads and writes its `welcomeNoticeVersion` through the existing public settings boundary; the shell itself remains policy-free.
## Model Experience ## Model Experience

View File

@@ -2,13 +2,13 @@
[English](README.md) | 中文 [English](README.md) | 中文
设置外壳、无特定功能归属文案与产品引导插件。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一页的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。 设置外壳、无特定功能归属文案与持久化产品引导 namespace。它以触发控件和模态设置面板占用 `sidebar.settings`,把 `settings.section` 账本投影成导航、把 `settings.onboarding` 账本投影成每次只挂载一个步骤的引导流程,并在设置页面上注册所有不属于单一功能的内容:触发器、标题栏与关闭控件内容、本地配置文件操作,「通用」分区及其 `settings.general.item` slot,以及 `settings` 字典。它渲染进的那些 slot 类型归 ui-settings——设置领域底座——所有;只有外壳自身的契约类型放在这里,因为它们引用 ui-sidebar 的 slot 类型,而底座不得依赖任何 `ui-*` 包。归具体功能所有的行(「权限」、「语言」、「外观」)、分区(「模型」)和条件式首次使用引导步骤仍由各自的功能包提供。
外壳不自带文案:所有文本都来自注册方。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。首次使用引导记录按升序投影,每次只挂载一个页面;接管界面框架(body 层级的展示层、遮罩、应用根节点 `inert`)经 ui-primitives 的 `OnboardingSurface` 由步骤自身持有,因此已挂载但仍在判定私有事实的步骤渲染 null 时不绘制也不阻塞任何内容——步骤判定期间外壳不会露出空白展示层。当前注册方会收到该条目的 id、`complete()` 和 `openSection(id)` 回调;完成或跳过当前页面后,所有权转交给下一项。持久化完成状态、能力就绪状态、文案、变更操作以及展示层包装均由注册方持有,因此独立注册的流程无法堆叠,外壳也不会成为第二个配置事实来源。 外壳不自带引导文案:所有文本都来自注册方。导航 label 可以是跟随语言的 thunk,因此导航投影经 `resolveSlotLabel` 解析,并在分区账本更新或 locale revision 变化时重新渲染(`ctx.get('locale')` 可选读取,无硬 locale 依赖)。首次使用引导记录按升序投影,每次只挂载一个步骤;可见步骤自行持有弹窗框架和应用根节点 `inert` 生命周期。已挂载但仍在判定私有事实的步骤渲染 null,因此判定期间不绘制也不阻塞任何内容。当前注册方会收到该条目的 id、`complete()` 和 `openSection(id)` 回调;完成或跳过当前步骤后,所有权转交给下一项。持久化完成状态、能力就绪状态、文案、变更操作以及可见包装均由注册方持有,因此独立注册的流程无法堆叠,外壳也不会成为第二个配置事实来源。
回环浏览器通过 `settings.describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染**打开配置文件**。该操作发送无路径参数且仅限回环访问的 `settings.openDocument` 请求;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。远程浏览器从不注册该操作,也从不发起这项特权设置读取。 回环浏览器通过 `settings.describe` 加载提供方的 `hasDocument` 能力,且只有在 Host 确认可准备好一份由提供方持有的本地文档时才渲染**打开配置文件**。该操作发送无路径参数且仅限回环访问的 `settings.openDocument` 请求;Host 会再次解析提供方路径、在文档缺失时将其创建出来,并交给原生文本编辑器(macOS 上使用 `open -t`,绕过浏览器文件关联;Linux 和 Windows 上使用桌面文件关联;WSL 上经 `wslpath -w` 转换后使用 Windows 文件关联)。打开失败时该操作仍可使用,并渲染本地化错误。临时读取失败或 Host 拓扑变化后,重新打开对话框或重新连接会刷新可用性。远程浏览器从不注册该操作,也从不发起这项特权设置读取。
宿主端在用户设置 seam 中注册 `ui-onboarding`。其中的 `welcomeNoticeVersion` 字段让既有设置文档中已写入的确认记录保持有效;它没有读取方,也没有任何已发布的引导步骤写入它。 宿主端在用户设置 seam 中注册 `ui-onboarding`。`ui-settings-models` 提供的欢迎步骤通过既有公开 settings 边界读写其中的 `welcomeNoticeVersion`;外壳本身仍不持有产品策略。
## 模型体验 ## 模型体验

View File

@@ -1,7 +1,7 @@
{ {
"name": "@deepseek-ai/dsh-client-ui-settings-general", "name": "@deepseek-ai/dsh-client-ui-settings-general",
"description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice", "description": "Settings ownerless-copy and product onboarding plugin: the General section, shell trigger/header chrome content, settings dictionaries, and the versioned welcome notice",
"version": "0.0.1-rc.5", "version": "0.1.0-rc.3",
"publishConfig": { "publishConfig": {
"access": "restricted" "access": "restricted"
}, },

View File

@@ -7,9 +7,8 @@
* aria-labelledby the title node; close: visually-hidden slot text). Modal * aria-labelledby the title node; close: visually-hidden slot text). Modal
* open state and the active section id are component-local viewing state; * open state and the active section id are component-local viewing state;
* the onboarding coordinator mounts exactly one ordered registrant while the * the onboarding coordinator mounts exactly one ordered registrant while the
* sessions-derived empty-Hero fact is active — the takeover chrome * sessions-derived empty-Hero fact is active. Visible dialog chrome belongs
* (OnboardingSurface) belongs to the step, so a mounted-but-deciding step * to the step, so a mounted-but-deciding step paints nothing here.
* paints nothing here.
*/ */
import { useCallback, useEffect, useId, useRef, useState } from 'react' import { useCallback, useEffect, useId, useRef, useState } from 'react'
import clsx from 'clsx' import clsx from 'clsx'
@@ -160,10 +159,9 @@ export function SettingsRoot(props: SettingsRootComponentProps) {
onClose={close} onClose={close}
/> />
)} )}
{/* The takeover chrome (OnboardingSurface: mask, opaque stage, `#root` {/* Dialog chrome and `#root` inert ownership live inside each step's
inert) lives inside the step component, wrapped around its visible visible branch. A step still deciding (private facts loading)
content — a step still deciding (private facts loading) renders renders null, so nothing paints or blocks while it decides. */}
null, so nothing paints or blocks while it decides. */}
{onboardingStep !== undefined && renderSlot('settings.onboarding', { {onboardingStep !== undefined && renderSlot('settings.onboarding', {
stepId: onboardingStep.id, stepId: onboardingStep.id,
complete: () => { completeOnboardingStep(onboardingStep.id) }, complete: () => { completeOnboardingStep(onboardingStep.id) },

Some files were not shown because too many files have changed in this diff Show More