dsh-llm-fallbacks 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -24
- package/README.zh-CN.md +58 -22
- package/dist/client/FallbacksCard.d.ts +1 -1
- package/dist/client/GeneralFallbacksRow.d.ts +1 -1
- package/dist/client/index.js +165 -5
- package/dist/client/use-snapshot.d.ts +11 -0
- package/dist/commands.d.ts +120 -27
- package/dist/gateway.d.ts +17 -0
- package/dist/index.js +473 -12
- package/dist/selectors.d.ts +6 -3
- package/dist/tui-settings.d.ts +140 -0
- package/dist/tui.d.ts +5 -4
- package/package.json +26 -23
- package/scripts/build-client.ts +9 -9
package/README.md
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
8
|

|
|
9
|
+

|
|
10
10
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
11
11
|
|
|
12
12
|
Automatic provider/model fallback chains for dsh (DeepSeek Harness): when an agent's LLM requests keep failing — retries exhausted, auth errors, quota exceeded, rate limiting (429) — the plugin switches provider/model along the fallback chain for the current role, and the current step/turn continues on the target model: tasks are not interrupted by model problems.
|
|
13
13
|
|
|
14
|
-
Works in both dsh front ends: the **web** profile (Settings →
|
|
14
|
+
Works in both dsh front ends: the **web** profile (Settings → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` session diagnostics, `/fallbacks config` readback, and the `/settings` fallbacks section for editing).
|
|
15
15
|
|
|
16
16
|
## Time slots
|
|
17
17
|
|
|
18
|
-
Time slots
|
|
18
|
+
Time slots rotate the **effective root chain** by wall-clock windows: each slot row carries its own fallback chain, and the first row whose window contains the current moment replaces the all-day chain for the next root request — the all-day chain stays as the last resort when no slot matches. Peak and valley windows can therefore use different chains while the failure walk (fallback switch) remains untouched.
|
|
19
19
|
|
|
20
20
|

|
|
21
21
|
|
|
@@ -30,7 +30,7 @@ Four frozen UTC+8 presets (windows are code constants; preset rows lock `tz` to
|
|
|
30
30
|
|
|
31
31
|
GLM Peak and GLM Valley are offered in the card picker only when `zai-coding-cn` is configured.
|
|
32
32
|
|
|
33
|
-
The first extra row whose window contains the current moment (in `fallbacks.tz`, default Asia/Shanghai) wins; no match → the all-day `rootChain`, whose tail (
|
|
33
|
+
The first extra row whose window contains the current moment (in `fallbacks.tz`, default Asia/Shanghai) wins; no match → the all-day `rootChain`, whose tail (Default model) must be exactly one official V4 model — `deepseek-official/deepseek-v4-flash` XOR `deepseek-official/deepseek-v4-pro`. Slot rotation is a routing seed, not a failure decision: it applies on the next root request, consumes no cooldown, and is logged as a time-slot switch — failure walks keep fallback switch. Full semantics → [Time-slot presets](#time-slot-presets) and [docs/configuration.md](docs/configuration.md).
|
|
34
34
|
|
|
35
35
|
## Quick start
|
|
36
36
|
|
|
@@ -57,18 +57,30 @@ pnpm repair:fallbacks-switch-logs -- --apply --backup # mark legacy events i
|
|
|
57
57
|
|
|
58
58
|
The script scans `~/.dsh/sessions` by default (override with `--root <dir>`), marks legacy `fallbacks/switch` events `ignorable: true` so the host read path accepts the session again, and keeps a `<file>.bak` per repaired log. `--apply` requires `--backup` and must run with dsh stopped. From 0.2.2 on, the plugin stops writing durable switch events, so no new sessions need repair.
|
|
59
59
|
|
|
60
|
+
### Configuration surfaces
|
|
61
|
+
|
|
62
|
+
The plugin's settings live in a shared `fallbacks:` namespace, editable from three surfaces:
|
|
63
|
+
|
|
64
|
+
| Surface | What it is | Notes |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| **Web settings card** | Settings → Plugins → Fallbacks | Full GUI editor for the `fallbacks:` namespace; writes the shared settings document |
|
|
67
|
+
| **`$DSH_HOME/settings.yaml`** | `fallbacks:` section in the dsh settings document | The shared source of truth — the same file the web card writes; readable and editable everywhere, including scripted setups |
|
|
68
|
+
| **TUI `/settings`** | fallbacks section in the dsh-tui settings screen | dsh-tui ≥ v0.8.5; native fields for simple keys, JSON text fields for complex structures (see [dsh-tui profile (terminal)](#dsh-tui-profile-terminal)) |
|
|
69
|
+
|
|
70
|
+
Pick the surface that matches your front end: web users get the card, terminal users get `/settings`, and the YAML file works everywhere. (`/fallbacks` and `/fallbacks config` are diagnostics — read-only views, not edit surfaces.)
|
|
71
|
+
|
|
60
72
|
### Minimal configuration
|
|
61
73
|
|
|
62
|
-
Add a `fallbacks:` section to the
|
|
74
|
+
Add a `fallbacks:` section to the shared settings document (`$DSH_HOME/settings.yaml` — see [Configuration surfaces](#configuration-surfaces)):
|
|
63
75
|
|
|
64
76
|
```yaml
|
|
65
77
|
fallbacks:
|
|
66
|
-
enabled: true # feature switch
|
|
67
|
-
rootChain: # all-day chain:
|
|
68
|
-
- anthropic/claude-3-5-sonnet #
|
|
69
|
-
- deepseek-official/deepseek-v4-flash # last
|
|
78
|
+
enabled: true # feature switch — defaults to off (plugin is a no-op otherwise)
|
|
79
|
+
rootChain: # all-day chain: leading entries = fallback walk, last = Default model (official V4)
|
|
80
|
+
- anthropic/claude-3-5-sonnet # walked first
|
|
81
|
+
- deepseek-official/deepseek-v4-flash # last resort (Flash or Pro)
|
|
70
82
|
timeSlots: # optional: rotate the effective root chain by wall-clock windows
|
|
71
|
-
- kind: preset # frozen UTC+8
|
|
83
|
+
- kind: preset # frozen UTC+8 window; only the chain is editable
|
|
72
84
|
preset: liang-peak # 09:00–12:00 and 14:00–18:00, every day
|
|
73
85
|
chain:
|
|
74
86
|
- anthropic/claude-3-5-sonnet
|
|
@@ -79,40 +91,64 @@ fallbacks:
|
|
|
79
91
|
days: [1, 5] # optional; omitted/empty = every day (0=Sunday…6=Saturday)
|
|
80
92
|
chain:
|
|
81
93
|
- openai/gpt-4o
|
|
82
|
-
roles: #
|
|
94
|
+
roles: # optional: declare role entities, then reference them from rules
|
|
83
95
|
list:
|
|
84
|
-
- id: reviewer # unique id
|
|
96
|
+
- id: reviewer # unique id; "inherit" is reserved
|
|
85
97
|
persona: Code-review subagents
|
|
86
98
|
chain:
|
|
87
99
|
- openai/gpt-4o-mini
|
|
88
|
-
fallback: inherit-root #
|
|
100
|
+
fallback: inherit-root # role chain first, then the inherited rootChain
|
|
89
101
|
rules: # subagent-only: rules never match root requests
|
|
90
|
-
- role: reviewer # all subagents → reviewer role
|
|
102
|
+
- role: reviewer # all subagents → the reviewer role
|
|
91
103
|
```
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
Build the section up in four steps:
|
|
106
|
+
|
|
107
|
+
**1. Enable the plugin.** `enabled: true` turns the fallback engine on. It defaults to **off** — with no chains configured the plugin is a complete no-op.
|
|
108
|
+
|
|
109
|
+
**2. Set the all-day `rootChain`.** Leading entries are the fallback chain, walked first when a request fails; the **last** entry is the Default model.
|
|
110
|
+
|
|
111
|
+
> **Conformance**: the last entry must be exactly one official V4 model — `deepseek-official/deepseek-v4-flash` XOR `deepseek-official/deepseek-v4-pro`. The settings card and gateway reject any other tail on save; a legacy non-official tail warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is.
|
|
112
|
+
|
|
113
|
+
**3. Add `timeSlots` (optional).** Rows rotate the effective root chain by wall-clock windows. Preset rows use frozen UTC+8 windows (only their chain is editable; while a preset row exists, `tz` locks to `Asia/Shanghai`); custom rows take `start`/`end` (may wrap midnight) and an optional `days` list. The first row whose window contains the current moment wins; no match → the all-day `rootChain`. Rotation is a routing seed — it applies on the next root request and consumes no cooldown (see [Time slots](#time-slots)).
|
|
114
|
+
|
|
115
|
+
**4. Add `roles` (optional).** Declare role entities in `roles.list` (id, persona, chain, optional `fallback` policy), then map subagents to them with `roles.rules`. Rules never match root requests — with no rule match (or on a root request) the built-in `inherit` role applies and appends the `rootChain`.
|
|
116
|
+
|
|
117
|
+
Full reference (role entities, fallback strategies, rules, selectors, preset roles, time-slot presets) → [docs/configuration.md](docs/configuration.md).
|
|
94
118
|
|
|
95
119
|
> **Upgrade note (behavior change)**: an existing `fallbacks:` section **without an explicit `enabled` key** resolves to `false` after upgrading — add `enabled: true` to keep the plugin active.
|
|
96
120
|
|
|
97
121
|
### Verify
|
|
98
122
|
|
|
99
|
-
Save and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent
|
|
123
|
+
Save the config and restart the session, then type `/fallbacks` — the read-only in-session diagnostics (origin, resolved role, chain, recent fallback switches, cooldown status). In a dsh-tui profile, `/fallbacks config` reads back the composed configuration; see [dsh-tui profile (terminal)](#dsh-tui-profile-terminal).
|
|
100
124
|
|
|
101
125
|
## Features
|
|
102
126
|
|
|
103
127
|
- **Automatic fallback for root and subagents**: any agent switches down the chain to the next available provider/model on model failure — no manual model switching.
|
|
104
128
|
- **Two-block config**: `rootChain` for the root agent; declared role entities (`roles.list`) referenced by `roles.rules` (or the built-in `inherit`).
|
|
105
129
|
- **Chain as root primary from the picker**: when `enabled` is on, the host model picker (web and TUI alike) shows a virtual `FallbacksChain` / `Auto` row — selecting it uses the configured chain as the root primary (a conforming all-day head is required for the override to succeed); selecting a real model keeps fallback-only (see [FallbacksChain in the model picker](#fallbackschain-in-the-model-picker)).
|
|
106
|
-
- **Time slots
|
|
130
|
+
- **Time slots**: optional `fallbacks.timeSlots` rows rotate the effective root chain by wall-clock windows in the config-level `tz` timezone (default `Asia/Shanghai`) — four frozen UTC+8 presets (`liang-peak` / `liang-valley` / `glm-peak` / `glm-valley`, windows are code constants, models-only edits) or custom `start`/`end`/`days` windows. The first matching row wins; the all-day row is always last. A slot change applies on the **next** root request and is logged as a **time-slot switch** — a routing seed, never a failure decision: it consumes no cooldown and does not count against `maxSwitchesPerStep`. Failure walks keep the **fallback switch** copy (see [Time-slot presets](#time-slot-presets)).
|
|
107
131
|
- **Dispatch-time role resolution**: on a subagent's first request its role is resolved in three stages — explicit (`agentPreset` matches a declared role id) → deterministic rules (unchanged) → LLM auto-match from the declared role taxonomy (`fallbacks.roleAutoMatch`, default `true`). The resolved role's chain-head model is injected into the first request and recorded via an explicit `role → model` log line (no durable `fallbacks/switch` event is written — issue #52 stop-write); set `roleAutoMatch: false` to disable the LLM auto-match stage (the explicit `agentPreset` stage still applies — with no explicit role this reproduces the previous rules-only behavior). The settings card always renders an **Enable role auto-match** switch (default `true`) to toggle it — the schema default applies even to legacy configs that never declared the key.
|
|
108
132
|
- **Cooldown and revert**: failed / switched-away models are not re-selected during cooldown; `revertPolicy: cooldown-expiry` returns to the primary model automatically.
|
|
109
133
|
- **Visible behavior**: every switch is recorded in an info-level log line (from/to/role/reason) — no silent model switching. The plugin deliberately writes **no** durable `fallbacks/switch` session events (issue #52: the apply()-time event-type registration was proven ineffective, and a session containing the event refused to load after a dsh restart). Sessions written by older plugin versions that contain such events are repaired by `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy events ignorable so affected sessions load again.
|
|
110
134
|
- **Safety valves**: `maxSwitchesPerStep` caps switches per step and `alwaysModeRetryCap` caps always-mode retries — chain loops cannot amplify latency.
|
|
111
|
-
- **No-config no-op**:
|
|
135
|
+
- **No-config no-op**: with no chains configured the plugin behaves exactly like not being installed (`enabled` is off by default — see [Minimal configuration](#minimal-configuration)).
|
|
136
|
+
|
|
137
|
+
## dsh-tui profile (terminal)
|
|
138
|
+
|
|
139
|
+
In a dsh-tui profile the plugin has three operator surfaces, with a strict duty split:
|
|
140
|
+
|
|
141
|
+
- **`/fallbacks`** — what happened this session: origin, resolved role, effective chain, recent fallback switches, cooldown status. Read-only.
|
|
142
|
+
- **`/fallbacks config`** — what is configured: composed-config readback (trigger codes, root chain, time slots, timezone, roles, role rules, cooldown, revert policy, safety valves, presets, role auto-match). Read-only apart from the one action command **`/fallbacks config revert-seed <role-id>`**, which restores a seeded role's persona to its declared seed default (a web-card action the settings seam cannot express).
|
|
143
|
+
- **`/settings`** — the edit surface. The plugin registers a **fallbacks** section with full parity to the web settings card: booleans (`enabled`, `roleAutoMatch`) render as toggles, selects (`presets`, `revertPolicy`) as pickers, and numbers (`cooldownMs`, `maxSwitchesPerStep`, `alwaysModeRetryCap`) as numeric inputs; complex structures (`rootChain`, `timeSlots`, `roles.list`, `roles.rules`) are JSON text fields and `triggerCodes` a comma-separated text field. Invalid drafts (bad JSON, non-conforming chains, malformed time-slot rows) block the save — the section never corrupts the config.
|
|
144
|
+
|
|
145
|
+
**Requirements**: the `/settings` fallbacks section needs **dsh-tui ≥ v0.8.5** (commit `c51661f` or later on `main`; the settings seam shipped in v0.8.0, the groups shape + validation in v0.8.5). On an older dsh-tui the section is absent, and file editing remains the only TUI edit surface.
|
|
146
|
+
|
|
147
|
+
File editing still works everywhere: the shared `$DSH_HOME/settings.yaml` (`fallbacks:` section — the same file the web card writes) for global settings, or the profile patch `~/.dsh/profiles/dsh-tui/cordis.patch.yml` (`config:` overrides on the plugin row) for dsh-tui-specific values. A patch row **replaces** the targeted row's whole `config` — restate every field you want to keep (schema defaults fill the rest).
|
|
112
148
|
|
|
113
149
|
## FallbacksChain in the model picker
|
|
114
150
|
|
|
115
|
-
When `enabled: true`, the plugin registers a virtual provider, **FallbacksChain**, with a single catalog row: **Auto**. The web profile and dsh-tui both see the row: they share the same adapter catalog, so no
|
|
151
|
+
When `enabled: true`, the plugin registers a virtual provider, **FallbacksChain**, with a single catalog row: **Auto**. The web profile and dsh-tui both see the row: they share the same adapter catalog, so the row needs no settings-page wiring or host patch (it is independent of the `/settings` fallbacks section, which edits configuration rather than the picker catalog). The row is visible whenever the plugin is enabled — a legacy or empty all-day chain does NOT hide it (the override just refuses to fire).
|
|
116
152
|
|
|
117
153
|
Selecting **FallbacksChain / Auto** uses the configured chain as the root **primary**: root requests route to the effective chain's first exact `provider/model` at request time, and the fallback engine degrades from that head as usual. Selecting any real catalog model keeps the v0.2.2 fallback-only behavior — the session model is primary and the chain engages only after it fails.
|
|
118
154
|
|
|
@@ -122,19 +158,19 @@ Notes:
|
|
|
122
158
|
|
|
123
159
|
- **Picker label**: the row's catalog `name` (what the composer trigger shows) is live — `Auto: DeepSeek V4 Flash[Liang Peak]` / `Auto: DeepSeek V4 Flash[all-day]` (catalog display name, not the model id); the id stays `Auto`. Bare `Auto` if the all-day tail is not conforming. Refresh by reopening the picker.
|
|
124
160
|
- **Root only**: the row is about the root agent. Subagent role resolution and injection are unchanged; a subagent session that inherits the selection still routes through the chain head — the virtual row is a thin delegate, never a second routing engine.
|
|
125
|
-
- **Conformance gate on the tail**: a successful override/delegate requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`, the card's
|
|
161
|
+
- **Conformance gate on the tail**: a successful override/delegate requires the all-day chain to be **tail-conforming** — its last entry must be exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`, the card's Default model panel); leading entries (Default fallback chain) are walked first. Disabling the plugin hides the row again (slot-row/chain edits never churn registration).
|
|
126
162
|
- **Stale selection**: if the row disappears (plugin disabled) while `FallbacksChain / Auto` is selected, the session keeps showing it as the current model with `routable: false` — pick a real model from the catalog to continue (host-native catalog semantics).
|
|
127
163
|
- **Capabilities follow the head**: the row's model metadata (context window, modalities, reasoning) mirrors the current effective head; retry attribution follows the permissive default — retries/failures are accounted to the real head pair, not to the `FallbacksChain` provider. Full semantics → [docs/configuration.md](docs/configuration.md).
|
|
128
164
|
|
|
129
|
-
## Time-slot presets
|
|
165
|
+
## Time-slot presets
|
|
130
166
|
|
|
131
|
-
Time slots
|
|
167
|
+
Time slots are introduced in the [featured overview](#time-slots) above; this section is the reference. Time-slot rows rotate the **effective root chain** by wall-clock windows — useful for peak/valley pricing without confusing wall-clock rotation with failure fallback. The copy split is strict: slot rotation logs and UI say **time-slot switch**; the failure walk keeps **fallback switch**; the conversation notice Model downgraded stays on the failure path only.
|
|
132
168
|
|
|
133
|
-
- **Match order**: at every root request, the first extra row whose window contains the current moment (in `fallbacks.tz`, default `Asia/Shanghai` / UTC+8) wins — that row's chain **replaces** the all-day chain. No row matches → the all-day `rootChain` is used. The all-day row is always last and **required**: its last entry must be exactly one official V4 model (Flash XOR Pro; leading
|
|
169
|
+
- **Match order**: at every root request, the first extra row whose window contains the current moment (in `fallbacks.tz`, default `Asia/Shanghai` / UTC+8) wins — that row's chain **replaces** the all-day chain. No row matches → the all-day `rootChain` is used. The all-day row is always last and **required**: its last entry must be exactly one official V4 model (Flash XOR Pro; leading Default fallback chain entries are walked first).
|
|
134
170
|
- **Presets** (frozen, not user-editable): `liang-peak` = 09:00–12:00 **and** 14:00–18:00 every day; `liang-valley` = every other UTC+8 time; `glm-peak` = Monday–Friday 14:00–18:00; `glm-valley` = every other time. One preset id = one row; the card picker never offers a duplicate.
|
|
135
171
|
- **Custom rows**: `start` / `end` (`HH:mm`, may wrap midnight) + optional `days` (0=Sunday…6=Saturday; omitted/empty = every day) + models.
|
|
136
172
|
- **Next-request apply**: a slot boundary crossing never preempts an in-flight step — the new row takes effect on the next root request. Rotation is mount-only: info log + card/`/fallbacks` status line, no durable switch event.
|
|
137
|
-
- **Settings card**: the
|
|
173
|
+
- **Settings card**: the Main agent section groups Time slots (extra rows — add preset / add custom / remove / reorder by buttons or **drag**; preset rows show a read-only window summary and edit models only; custom rows carry an editable name; the **timezone picker** lives here and **locks to Asia/Shanghai while any preset row exists**, since preset windows are frozen UTC+8 constants), Default fallback chain (walked first when no slot matches) and Default model (the official V4 Flash | Pro last-resort fallback). Rows are collapsible to name + first model. There is no `timeSlots.enabled` master switch (adding a row is the opt-in) and no `rootMode` control.
|
|
138
174
|
|
|
139
175
|
## Preset roles
|
|
140
176
|
|
package/README.zh-CN.md
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
[](LICENSE)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
8
|

|
|
9
|
+

|
|
10
10
|
[](https://dshfind.com/zh/plugins/omdsh-dev/dsh-llm-fallbacks?ref=badge)
|
|
11
11
|
|
|
12
12
|
dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
|
|
13
13
|
|
|
14
|
-
两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks`
|
|
14
|
+
两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Fallbacks 卡片)与 **dsh-tui** 终端 profile(`/fallbacks` 会话诊断、`/fallbacks config` 回读,以及 `/settings` 中的 fallbacks 区块用于编辑)。
|
|
15
15
|
|
|
16
16
|
## 峰谷无忧
|
|
17
17
|
|
|
@@ -57,46 +57,70 @@ pnpm repair:fallbacks-switch-logs -- --apply --backup # 给旧事件打 igno
|
|
|
57
57
|
|
|
58
58
|
脚本默认扫描 `~/.dsh/sessions`(可用 `--root <dir>` 覆盖),把遗留 `fallbacks/switch` 事件标记为 `ignorable: true`,宿主读路径即可重新接受该会话;每个被修复的日志保留一份 `<file>.bak`。`--apply` 必须搭配 `--backup`,且须在 dsh 停止时运行。从 0.2.2 起插件不再写 durable 切换事件,新会话无需修复。
|
|
59
59
|
|
|
60
|
+
### 配置界面
|
|
61
|
+
|
|
62
|
+
插件的设置存在于共享的 `fallbacks:` 命名空间中,可通过三个界面编辑:
|
|
63
|
+
|
|
64
|
+
| 界面 | 是什么 | 说明 |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| **Web 设置卡** | 设置 → 插件配置 → Fallbacks | `fallbacks:` 命名空间的完整 GUI 编辑器;写入共享设置文档 |
|
|
67
|
+
| **`$DSH_HOME/settings.yaml`** | dsh 设置文档中的 `fallbacks:` 分节 | 共享的事实源——与 Web 卡写入的是同一个文件;任何场景(包括脚本化配置)都可读写 |
|
|
68
|
+
| **TUI `/settings`** | dsh-tui 设置界面中的 fallbacks 区块 | 需要 dsh-tui ≥ v0.8.5;简单键用原生字段,复杂结构用 JSON 文本字段(见 [dsh-tui profile(终端)](#dsh-tui-profile终端)) |
|
|
69
|
+
|
|
70
|
+
按你的前端选择入口:web 用户用设置卡,终端用户用 `/settings`,YAML 文件处处可用。(`/fallbacks` 与 `/fallbacks config` 是诊断命令——只读视图,不是编辑入口。)
|
|
71
|
+
|
|
60
72
|
### 最小配置
|
|
61
73
|
|
|
62
|
-
|
|
74
|
+
在共享设置文档(`$DSH_HOME/settings.yaml`——见 [配置界面](#配置界面))中添加 `fallbacks:` 分节:
|
|
63
75
|
|
|
64
76
|
```yaml
|
|
65
77
|
fallbacks:
|
|
66
|
-
enabled: true
|
|
67
|
-
rootChain:
|
|
68
|
-
- anthropic/claude-3-5-sonnet #
|
|
69
|
-
- deepseek-official/deepseek-v4-flash #
|
|
70
|
-
timeSlots:
|
|
71
|
-
- kind: preset
|
|
72
|
-
preset: liang-peak
|
|
78
|
+
enabled: true # 功能开关——默认关闭(否则插件完全 no-op)
|
|
79
|
+
rootChain: # 全时段链:前面的条目 = 降级路径,最后一项 = 默认模型(官方 V4)
|
|
80
|
+
- anthropic/claude-3-5-sonnet # 先走
|
|
81
|
+
- deepseek-official/deepseek-v4-flash # 最后一档(Flash 或 Pro)
|
|
82
|
+
timeSlots: # 可选:按墙钟窗口轮换生效 root 链
|
|
83
|
+
- kind: preset # 冻结的 UTC+8 窗口;仅链可编辑
|
|
84
|
+
preset: liang-peak # 每天 09:00–12:00 与 14:00–18:00
|
|
73
85
|
chain:
|
|
74
86
|
- anthropic/claude-3-5-sonnet
|
|
75
|
-
- kind: custom
|
|
76
|
-
name: evening
|
|
87
|
+
- kind: custom # 自定义窗口(可跨午夜)
|
|
88
|
+
name: evening # 可选显示名称
|
|
77
89
|
start: '22:00'
|
|
78
90
|
end: '02:00'
|
|
79
|
-
days: [1, 5]
|
|
91
|
+
days: [1, 5] # 可选;缺省/空 = 每天(0=周日…6=周六)
|
|
80
92
|
chain:
|
|
81
93
|
- openai/gpt-4o
|
|
82
|
-
roles:
|
|
94
|
+
roles: # 可选:先声明角色实体,再由规则引用
|
|
83
95
|
list:
|
|
84
|
-
- id: reviewer
|
|
96
|
+
- id: reviewer # 唯一 id;"inherit" 为保留字
|
|
85
97
|
persona: 代码审查子代理
|
|
86
98
|
chain:
|
|
87
99
|
- openai/gpt-4o-mini
|
|
88
|
-
fallback: inherit-root #
|
|
89
|
-
rules:
|
|
90
|
-
- role: reviewer
|
|
100
|
+
fallback: inherit-root # 先走角色链,再追加继承的 rootChain
|
|
101
|
+
rules: # 仅对子代理生效:规则不匹配 root 请求
|
|
102
|
+
- role: reviewer # 所有 subagent → reviewer 角色
|
|
91
103
|
```
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
按四个步骤逐步构建:
|
|
106
|
+
|
|
107
|
+
**1. 启用插件。** `enabled: true` 打开降级引擎。默认**关闭(`false`)**——未配置任何链时插件完全 no-op。
|
|
108
|
+
|
|
109
|
+
**2. 配置全时段 `rootChain`。** 前面的条目是降级链,请求失败时先走;**最后**一项是默认模型。
|
|
110
|
+
|
|
111
|
+
> **链尾合规**:最后一项必须是恰好一个官方 V4 模型——`deepseek-official/deepseek-v4-flash` 或 `deepseek-official/deepseek-v4-pro`(二选一)。设置卡与 gateway 在保存时拒绝其它尾巴;遗留的非合规尾巴启动时告警并继续按 fallback-only 走原链,但无法原样保存。
|
|
112
|
+
|
|
113
|
+
**3. 添加 `timeSlots`(可选)。** 各行按墙钟窗口轮换生效 root 链。预设行使用冻结的 UTC+8 窗口(仅链可编辑;存在预设行时 `tz` 锁定 `Asia/Shanghai`);自定义行使用 `start`/`end`(可跨午夜)与可选的 `days` 列表。第一个窗口包含当前时刻的行生效;无行命中 → 全时段 `rootChain`。分时切换是路由种子——在下一个 root 请求生效、不消耗冷却(见 [峰谷无忧](#峰谷无忧))。
|
|
114
|
+
|
|
115
|
+
**4. 添加 `roles`(可选)。** 在 `roles.list` 中声明角色实体(id、persona、chain、可选的 `fallback` 策略),再用 `roles.rules` 把 subagent 映射到角色。规则绝不匹配 root 请求——未命中规则(或 root 请求)时由内置 `inherit` 角色兜底,追加 `rootChain`。
|
|
116
|
+
|
|
117
|
+
完整参考(角色实体、fallback 策略、规则、selector、预设角色、分时槽预设)→ [docs/configuration.md](docs/configuration.md)。
|
|
94
118
|
|
|
95
119
|
> **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
|
|
96
120
|
|
|
97
121
|
### 验证
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
保存配置并重启会话,然后键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近降级切换、冷却状态)。在 dsh-tui profile 中,`/fallbacks config` 回读组合配置;见 [dsh-tui profile(终端)](#dsh-tui-profile终端)。
|
|
100
124
|
|
|
101
125
|
## 能力一览
|
|
102
126
|
|
|
@@ -108,11 +132,23 @@ fallbacks:
|
|
|
108
132
|
- **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型。
|
|
109
133
|
- **行为可见**:每次切换以 info 级日志行(from/to/role/reason)记录——无静默换模型。插件**刻意不写** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的事件类型注册被证伪无效,含该事件的会话在 dsh 重启后拒绝加载)。由旧版插件写入、含此类事件的会话由 `scripts/repair-fallbacks-switch-logs.ts` 修复——旧事件被标记 ignorable 后,受影响会话可重新加载。
|
|
110
134
|
- **安全阀**:`maxSwitchesPerStep` 限制每 step 切换次数、`alwaysModeRetryCap` 限制 always 模式重试——链循环不会放大延迟。
|
|
111
|
-
- **无配置回归(no-op
|
|
135
|
+
- **无配置回归(no-op)**:未配置任何链时行为与未安装插件完全一致——`enabled` 默认关闭(见 [最小配置](#最小配置))。
|
|
136
|
+
|
|
137
|
+
## dsh-tui profile(终端)
|
|
138
|
+
|
|
139
|
+
在 dsh-tui profile 中,插件有三个操作面——职责严格区分:
|
|
140
|
+
|
|
141
|
+
- **`/fallbacks`** —— 本次会话发生了什么:来源、解析角色、生效链、最近降级切换、冷却状态。只读。
|
|
142
|
+
- **`/fallbacks config`** —— 配置了什么:组合配置回读(触发码、根链、分时槽、时区、角色、角色规则、冷却、回主策略、安全阀、预置、角色自动匹配)。除唯一的动作命令 **`/fallbacks config revert-seed <role-id>`** 外只读——该命令把某个 seed 角色的 persona 还原为已声明的默认(设置 seam 无法表达 Web 卡的这类动作能力)。
|
|
143
|
+
- **`/settings`** —— 编辑界面。插件注册 **fallbacks** 区块,与 **Web 设置卡完全一致**:布尔(`enabled`、`roleAutoMatch`)渲染为开关、下拉(`presets`、`revertPolicy`)为选择器、数值(`cooldownMs`、`maxSwitchesPerStep`、`alwaysModeRetryCap`)为数字输入;复杂结构(`rootChain`、`timeSlots`、`roles.list`、`roles.rules`)为 JSON 文本字段,`triggerCodes` 为逗号分隔文本字段。非法草稿(JSON 解析失败、链尾不合规、分时行畸形)会阻止保存——区块绝不写入损坏配置。
|
|
144
|
+
|
|
145
|
+
**版本要求**:`/settings` 的 fallbacks 区块需要 **dsh-tui ≥ v0.8.5**(`main` 上 commit `c51661f` 及以后;settings seam 于 v0.8.0 引入,groups 结构与校验于 v0.8.5 引入)。更旧的 dsh-tui 没有该区块,文件编辑仍是 TUI 唯一编辑面。
|
|
146
|
+
|
|
147
|
+
文件编辑在任意情况下仍然可用:全局设置写共享的 `$DSH_HOME/settings.yaml`(`fallbacks:` 分节——与 Web 卡写的是同一个文件);dsh-tui 专属覆盖写 profile patch `~/.dsh/profiles/dsh-tui/cordis.patch.yml`(插件行上的 `config:` 覆盖)。注意:patch 行会**整体替换**目标行的整个 `config`——想保留的字段都要写全(schema 默认值补齐其余)。
|
|
112
148
|
|
|
113
149
|
## 模型选择器中的 FallbacksChain
|
|
114
150
|
|
|
115
|
-
当 `enabled: true` 时,插件注册一个虚拟 provider **FallbacksChain**,目录中只有一行:**Auto**。web profile 与 dsh-tui 都能看到这一行:两者共享同一个 adapter catalog
|
|
151
|
+
当 `enabled: true` 时,插件注册一个虚拟 provider **FallbacksChain**,目录中只有一行:**Auto**。web profile 与 dsh-tui 都能看到这一行:两者共享同一个 adapter catalog,无需设置页接线或宿主补丁(它与 `/settings` 的 fallbacks 区块相互独立——区块编辑的是配置,不是选择器目录)。该行只要插件启用就可见——遗留多模型或空的 all-day 链**不会**隐藏它(只是覆盖不会生效)。
|
|
116
152
|
|
|
117
153
|
选择 **FallbacksChain / Auto** = 把配置的链作为 root **主模型**:root 请求路由到请求时刻生效链的第一个精确 `provider/model`,失败后由降级引擎从该链头照常沿链切换。选择任何真实目录模型则保持 v0.2.2 的 fallback-only 行为——会话模型为主,链只在它失败后介入。
|
|
118
154
|
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
*/
|
|
81
81
|
import type { ReactNode } from 'react';
|
|
82
82
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
83
|
-
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-
|
|
83
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
84
84
|
import { FallbacksSettingsController, type FallbacksSettingsState } from './fallbacks-store.ts';
|
|
85
85
|
/** Injected dependencies of {@link FallbacksCard} (slot `inject`). */
|
|
86
86
|
export interface FallbacksCardInjected {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
-
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-
|
|
3
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
4
4
|
import type { FallbacksSettingsController, FallbacksSettingsState } from './fallbacks-store.ts';
|
|
5
5
|
/** Injected dependencies of {@link GeneralFallbacksRow} (slot `inject`). */
|
|
6
6
|
export interface GeneralFallbacksRowInjected {
|
package/dist/client/index.js
CHANGED
|
@@ -4,11 +4,170 @@ window.__ModuleLoader__.load({
|
|
|
4
4
|
var module = { exports: {} };
|
|
5
5
|
var exports = module.exports;
|
|
6
6
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
-
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
9
|
+
//#endregion
|
|
8
10
|
let react = require("react");
|
|
9
11
|
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
12
|
let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
|
|
11
13
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
14
|
+
//#region node_modules/use-sync-external-store/cjs/use-sync-external-store-shim.production.js
|
|
15
|
+
/**
|
|
16
|
+
* @license React
|
|
17
|
+
* use-sync-external-store-shim.production.js
|
|
18
|
+
*
|
|
19
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
20
|
+
*
|
|
21
|
+
* This source code is licensed under the MIT license found in the
|
|
22
|
+
* LICENSE file in the root directory of this source tree.
|
|
23
|
+
*/
|
|
24
|
+
var require_use_sync_external_store_shim_production = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
25
|
+
var React$1 = require("react");
|
|
26
|
+
function is(x, y) {
|
|
27
|
+
return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
|
|
28
|
+
}
|
|
29
|
+
var objectIs = "function" === typeof Object.is ? Object.is : is;
|
|
30
|
+
var useState = React$1.useState;
|
|
31
|
+
var useEffect = React$1.useEffect;
|
|
32
|
+
var useLayoutEffect = React$1.useLayoutEffect;
|
|
33
|
+
var useDebugValue = React$1.useDebugValue;
|
|
34
|
+
function useSyncExternalStore$2(subscribe, getSnapshot) {
|
|
35
|
+
var value = getSnapshot(), _useState = useState({ inst: {
|
|
36
|
+
value,
|
|
37
|
+
getSnapshot
|
|
38
|
+
} }), inst = _useState[0].inst, forceUpdate = _useState[1];
|
|
39
|
+
useLayoutEffect(function() {
|
|
40
|
+
inst.value = value;
|
|
41
|
+
inst.getSnapshot = getSnapshot;
|
|
42
|
+
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
|
|
43
|
+
}, [
|
|
44
|
+
subscribe,
|
|
45
|
+
value,
|
|
46
|
+
getSnapshot
|
|
47
|
+
]);
|
|
48
|
+
useEffect(function() {
|
|
49
|
+
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
|
|
50
|
+
return subscribe(function() {
|
|
51
|
+
checkIfSnapshotChanged(inst) && forceUpdate({ inst });
|
|
52
|
+
});
|
|
53
|
+
}, [subscribe]);
|
|
54
|
+
useDebugValue(value);
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function checkIfSnapshotChanged(inst) {
|
|
58
|
+
var latestGetSnapshot = inst.getSnapshot;
|
|
59
|
+
inst = inst.value;
|
|
60
|
+
try {
|
|
61
|
+
var nextValue = latestGetSnapshot();
|
|
62
|
+
return !objectIs(inst, nextValue);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return !0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function useSyncExternalStore$1(subscribe, getSnapshot) {
|
|
68
|
+
return getSnapshot();
|
|
69
|
+
}
|
|
70
|
+
var shim = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? useSyncExternalStore$1 : useSyncExternalStore$2;
|
|
71
|
+
exports.useSyncExternalStore = void 0 !== React$1.useSyncExternalStore ? React$1.useSyncExternalStore : shim;
|
|
72
|
+
}));
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region node_modules/use-sync-external-store/shim/index.js
|
|
75
|
+
var require_shim = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
76
|
+
module.exports = require_use_sync_external_store_shim_production();
|
|
77
|
+
}));
|
|
78
|
+
//#endregion
|
|
79
|
+
//#region node_modules/use-sync-external-store/cjs/use-sync-external-store-shim/with-selector.production.js
|
|
80
|
+
/**
|
|
81
|
+
* @license React
|
|
82
|
+
* use-sync-external-store-shim/with-selector.production.js
|
|
83
|
+
*
|
|
84
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
85
|
+
*
|
|
86
|
+
* This source code is licensed under the MIT license found in the
|
|
87
|
+
* LICENSE file in the root directory of this source tree.
|
|
88
|
+
*/
|
|
89
|
+
var require_with_selector_production = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
90
|
+
var React = require("react");
|
|
91
|
+
var shim = require_shim();
|
|
92
|
+
function is(x, y) {
|
|
93
|
+
return x === y && (0 !== x || 1 / x === 1 / y) || x !== x && y !== y;
|
|
94
|
+
}
|
|
95
|
+
var objectIs = "function" === typeof Object.is ? Object.is : is;
|
|
96
|
+
var useSyncExternalStore = shim.useSyncExternalStore;
|
|
97
|
+
var useRef = React.useRef;
|
|
98
|
+
var useEffect = React.useEffect;
|
|
99
|
+
var useMemo = React.useMemo;
|
|
100
|
+
var useDebugValue = React.useDebugValue;
|
|
101
|
+
exports.useSyncExternalStoreWithSelector = function(subscribe, getSnapshot, getServerSnapshot, selector, isEqual) {
|
|
102
|
+
var instRef = useRef(null);
|
|
103
|
+
if (null === instRef.current) {
|
|
104
|
+
var inst = {
|
|
105
|
+
hasValue: !1,
|
|
106
|
+
value: null
|
|
107
|
+
};
|
|
108
|
+
instRef.current = inst;
|
|
109
|
+
} else inst = instRef.current;
|
|
110
|
+
instRef = useMemo(function() {
|
|
111
|
+
function memoizedSelector(nextSnapshot) {
|
|
112
|
+
if (!hasMemo) {
|
|
113
|
+
hasMemo = !0;
|
|
114
|
+
memoizedSnapshot = nextSnapshot;
|
|
115
|
+
nextSnapshot = selector(nextSnapshot);
|
|
116
|
+
if (void 0 !== isEqual && inst.hasValue) {
|
|
117
|
+
var currentSelection = inst.value;
|
|
118
|
+
if (isEqual(currentSelection, nextSnapshot)) return memoizedSelection = currentSelection;
|
|
119
|
+
}
|
|
120
|
+
return memoizedSelection = nextSnapshot;
|
|
121
|
+
}
|
|
122
|
+
currentSelection = memoizedSelection;
|
|
123
|
+
if (objectIs(memoizedSnapshot, nextSnapshot)) return currentSelection;
|
|
124
|
+
var nextSelection = selector(nextSnapshot);
|
|
125
|
+
if (void 0 !== isEqual && isEqual(currentSelection, nextSelection)) return memoizedSnapshot = nextSnapshot, currentSelection;
|
|
126
|
+
memoizedSnapshot = nextSnapshot;
|
|
127
|
+
return memoizedSelection = nextSelection;
|
|
128
|
+
}
|
|
129
|
+
var hasMemo = !1, memoizedSnapshot, memoizedSelection, maybeGetServerSnapshot = void 0 === getServerSnapshot ? null : getServerSnapshot;
|
|
130
|
+
return [function() {
|
|
131
|
+
return memoizedSelector(getSnapshot());
|
|
132
|
+
}, null === maybeGetServerSnapshot ? void 0 : function() {
|
|
133
|
+
return memoizedSelector(maybeGetServerSnapshot());
|
|
134
|
+
}];
|
|
135
|
+
}, [
|
|
136
|
+
getSnapshot,
|
|
137
|
+
getServerSnapshot,
|
|
138
|
+
selector,
|
|
139
|
+
isEqual
|
|
140
|
+
]);
|
|
141
|
+
var value = useSyncExternalStore(subscribe, instRef[0], instRef[1]);
|
|
142
|
+
useEffect(function() {
|
|
143
|
+
inst.hasValue = !0;
|
|
144
|
+
inst.value = value;
|
|
145
|
+
}, [value]);
|
|
146
|
+
useDebugValue(value);
|
|
147
|
+
return value;
|
|
148
|
+
};
|
|
149
|
+
}));
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region src/client/use-snapshot.ts
|
|
152
|
+
var import_with_selector = (/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
153
|
+
module.exports = require_with_selector_production();
|
|
154
|
+
})))();
|
|
155
|
+
/**
|
|
156
|
+
* Bind a bare observable source to a typed uSES selector hook.
|
|
157
|
+
* subscribe/getSnapshot are captured once per source into stable closures
|
|
158
|
+
* (also re-binds `this` for method-based sources), so components never
|
|
159
|
+
* resubscribe across renders. Equality defaults to Object.is.
|
|
160
|
+
* @param w - snapshot source (engine store, Session object, store instance).
|
|
161
|
+
* @returns the selector hook.
|
|
162
|
+
*/
|
|
163
|
+
function bindSnapshotSelector(w) {
|
|
164
|
+
const subscribe = (fn) => w.subscribe(fn);
|
|
165
|
+
const getSnapshot = () => w.getSnapshot();
|
|
166
|
+
return function useSelector(sel, eq) {
|
|
167
|
+
return (0, import_with_selector.useSyncExternalStoreWithSelector)(subscribe, getSnapshot, void 0, sel, eq);
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
12
171
|
//#region src/selectors.ts
|
|
13
172
|
/** Catchable error for illegal/unknown selectors (config-warning path). */
|
|
14
173
|
var SelectorError = class extends Error {
|
|
@@ -21,7 +180,8 @@ window.__ModuleLoader__.load({
|
|
|
21
180
|
* Parse a chain key or entry selector.
|
|
22
181
|
*
|
|
23
182
|
* Accepts `provider/model` and `provider/*`; throws {@link SelectorError}
|
|
24
|
-
* on anything else (missing separator, empty parts,
|
|
183
|
+
* on anything else (missing separator, empty parts, wildcard inside the
|
|
184
|
+
* model segment).
|
|
25
185
|
*/
|
|
26
186
|
function parseSelector(input) {
|
|
27
187
|
if (typeof input !== "string") throw new SelectorError(`invalid selector ${String(input)}: expected "provider/model" or "provider/*"`);
|
|
@@ -31,7 +191,7 @@ window.__ModuleLoader__.load({
|
|
|
31
191
|
const provider = trimmed.slice(0, slash).trim();
|
|
32
192
|
const modelPart = trimmed.slice(slash + 1).trim();
|
|
33
193
|
if (!provider || !modelPart) throw new SelectorError(`invalid selector "${input}": empty provider or model`);
|
|
34
|
-
if (modelPart.includes("
|
|
194
|
+
if (modelPart !== "*" && modelPart.includes("*")) throw new SelectorError(`invalid selector "${input}": unexpected wildcard in model`);
|
|
35
195
|
return {
|
|
36
196
|
provider,
|
|
37
197
|
model: modelPart === "*" ? void 0 : modelPart,
|
|
@@ -318,7 +478,7 @@ window.__ModuleLoader__.load({
|
|
|
318
478
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
319
479
|
}
|
|
320
480
|
/**
|
|
321
|
-
* Read a nested value by path — the
|
|
481
|
+
* Read a nested value by path — the upstream `dsh-client-schema-form`
|
|
322
482
|
* `getPath` semantics, copied locally so the provider-configured join needs no
|
|
323
483
|
* new dependency (array indexes as numeric keys, `undefined` along a missing
|
|
324
484
|
* branch).
|
|
@@ -4187,7 +4347,7 @@ window.__ModuleLoader__.load({
|
|
|
4187
4347
|
const connection = ctx.get("connection");
|
|
4188
4348
|
const sessions = ctx.get("sessions");
|
|
4189
4349
|
const controller = new FallbacksSettingsController(connection.api, connection.rpc);
|
|
4190
|
-
const useSnapshot =
|
|
4350
|
+
const useSnapshot = bindSnapshotSelector(controller.store);
|
|
4191
4351
|
ctx.effect(() => {
|
|
4192
4352
|
const syncSession = () => {
|
|
4193
4353
|
controller.setCurrentSession(sessions?.list.getSnapshot().current);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
2
|
+
export type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots';
|
|
3
|
+
/**
|
|
4
|
+
* Bind a bare observable source to a typed uSES selector hook.
|
|
5
|
+
* subscribe/getSnapshot are captured once per source into stable closures
|
|
6
|
+
* (also re-binds `this` for method-based sources), so components never
|
|
7
|
+
* resubscribe across renders. Equality defaults to Object.is.
|
|
8
|
+
* @param w - snapshot source (engine store, Session object, store instance).
|
|
9
|
+
* @returns the selector hook.
|
|
10
|
+
*/
|
|
11
|
+
export declare function bindSnapshotSelector<T>(w: HostObservable<T>): SnapshotSelectorHook<T>;
|