dsh-llm-fallbacks 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +119 -0
- package/bundle/cordis.patch.yml +19 -0
- package/dist/chains.d.ts +167 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +87 -0
- package/dist/client/FallbacksCard.d.ts +86 -0
- package/dist/client/GeneralFallbacksRow.d.ts +19 -0
- package/dist/client/fallbacks-store.d.ts +379 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +3916 -0
- package/dist/client/locales.d.ts +281 -0
- package/dist/client/switch-guard.d.ts +17 -0
- package/dist/commands.d.ts +176 -0
- package/dist/config.d.ts +136 -0
- package/dist/cooldown.d.ts +48 -0
- package/dist/events.d.ts +36 -0
- package/dist/gateway.d.ts +176 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1853 -0
- package/dist/roles.d.ts +70 -0
- package/dist/selectors.d.ts +35 -0
- package/dist/state.d.ts +112 -0
- package/package.json +91 -0
- package/scripts/build-client.ts +374 -0
- package/scripts/verify-dist.mjs +68 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-external
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# dsh-llm-fallbacks
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
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.
|
|
11
|
+
|
|
12
|
+
Install with a single command (pnpm ≥ 10 needs one build-allow step — see [Install](#install)):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
dsh plugin --profile web add github:btspoony/dsh-llm-fallbacks # pin a commit with #<sha>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **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.
|
|
21
|
+
- **Two-block config**: block 1 `rootChain` — the root agent's single fallback chain (empty = root does not fall back); block 2 declared roles — `roles.list` role entities (id/label/description/chain/fallback) that `roles.rules` reference by id (or the built-in `inherit`); no rule match → `inherit` → `rootChain`.
|
|
22
|
+
- **Entry syntax**: chain entries are `provider/model` (exact switch) or `provider/*` (keep the failed model id, switch provider only) — the old chain-key namespace (provider/model keys, role-name keys) is gone.
|
|
23
|
+
- **Cooldown and revert**: models that were switched away from / failed are not re-selected during the cooldown period; `revertPolicy: cooldown-expiry` automatically returns to the primary model when the cooldown expires, while `never` does not return within the session.
|
|
24
|
+
- **Visible behavior**: every switch appends a persisted session event `fallbacks/switch` (from/to/role/reason), alongside info-level logs (candidate attempt order and skip reasons) and the read-only status block on the Settings → 插件配置 → Fallbacks card — no silent model switching.
|
|
25
|
+
- **Safety valves**: switching stops and the original error semantics are kept once `maxSwitchesPerStep` is exceeded for a step, preventing chain loops from amplifying latency; `mode: 'always'` providers additionally have a retry cap (`alwaysModeRetryCap`).
|
|
26
|
+
- **No-config no-op**: `enabled` defaults to off (`false`); with no `rootChain`/role chains, unmatched trigger codes, or unresolved roles the plugin is a complete no-op — identical to not being installed, and no events are emitted.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
### One-line git install
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web add github:btspoony/dsh-llm-fallbacks # pin a commit with #<sha>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A git install fetches **sources, not built artifacts**, so the bundle builds itself on install (`prepare` self-build). The plugin is **mount-only**: it never modifies the dsh source tree, and no patch / postinstall step exists — dsh upgrades never require re-patching. pnpm ≥ 10 blocks a git dependency's `prepare` by default: the first `add` fails with `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`, and pnpm prints the exact package key. Allow the build in the profile's `pnpm-workspace.yaml` (`onlyBuiltDependencies: [dsh-llm-fallbacks]`, or run `dsh plugin --profile web approve-builds`), then re-run the `add`. Treat that allowance as permission to execute the package's code on your machine at install time, and pin a commit (`github:btspoony/dsh-llm-fallbacks#<sha>`) so a later push cannot silently change what runs. The full URL form works equivalently: `dsh plugin --profile web add https://github.com/btspoony/dsh-llm-fallbacks.git`.
|
|
37
|
+
|
|
38
|
+
### Local directory install (recommended for development / verification)
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
# 1) Build in the plugin repo (the prepare self-build runs the pnpm toolchain: tsdown + tsc, no bun)
|
|
42
|
+
pnpm install
|
|
43
|
+
# 2) Add to the target profile (example: web)
|
|
44
|
+
dsh plugin --profile web add .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **Development prerequisite**: type-checking and tests resolve the real
|
|
48
|
+
> `@deepseek-ai/*` packages (peer deps, host-provided at runtime) from the npm
|
|
49
|
+
> registry at `0.1.0-rc.6` — `autoInstallPeers` + the registry auth token in
|
|
50
|
+
> the user-level `~/.npmrc` (pnpm 11 no longer expands `${NPM_TOKEN}` from a
|
|
51
|
+
> project `.npmrc`), no local link farm.
|
|
52
|
+
|
|
53
|
+
> Both methods, uninstall, and `--dump-config` verification — including the bundle-layer ordering requirements — are covered in [docs/install.md](docs/install.md).
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
### Minimal configuration
|
|
58
|
+
|
|
59
|
+
Add a `fallbacks:` section to the dsh settings document (default `$DSH_HOME/settings.yaml`):
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
fallbacks:
|
|
63
|
+
enabled: true # feature switch; defaults to false — set explicitly to enable
|
|
64
|
+
rootChain: # block 1: root agent's chain; tried in order after the primary model fails
|
|
65
|
+
- anthropic/claude-3-5-sonnet
|
|
66
|
+
- openai/*
|
|
67
|
+
roles: # block 2: declare roles first, then let rules reference them
|
|
68
|
+
list:
|
|
69
|
+
- id: reviewer # role entity: unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
|
|
70
|
+
label: Reviewer
|
|
71
|
+
description: Code-review subagents
|
|
72
|
+
chain:
|
|
73
|
+
- openai/gpt-4o-mini
|
|
74
|
+
fallback: inherit-root # default: role chain, then append rootChain
|
|
75
|
+
rules:
|
|
76
|
+
- origin: subagent # all subagents → reviewer role (own chain + inherited root)
|
|
77
|
+
role: reviewer
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Roles are declared entities: `roles.list` holds role cards (id/label/description + optional chain/fallback), and `roles.rules` match origin/provider/model in order to a declared role id or the built-in `inherit` (first match wins) — **no rule match → `inherit` → `rootChain`**. A declared role is only ever hit when a rule references it. The legacy chain-key namespace and role-default field are gone (migration table: [docs/configuration.md](docs/configuration.md)).
|
|
81
|
+
|
|
82
|
+
Save and restart the web session for the changes to take effect. The feature switch `fallbacks.enabled` **defaults to off (`false`)** — the plugin only engages once it is turned on; `triggerCodes` defaults to `AUTH` / `QUOTA` / `RATE_LIMIT`; and with **no `rootChain`/role chains configured the behavior is identical to not having the plugin installed**. More examples (role entities, fallback strategies, rules referencing `inherit`) → [docs/configuration.md](docs/configuration.md).
|
|
83
|
+
|
|
84
|
+
> **Upgrade note (behavior change)**: an existing `fallbacks:` section **without an explicit `enabled` key** now resolves to `false` after upgrading — add `enabled: true` to keep the plugin active.
|
|
85
|
+
|
|
86
|
+
## `/fallbacks` command (in-session diagnostics)
|
|
87
|
+
|
|
88
|
+
Type `/fallbacks` in any session to inspect this session's fallback state — no need to open the settings page:
|
|
89
|
+
|
|
90
|
+
- **Session origin** (`root` / `subagent`) and the **resolved role** (the `role` of the first matching `roles.rules` entry, otherwise the built-in `inherit`);
|
|
91
|
+
- the **resolved chain** for that role (the role's own chain entries, annotated `(inherit-root)` when `rootChain` is appended — `rootChain` entries render in full only when the role has no own chain; `fallback: none` with an empty own chain, or no chain at all, → `not configured`);
|
|
92
|
+
- the **recent switches** (`fallbacks/switch` events, newest first, up to 5): from/to provider/model, role, reason;
|
|
93
|
+
- the **cooldown status**: which `provider/model` keys are currently suppressed and until when.
|
|
94
|
+
|
|
95
|
+
The command is **read-only** — it never mutates fallback state (no cooldown reset, no pending-switch writes). It registers through a conditional `commands` child, so it appears only when the host composes the slash-command registry — with no registry the command is silently unavailable (no top-level inject pollution). Output is zh by default (the host carries no per-session locale signal); the en dictionary lives in the same copy table.
|
|
96
|
+
|
|
97
|
+
## Mount-only (no dsh modification)
|
|
98
|
+
|
|
99
|
+
The plugin installs as a **pure mount** — it never modifies the dsh source tree:
|
|
100
|
+
|
|
101
|
+
- **Install = bundle insert + client inject + own gateway**: `bundle/cordis.patch.yml`
|
|
102
|
+
inserts the plugin row over the profile bundle stack, `dsh.client.inject` mounts
|
|
103
|
+
the Fallbacks card on the Settings → 插件配置 page, and settings read/write/reset
|
|
104
|
+
go through the plugin's own gateway channel (`/api/fallbacks/get|set|reset`).
|
|
105
|
+
- **No patches, no auto-apply step**: there are no dsh-body patch files and no install
|
|
106
|
+
lifecycle step that applies one. A one-line git install works as-is.
|
|
107
|
+
- **dsh upgrades never require re-patching**: a dsh upgrade that resets the source
|
|
108
|
+
tree changes nothing for this plugin — it keeps working without any re-apply step.
|
|
109
|
+
- **Stale leftover patches are harmless**: the plugin never depends on a patch
|
|
110
|
+
export (role resolution is rules-only; the model-selection marker coordination
|
|
111
|
+
was removed), so a previously patched dsh tree can be left as-is or manually
|
|
112
|
+
reverted — neither is required.
|
|
113
|
+
|
|
114
|
+
## Documentation
|
|
115
|
+
|
|
116
|
+
| Doc | Content |
|
|
117
|
+
|---|---|
|
|
118
|
+
| [docs/install.md](docs/install.md) | profile install / git install / uninstall / `--dump-config` verification |
|
|
119
|
+
| [docs/configuration.md](docs/configuration.md) | full `fallbacks` namespace reference, selector syntax, example YAML, plugin-config card usage, behavior notes |
|
|
120
|
+
| [docs/verification.md](docs/verification.md) | verification records (test matrix, bundle layer order, runtime contracts, QA gate script) |
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
Released under the **MIT** License — see [LICENSE](LICENSE). The LICENSE file is authoritative for copyright and license terms.
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# dsh-llm-fallbacks
|
|
2
|
+
|
|
3
|
+
[English](README.md) | [中文](README.zh-CN.md)
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
dsh(DeepSeek Harness)的自动模型降级插件:当 root agent 或 subagent 的模型请求持续失败(重试耗尽、权限、配额超限、限流 429)时,按角色/模型 fallback 链自动切换 provider/model,当前 step/turn 在目标模型上继续完成——任务不因模型问题中断。
|
|
11
|
+
|
|
12
|
+
一句话安装(pnpm ≥ 10 需一次构建放行,见 [安装](#安装)):
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
dsh plugin --profile web add github:btspoony/dsh-llm-fallbacks # 钉 commit:加 #<sha>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 能力一览
|
|
19
|
+
|
|
20
|
+
- **root / subagent 自动降级**:任意 agent 在模型故障下按链切换到下一个可用 provider/model,无需手动换模型。
|
|
21
|
+
- **两块制配置**:块 1 `rootChain`——root 主代理唯一降级链(空 = root 不降级);块 2 声明式角色——`roles.list` 角色实体(id/label/description/chain/fallback)供 `roles.rules` 按 id 引用(或内置 `inherit`);未命中规则 → `inherit` → `rootChain`。
|
|
22
|
+
- **条目语法**:链条目为 `provider/model`(精确切换)或 `provider/*`(保留失败模型 id 仅换 provider)——旧链键命名空间(provider/model 键、角色名键)已删除。
|
|
23
|
+
- **冷却与回主**:被切离/失败的模型在冷却期内不再入选;`revertPolicy: cooldown-expiry` 冷却到期后自动回主模型,`never` 会话内不回。
|
|
24
|
+
- **行为可见**:每次切换追加持久化会话事件 `fallbacks/switch`(from/to/role/reason),配合 info 级日志(候选尝试顺序与跳过原因)与设置 → 插件配置 → Fallbacks 卡片上的只读状态块,无静默换模型。
|
|
25
|
+
- **安全阀**:每 step 的 `maxSwitchesPerStep` 超限后停止切换、保持原错误语义,防止链循环放大延迟;`mode: 'always'` 的 provider 另有重试上限(`alwaysModeRetryCap`)。
|
|
26
|
+
- **无配置回归(no-op)**:`enabled` 默认关闭(`false`),无 `rootChain`/角色链 / 未命中触发码 / 角色解析失败时插件完全 no-op——行为与未安装时一致,不产生任何事件。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
### 一句话 git 安装
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
dsh plugin --profile web add github:btspoony/dsh-llm-fallbacks # 钉 commit:加 #<sha>
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
git 安装拉取的是**源码而非构建产物**,安装时由包自行构建(`prepare` 自构建)。插件为**纯挂载**:对 dsh 源码树零修改,无任何补丁 / postinstall 步骤——dsh 升级无需重打。pnpm ≥ 10 默认不执行 git 依赖的 `prepare`:第一次 `add` 会失败并打印 `ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED`,同时给出精确的包 key。在该 profile 的 `pnpm-workspace.yaml` 中放行构建(`onlyBuiltDependencies: [dsh-llm-fallbacks]`,或运行 `dsh plugin --profile web approve-builds`),然后重跑 `add`。请把这次放行当作它本来的样子:允许该包代码在安装期于你的机器上执行——建议钉 commit(`github:btspoony/dsh-llm-fallbacks#<sha>`),防止后续 push 悄悄改变实际运行的代码。完整 URL 形式等价:`dsh plugin --profile web add https://github.com/btspoony/dsh-llm-fallbacks.git`。
|
|
37
|
+
|
|
38
|
+
### 本地目录安装(开发/验证推荐)
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
# 1) 在插件仓库目录构建(prepare 自构建走 pnpm 工具链:tsdown + tsc,无 bun)
|
|
42
|
+
pnpm install
|
|
43
|
+
# 2) 装入目标 profile(示例为 web)
|
|
44
|
+
dsh plugin --profile web add .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> **开发前置**:类型检查与测试解析的是**真实** `@deepseek-ai/*` 包(peer 依赖,运行时由宿主提供),
|
|
48
|
+
> 来源为 npm registry 的 `0.1.0-rc.6`(`autoInstallPeers` + 用户级 `~/.npmrc` 的 registry
|
|
49
|
+
> 认证令牌——pnpm 11 起项目级 `.npmrc` 不再展开 `${NPM_TOKEN}`,无本地 link farm)。
|
|
50
|
+
|
|
51
|
+
> 两种安装方式、卸载与 `--dump-config` 验证(含 bundle 层顺序要求)详见 [docs/install.md](docs/install.md)。
|
|
52
|
+
|
|
53
|
+
## 快速开始
|
|
54
|
+
|
|
55
|
+
### 最小配置
|
|
56
|
+
|
|
57
|
+
在 dsh 的设置文档(默认 `$DSH_HOME/settings.yaml`)中添加 `fallbacks:` 分节:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
fallbacks:
|
|
61
|
+
enabled: true # 功能级开关;默认关闭(false),需显式打开后生效
|
|
62
|
+
rootChain: # 块 1:root 主代理降级链;主模型失败后按顺序尝试
|
|
63
|
+
- anthropic/claude-3-5-sonnet
|
|
64
|
+
- openai/*
|
|
65
|
+
roles: # 块 2:先声明角色,再让规则引用
|
|
66
|
+
list:
|
|
67
|
+
- id: reviewer # 角色实体:id 唯一(/^[a-z0-9-]{1,32}$/);inherit 为保留字
|
|
68
|
+
label: 审查者
|
|
69
|
+
description: 代码审查子代理
|
|
70
|
+
chain:
|
|
71
|
+
- openai/gpt-4o-mini
|
|
72
|
+
fallback: inherit-root # 默认:角色链后追加 rootChain
|
|
73
|
+
rules:
|
|
74
|
+
- origin: subagent # 所有 subagent → reviewer 角色(自身链 + 继承 root)
|
|
75
|
+
role: reviewer
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
角色是声明式实体:`roles.list` 存放角色卡(id/label/description + 可选 chain/fallback),`roles.rules` 按 origin/provider/model 顺序匹配到**已声明角色 id 或内置 `inherit`**(首个命中即停)——**未命中 → `inherit` → `rootChain`**。只声明不写规则的角色永不命中。旧链键命名空间与角色兜底字段已删除(迁移表见 [docs/configuration.md](docs/configuration.md))。
|
|
79
|
+
|
|
80
|
+
保存并重启 web 会话后生效。功能级开关 `fallbacks.enabled` **默认关闭(`false`)**——打开开关后插件才会介入;`triggerCodes` 默认覆盖 `AUTH` / `QUOTA` / `RATE_LIMIT`;**未配置任何 `rootChain`/角色链时行为与未安装插件完全一致**。更多示例(角色实体、fallback 策略、引用 `inherit` 的规则)见 [docs/configuration.md](docs/configuration.md)。
|
|
81
|
+
|
|
82
|
+
> **升级提示(行为变更)**:已有 `fallbacks:` 配置若**未显式写 `enabled` 键**,升级后解析为 `false`——请补上 `enabled: true` 以保持插件继续生效。
|
|
83
|
+
|
|
84
|
+
## `/fallbacks` 命令(会话内诊断)
|
|
85
|
+
|
|
86
|
+
在任意会话中键入 `/fallbacks` 即可查看当前会话的 fallback 状态,无需打开设置页:
|
|
87
|
+
|
|
88
|
+
- **会话来源**(`root` / `subagent`)与**解析角色**(首个命中规则的 `role`,未命中则内置 `inherit`);
|
|
89
|
+
- 该角色的**解析链**(角色自身链 + 继承的 `rootChain`,`fallback: none` 时仅角色链)——无链时显示「未配置」;
|
|
90
|
+
- **最近切换**(`fallbacks/switch` 事件,最新在前,至多 5 条):from/to provider/model、role、reason;
|
|
91
|
+
- **冷却状态**:哪些 `provider/model` 处于冷却、冷却至何时(`revertPolicy: 'never'` 显示「会话内不再回主」)。
|
|
92
|
+
|
|
93
|
+
命令**只读**——绝不修改 fallback 状态(不重置冷却、不写待应用切换)。它经条件 `commands` 子注入注册,仅在宿主组合了斜杠命令注册表时出现;无注册表时命令静默不可用(无顶层 inject 污染)。输出默认中文(宿主侧无会话级 locale 信号);英文词典在同一副本表中。
|
|
94
|
+
|
|
95
|
+
## 纯挂载(零 dsh 修改)
|
|
96
|
+
|
|
97
|
+
插件以**纯挂载**方式安装,**从不修改 dsh 源码树**:
|
|
98
|
+
|
|
99
|
+
- **安装 = bundle 行插入 + client inject + 自有 gateway**:`bundle/cordis.patch.yml`
|
|
100
|
+
把插件行插入 profile bundle 栈,`dsh.client.inject` 在设置 → 插件配置页挂载
|
|
101
|
+
Fallbacks 卡片,设置读写/重置
|
|
102
|
+
走插件自有 gateway 通道(`/api/fallbacks/get|set|reset`)。
|
|
103
|
+
- **无补丁、无自动打补丁**:没有 dsh 本体 patch 文件,也没有任何安装期 apply 步骤;
|
|
104
|
+
一句话 git 安装即可用。
|
|
105
|
+
- **dsh 升级永不需重打**:dsh 升级重置源码树对本插件无影响——无需任何重打步骤。
|
|
106
|
+
- **残留旧补丁无害**:插件不依赖任何补丁导出(角色解析 rules-only;model-selection
|
|
107
|
+
标记协调已移除)——已打过旧补丁的 dsh 树可原样保留或手动回滚,均非必需。
|
|
108
|
+
|
|
109
|
+
## 文档
|
|
110
|
+
|
|
111
|
+
| 文档 | 内容 |
|
|
112
|
+
|---|---|
|
|
113
|
+
| [docs/install.md](docs/install.md) | profile 安装 / git 安装 / 卸载 / `--dump-config` 验证 |
|
|
114
|
+
| [docs/configuration.md](docs/configuration.md) | `fallbacks` 命名空间全字段、selector 语法、示例 YAML、插件配置卡使用、行为说明 |
|
|
115
|
+
| [docs/verification.md](docs/verification.md) | 验证记录(测试矩阵、bundle 层序、运行契约、QA gate 剧本) |
|
|
116
|
+
|
|
117
|
+
## 许可
|
|
118
|
+
|
|
119
|
+
本项目以 **MIT** 许可证发布,全文见 [LICENSE](LICENSE)。版权与许可条款以 LICENSE 文件为准。
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# The dsh-llm-fallbacks profile-bundle patch: ONE insert of the plugin row over
|
|
2
|
+
# the profile's bundle stack (`dsh.profile.bundles` order — dsh-base first,
|
|
3
|
+
# this bundle after the llm-retry bundle; later layers win per row).
|
|
4
|
+
#
|
|
5
|
+
# Insert position matters for the host waterfall: the profile must list the
|
|
6
|
+
# llm-retry bundle BEFORE dsh-llm-fallbacks, so this plugin's
|
|
7
|
+
# `agent/request-error` listener composes after llm-retry and only intervenes
|
|
8
|
+
# once retry policy is exhausted (normal mode) or delegates downstream first
|
|
9
|
+
# (always mode). See plan Risk table.
|
|
10
|
+
#
|
|
11
|
+
# A patch replaces the targeted row's whole `config` rather than merging into
|
|
12
|
+
# it, so a user-level override must restate every field it keeps. The row
|
|
13
|
+
# ships only neutral defaults; profile-layer (`cordis.patch.yml`) overrides
|
|
14
|
+
# win.
|
|
15
|
+
|
|
16
|
+
- insert:
|
|
17
|
+
- id: llm-fallbacks
|
|
18
|
+
name: 'dsh-llm-fallbacks'
|
|
19
|
+
config: {}
|
package/dist/chains.d.ts
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback chain resolution (spec §7.2; plan fallbacks-role-runtime Task 1).
|
|
3
|
+
*
|
|
4
|
+
* `resolveChain` returns the ordered candidate list for a failing
|
|
5
|
+
* (provider, model). Candidates come from the role's own chain plus
|
|
6
|
+
* `rootChain` appended as an inherit-root fallback — **append-not-replace**:
|
|
7
|
+
* `[...(roleDef.chain ?? []), ...(roleDef.fallback === 'none' ? [] :
|
|
8
|
+
* rootChain)]` (role entries first, rootChain entries after). The reserved
|
|
9
|
+
* built-in `inherit` id (INHERIT_ROLE_ID) resolves to `rootChain` silently —
|
|
10
|
+
* it is the legal no-rule-match role, not a typo'd id. A truly undeclared
|
|
11
|
+
* role id (defense) resolves to `rootChain` alone + one warn. Chain-key
|
|
12
|
+
* specificity lookup (`exact` / `provider/*` / role / `default` keys) is
|
|
13
|
+
* deleted — the two-block config model has no chain keys.
|
|
14
|
+
*
|
|
15
|
+
* Wildcard entries (`provider/*`) are resolved against the failing model —
|
|
16
|
+
* keep the model id, swap only the provider; the "target provider has no
|
|
17
|
+
* such model id" skip is judged by {@link resolveCandidate} (via
|
|
18
|
+
* `modelExists`) or by the caller's filter.
|
|
19
|
+
*
|
|
20
|
+
* The concatenation itself lives in ONE helper — {@link buildRoleEntries}
|
|
21
|
+
* (qc1 S-3 SSOT) — shared by {@link resolveChainViews} and
|
|
22
|
+
* {@link hasWildcardEntry}, so the wildcard probe is exact (qc2 F-003:
|
|
23
|
+
* `fallback: 'none'` excludes `rootChain`) and can never diverge from the
|
|
24
|
+
* resolution. Defensive warns (unknown role / undeclared rule target) go
|
|
25
|
+
* through an injected `warn` callback — the wiring passes the cordis
|
|
26
|
+
* `logger.warn` (qc2 F-002 / qc3 S-3), with `console.warn` as the
|
|
27
|
+
* direct-call default.
|
|
28
|
+
*
|
|
29
|
+
* Cooldown / failed-set / same-as-current filtering is caller-side;
|
|
30
|
+
* {@link createCandidateFilter} provides the ready-made predicate.
|
|
31
|
+
*
|
|
32
|
+
* @module dsh-llm-fallbacks/chains
|
|
33
|
+
*/
|
|
34
|
+
import { type FallbacksRole } from './config.ts';
|
|
35
|
+
import type { CooldownStore, StepFailureSet } from './cooldown.ts';
|
|
36
|
+
import { type Selector } from './selectors.ts';
|
|
37
|
+
/** The model that just failed — the anchor for chain resolution. */
|
|
38
|
+
export interface FailingModel {
|
|
39
|
+
provider: string;
|
|
40
|
+
model: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve one chain entry against the failing model.
|
|
44
|
+
*
|
|
45
|
+
* - `provider/model` → that exact selector (not subject to `modelExists` —
|
|
46
|
+
* the user explicitly listed it).
|
|
47
|
+
* - `provider/*` → keeps the failing model id, swaps only the provider; when
|
|
48
|
+
* `modelExists` is given and the target provider has no such model id →
|
|
49
|
+
* `null` (skip).
|
|
50
|
+
* - Malformed entries → `null`: they "do not take effect"; the strict throw
|
|
51
|
+
* path is {@link parseSelector}, used by the Task 3 config-warning path.
|
|
52
|
+
*/
|
|
53
|
+
export declare function resolveCandidate(entry: string, failing: FailingModel, modelExists?: (provider: string, model: string) => boolean): Selector | null;
|
|
54
|
+
/**
|
|
55
|
+
* Single-pass chain resolution for the decision path (T1 review Important
|
|
56
|
+
* #2). Walks the role's concatenated entries ONCE and returns the unfiltered
|
|
57
|
+
* view — `all` (every resolvable candidate, no filter and no existence
|
|
58
|
+
* probe: the caller's early-exit / annotation view) — plus the parallel
|
|
59
|
+
* `wildcard` provenance (`wildcard[i]` = candidate `all[i]` came from a
|
|
60
|
+
* `provider/*` entry). The caller builds the existence probe from `all` and
|
|
61
|
+
* then derives the surviving view with {@link selectCandidates}, so the
|
|
62
|
+
* decision path never resolves the chain twice (and an unknown role warns
|
|
63
|
+
* at most once per decision).
|
|
64
|
+
*
|
|
65
|
+
* The reserved built-in `inherit` id (INHERIT_ROLE_ID) is legal — it
|
|
66
|
+
* resolves to `rootChain` silently (the no-rule-match role, not a typo'd
|
|
67
|
+
* id). Only a truly undeclared id (∉ declared ids ∪ {'inherit'}) warns once
|
|
68
|
+
* (defensive — never crashes) through `warn` (defaults to `console.warn`
|
|
69
|
+
* for direct-call compatibility; the decision path injects the cordis
|
|
70
|
+
* `logger.warn` — qc2 F-002 / qc3 S-3). Malformed entries never become
|
|
71
|
+
* candidates.
|
|
72
|
+
*/
|
|
73
|
+
export declare function resolveChainViews(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string, provider: string, model: string, warn?: (message: string) => void): {
|
|
74
|
+
all: Selector[];
|
|
75
|
+
wildcard: boolean[];
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Derive the surviving candidates from a {@link resolveChainViews} pass:
|
|
79
|
+
* the caller filter plus the wildcard-only existence probe. `modelExists`
|
|
80
|
+
* applies to `provider/*`-origin candidates only (scoped by the parallel
|
|
81
|
+
* `wildcard` provenance — T2 review Important #1: exact entries are never
|
|
82
|
+
* existence-filtered), mirroring how {@link resolveCandidate} receives it.
|
|
83
|
+
* Order and duplicates are preserved.
|
|
84
|
+
*/
|
|
85
|
+
export declare function selectCandidates(all: readonly Selector[], wildcard: readonly boolean[], filter?: (candidate: Selector) => boolean, modelExists?: (provider: string, model: string) => boolean): Selector[];
|
|
86
|
+
/**
|
|
87
|
+
* Ordered fallback candidates for the failing (provider, model).
|
|
88
|
+
*
|
|
89
|
+
* Candidates are the role's concatenated entries (spec §7.2, see
|
|
90
|
+
* {@link buildRoleEntries} — the single concatenation SSOT): the declared
|
|
91
|
+
* role's own `chain` followed by `rootChain` unless `fallback: 'none'`
|
|
92
|
+
* (append-not-replace — role entries first, rootChain as the tail). The
|
|
93
|
+
* reserved built-in `inherit` id resolves to `rootChain` silently; a truly
|
|
94
|
+
* undeclared/unknown `role` id resolves to `rootChain` alone and warns once
|
|
95
|
+
* (defensive — never crashes). `filter` optionally drops candidates — the
|
|
96
|
+
* caller owns cooldown/failed-set/same-model filtering (see
|
|
97
|
+
* {@link createCandidateFilter}). The `modelExists` existence probe is NOT
|
|
98
|
+
* applied per entry here: {@link resolveChainViews} returns the parallel
|
|
99
|
+
* `wildcard` provenance, and {@link selectCandidates} applies `modelExists`
|
|
100
|
+
* to `provider/*`-origin candidates ONLY (qc1 S-2 — exact entries are never
|
|
101
|
+
* existence-filtered, spec §2 clause 2 / T2 review Important #1
|
|
102
|
+
* decision-path contract). `warn` forwards to {@link resolveChainViews}
|
|
103
|
+
* (defaults to `console.warn`; the decision path injects `logger.warn`).
|
|
104
|
+
*
|
|
105
|
+
* Single-pass: delegates to {@link resolveChainViews} + {@link selectCandidates}
|
|
106
|
+
* (one concatenated-entry walk, warn at most once per call).
|
|
107
|
+
*/
|
|
108
|
+
export declare function resolveChain(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string, provider: string, model: string, filter?: (candidate: Selector) => boolean, modelExists?: (provider: string, model: string) => boolean, warn?: (message: string) => void): Selector[];
|
|
109
|
+
/**
|
|
110
|
+
* Whether any entry of the role's concatenated candidates is a wildcard
|
|
111
|
+
* (`provider/*`). F-002: {@link resolveChain} resolves wildcard entries to
|
|
112
|
+
* concrete models, so the wildcard provenance is invisible on the resolved
|
|
113
|
+
* candidate list — the decision path consults the same concatenation
|
|
114
|
+
* `resolveChain` walks to decide whether the catalog existence probe is
|
|
115
|
+
* needed at all.
|
|
116
|
+
*
|
|
117
|
+
* The probe walks {@link buildRoleEntries} — the SAME concatenation
|
|
118
|
+
* {@link resolveChainViews} resolves (qc1 S-3 single SSOT) — so it is
|
|
119
|
+
* exact, never an over-approximation (qc2 F-003 / qc3 S-1): for
|
|
120
|
+
* `fallback: 'none'` roles `rootChain` is excluded, and a wildcard that can
|
|
121
|
+
* never reach the candidate list never builds a catalog probe. No
|
|
122
|
+
* false-negative is possible: any wildcard the resolution could reach is on
|
|
123
|
+
* this concatenation by construction. Malformed entries never become
|
|
124
|
+
* candidates and are skipped.
|
|
125
|
+
*/
|
|
126
|
+
export declare function hasWildcardEntry(roles: readonly FallbacksRole[], rootChain: readonly string[], role: string): boolean;
|
|
127
|
+
/** Inputs for {@link createCandidateFilter}. */
|
|
128
|
+
export interface CandidateFilterOptions {
|
|
129
|
+
/** The currently active (failing) model — skipped as "same as current". */
|
|
130
|
+
current: FailingModel;
|
|
131
|
+
/** Cooldown store: suppressed candidates are skipped (keyed `provider/model`). */
|
|
132
|
+
cooldown: Pick<CooldownStore, 'isSuppressed'>;
|
|
133
|
+
/** Failed-set for the current step: already-failed candidates are skipped. */
|
|
134
|
+
failed: Pick<StepFailureSet, 'has'>;
|
|
135
|
+
/** Optional existence probe: candidates the target provider lacks are skipped. */
|
|
136
|
+
modelExists?: (provider: string, model: string) => boolean;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* The caller-side candidate filter (Task 3): a candidate is usable when it
|
|
140
|
+
* differs from the current model, is not cooldown-suppressed, has not failed
|
|
141
|
+
* in this step, and (when `modelExists` is given) exists on its provider.
|
|
142
|
+
*/
|
|
143
|
+
export declare function createCandidateFilter(options: CandidateFilterOptions): (candidate: Selector) => boolean;
|
|
144
|
+
/** Why one considered candidate was excluded from the selection (spec §2 行为可见性). */
|
|
145
|
+
export type CandidateSkipReason = 'same-as-current' | 'cooldown' | 'step-failed' | 'missing-id';
|
|
146
|
+
/** One entry of the ordered, per-candidate annotation (T3 review Minor 1). */
|
|
147
|
+
export interface AnnotatedCandidate {
|
|
148
|
+
candidate: Selector;
|
|
149
|
+
/** Why this candidate was excluded; `undefined` = it survived every exclusion. */
|
|
150
|
+
skip?: CandidateSkipReason;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Annotate the ordered "considered" candidate list with each candidate's skip
|
|
154
|
+
* reason (spec §2 行为可见性: the switch log must show the attempt order and
|
|
155
|
+
* why each candidate was skipped). The selection-relevant view is the
|
|
156
|
+
* `surviving` list the decision path resolved with filter + existence probe:
|
|
157
|
+
* a candidate absent from it failed one of the exclusions, and the concrete
|
|
158
|
+
* reason is derived from the same checks {@link createCandidateFilter}
|
|
159
|
+
* applies, in the same precedence. `missing-id` therefore only ever labels
|
|
160
|
+
* entries the existence probe dropped — exact entries are never
|
|
161
|
+
* existence-probed (T2 contract), so they stay in `surviving` and are
|
|
162
|
+
* reported as usable, never as missing-id.
|
|
163
|
+
*
|
|
164
|
+
* This is the diagnostic counterpart of {@link createCandidateFilter} — pure,
|
|
165
|
+
* order-preserving, and duplicate-preserving.
|
|
166
|
+
*/
|
|
167
|
+
export declare function annotateCandidates(candidates: readonly Selector[], surviving: readonly Selector[], options: Pick<CandidateFilterOptions, 'current' | 'cooldown' | 'failed'>): AnnotatedCandidate[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation-level fallback-switch visibility (plan fallbacks-aux-seams,
|
|
3
|
+
* task 2, D1+D2 seam).
|
|
4
|
+
*
|
|
5
|
+
* Every `fallbacks/switch` session event becomes its own chat-transcript
|
|
6
|
+
* node (`fallbacks-switch`), rendered as a compact system-style line at the
|
|
7
|
+
* switch's event seq — the user sees the recovery happen in place
|
|
8
|
+
* (provider/model A → B, role · reason), instead of the event existing only
|
|
9
|
+
* in the raw `sessions.history` event feed (it is NOT a SurfaceEventType,
|
|
10
|
+
* so the `unknown-surface` fallback never picked it up and the transcript
|
|
11
|
+
* showed nothing).
|
|
12
|
+
*
|
|
13
|
+
* Contract notes (dsh-private, verified 2026-08-12):
|
|
14
|
+
* - D1 registry: `ConversationEventRegistry.register(definition)` — service
|
|
15
|
+
* on the client Context (`runtime/src/client/index.ts:171,189-192`);
|
|
16
|
+
* external registration precedent `ui-workflow-run/src/client/index.ts:18-28`.
|
|
17
|
+
* The engine feeds EVERY session event to each definition's `match`
|
|
18
|
+
* (`runtime/src/client/sessions/conversation-assembler.ts:370-382`) —
|
|
19
|
+
* non-surface plugin events included — and the client session appends live
|
|
20
|
+
* events into the engine (`sessions/session.ts:673` `conversation.append`).
|
|
21
|
+
* - D2 seat: `conversation.chat.node` is a keyed seat dispatched by
|
|
22
|
+
* `ChatConversationViewNode.kind` (`ui-conversation contract/slots.ts:56-63`;
|
|
23
|
+
* `chat/ChatNodeSeat.tsx:48-51`), externally registrable as
|
|
24
|
+
* `{ name, key, locale }` (precedents: ui-tool `tool-call`, ui-goal
|
|
25
|
+
* `command-input`, ui-workflow-run `workflow-run`).
|
|
26
|
+
* - Purity: this file only type-imports `@deepseek-ai/dsh-client-runtime/client`
|
|
27
|
+
* and `@deepseek-ai/dsh-client-ui-conversation/client` (both erased at
|
|
28
|
+
* build); the renderer self-draws on `--dsw-alias-*` tokens. Render-only:
|
|
29
|
+
* the Definition is a pure view contribution — no message construction,
|
|
30
|
+
* no model-context injection (C4 excluded by scope).
|
|
31
|
+
*/
|
|
32
|
+
import type { ReactNode } from 'react';
|
|
33
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
34
|
+
import type { ConversationNodeDefinition } from '@deepseek-ai/dsh-client-runtime/client';
|
|
35
|
+
import type { FallbackSwitchReason } from '../events.ts';
|
|
36
|
+
/** Final chat payload of one decided fallback switch (snapshot of the event). */
|
|
37
|
+
export interface FallbacksSwitchChatData {
|
|
38
|
+
readonly seq: number;
|
|
39
|
+
readonly time: number;
|
|
40
|
+
readonly turn: number;
|
|
41
|
+
readonly step: number;
|
|
42
|
+
/** The model the request was using when the switch was decided. */
|
|
43
|
+
readonly from: {
|
|
44
|
+
readonly provider: string;
|
|
45
|
+
readonly model: string;
|
|
46
|
+
};
|
|
47
|
+
/** The chain candidate the switch moves to. */
|
|
48
|
+
readonly to: {
|
|
49
|
+
readonly provider: string;
|
|
50
|
+
readonly model: string;
|
|
51
|
+
};
|
|
52
|
+
/** The fallback-chain role the decision resolved for the agent. */
|
|
53
|
+
readonly role: string;
|
|
54
|
+
readonly reason: FallbackSwitchReason;
|
|
55
|
+
}
|
|
56
|
+
declare module '@deepseek-ai/dsh-client-ui-conversation/client' {
|
|
57
|
+
interface ChatNodeDataMap {
|
|
58
|
+
/** One decided fallback provider/model switch, rendered at its event seq. */
|
|
59
|
+
'fallbacks-switch': FallbacksSwitchChatData;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* One switch event → one chat node. Each `fallbacks/switch` event is its own
|
|
64
|
+
* Context (id = event seq — the durable unique key), so every match is a
|
|
65
|
+
* `start`; `update` is a passthrough (no aggregation — D3's per-Turn
|
|
66
|
+
* counting is a separate, unselected seam).
|
|
67
|
+
*/
|
|
68
|
+
export declare const fallbackSwitchDefinition: ConversationNodeDefinition<FallbacksSwitchChatData>;
|
|
69
|
+
/**
|
|
70
|
+
* Props delivered by the keyed chat-node seat: runtime share + the `fallbacks` locale seat.
|
|
71
|
+
*/
|
|
72
|
+
export type ConversationFallbackSwitchProps = PropsRuntime<'conversation.chat.node', 'fallbacks-switch'> & PropsLocale<'fallbacks'>;
|
|
73
|
+
/**
|
|
74
|
+
* Render one fallback switch as a compact system-style transcript line.
|
|
75
|
+
*
|
|
76
|
+
* Geometry follows the upstream chat system rows (the compaction boundary
|
|
77
|
+
* notice: dim title + separator + ellipsized summary — `chat/MessageItem
|
|
78
|
+
* .module.css:38-122`); every color resolves through a `--dsw-alias-*`
|
|
79
|
+
* token. A reason outside the current union renders raw (forward-compatible
|
|
80
|
+
* durable log, same rule as the card/general row summaries). A malformed or
|
|
81
|
+
* partial payload (version skew) degrades to the title-only line instead of
|
|
82
|
+
* throwing during interpolation — the transcript slot stays visible with a
|
|
83
|
+
* truthful "a switch happened" notice and no summary details.
|
|
84
|
+
* @param props - composed keyed seat props.
|
|
85
|
+
* @returns the switch line element tree.
|
|
86
|
+
*/
|
|
87
|
+
export declare function ConversationFallbackSwitch({ node, t }: ConversationFallbackSwitchProps): ReactNode;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallbacks settings card — the `fallbacks` plugin card on the web settings
|
|
3
|
+
* "插件配置" page (spec §4). Registered into the `settings.plugin.item` slot
|
|
4
|
+
* (id `fallbacks`, order 30, alongside the upstream bash/agent-loop/web-search
|
|
5
|
+
* cards and the advisor card); owner props are empty and all data flows
|
|
6
|
+
* through {@link FallbacksSettingsController}.
|
|
7
|
+
*
|
|
8
|
+
* The card chrome replicates the upstream `PluginCard` contract (self-drawn:
|
|
9
|
+
* the upstream client value face exports no reusable card): a collapsible
|
|
10
|
+
* `<li>` whose header is a button stacking the plugin name over its
|
|
11
|
+
* description, with a dirty "unsaved" pill and a rotating chevron
|
|
12
|
+
* (`IconChevronDownOutline14` from ui-primitives — a CLIENT_EXTERNALS value
|
|
13
|
+
* import), `aria-expanded`/`aria-label` like the upstream header; a divider
|
|
14
|
+
* under the header; then the form content; then a footer with
|
|
15
|
+
* Discard / Reset / Save carrying the upstream disabled semantics — save =
|
|
16
|
+
* `!dirty || saving || !writable`, discard = `!dirty || saving` (KD-U1).
|
|
17
|
+
* Disclosure is card-local state: which card a user has open is a reading
|
|
18
|
+
* gesture, and staged edits outlive collapsing — the pill rides the header
|
|
19
|
+
* (upstream rationale).
|
|
20
|
+
*
|
|
21
|
+
* The form body is the two-block editing surface (spec §8): the `enabled`
|
|
22
|
+
* checkbox row, the 6 top-level scalar fields (trigger codes / revert
|
|
23
|
+
* policy / three numeric fields), the `rootChain` block (block 1 — the
|
|
24
|
+
* root agent's single chain, no key input), and the roles block (block 2 —
|
|
25
|
+
* declared role entity cards from `roles.list` plus the rule rows from
|
|
26
|
+
* `roles.rules`, whose role field is a dropdown bound to the declared ids
|
|
27
|
+
* + the built-in `inherit`, same-page live). Saving runs `validateDraft`
|
|
28
|
+
* first — id format/reserved word/duplicates, undeclared rule role
|
|
29
|
+
* references, and illegal selectors block the write with a validation
|
|
30
|
+
* banner + inline red borders (never touching the store error path); a
|
|
31
|
+
* non-empty `state.legacyKeys` renders the migration banner at the top of
|
|
32
|
+
* the card body. The row editors keep their filled editorCard surface
|
|
33
|
+
* inside the card, with `--dsw-alias-*` tokens throughout. The reset-
|
|
34
|
+
* to-defaults confirmation stays a `Modal` (the delete-confirm pattern of
|
|
35
|
+
* the Models page) — no `window.confirm`.
|
|
36
|
+
*
|
|
37
|
+
* The page-only chrome is gone (720px column wrapper, title/intro banners,
|
|
38
|
+
* page-bottom status block): the AC-7 read-only status (derived effective
|
|
39
|
+
* model + recent-switch summary) is folded into the card body above the
|
|
40
|
+
* footer, and the plugin-config section owns the column width.
|
|
41
|
+
*
|
|
42
|
+
* Degraded/error/loading states keep the same card chrome (KD-U3): the
|
|
43
|
+
* header always renders title+description+chevron, and the body carries the
|
|
44
|
+
* config-channel notice or the load error. A card that cannot reach the
|
|
45
|
+
* `fallbacks/get` gateway channel (`ready && !present`) keeps the USABLE
|
|
46
|
+
* skeleton — the form stays writable and saves are attempted (KD-G5) — with
|
|
47
|
+
* the `unavailable` notice ALWAYS visible (derived open — the header cannot
|
|
48
|
+
* collapse it away), while a healthy card is collapsed until the user
|
|
49
|
+
* expands it (AC-1, the documented divergence from upstream whose
|
|
50
|
+
* unavailable card renders nothing). A hard load failure (`status ===
|
|
51
|
+
* 'error'`) also forces the body open with an error notice and — when the
|
|
52
|
+
* form is inert (`!writable`, i.e. the load never landed) — a Retry button;
|
|
53
|
+
* a save failure keeps the editable form so the Save action itself is the
|
|
54
|
+
* retry (the single `state.error` surface covers both, unlike the advisor's
|
|
55
|
+
* separate apply-failure hints).
|
|
56
|
+
*
|
|
57
|
+
* The degraded derivation is latched in the card (the store stays untouched):
|
|
58
|
+
* `present` only ever changes inside the store's `accept()`, so the settled
|
|
59
|
+
* `ready` read is authoritative, and a card-local latch carries that value
|
|
60
|
+
* through refresh/save windows (`loading`/`saving`) so the notice body can
|
|
61
|
+
* never collapse mid-refresh (the advisor's latched `degraded` field,
|
|
62
|
+
* implemented without a store change); on a first mount the latch is false,
|
|
63
|
+
* so the healthy card starts (and stays) collapsed through its first load.
|
|
64
|
+
*/
|
|
65
|
+
import type { ReactNode } from 'react';
|
|
66
|
+
import type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
67
|
+
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react';
|
|
68
|
+
import { FallbacksSettingsController, type FallbacksSettingsState } from './fallbacks-store.ts';
|
|
69
|
+
/** Injected dependencies of {@link FallbacksCard} (slot `inject`). */
|
|
70
|
+
export interface FallbacksCardInjected {
|
|
71
|
+
/** The card store (loaded on mount, refreshed on pushed invalidations). */
|
|
72
|
+
controller: FallbacksSettingsController;
|
|
73
|
+
/** uSES subscription hook bound to the store (inject face — advisor pattern). */
|
|
74
|
+
useSnapshot: SnapshotSelectorHook<FallbacksSettingsState>;
|
|
75
|
+
}
|
|
76
|
+
/** Props delivered by the slot outlet: runtime share + locale seat + inject face. */
|
|
77
|
+
export type FallbacksCardProps = PropsRuntime<'settings.plugin.item'> & PropsLocale<'fallbacks'> & FallbacksCardInjected;
|
|
78
|
+
/**
|
|
79
|
+
* Render the Fallbacks settings card inside the plugin-config section,
|
|
80
|
+
* replicating the upstream PluginCard chrome (KD-U1). The body carries the
|
|
81
|
+
* existing form content unchanged plus the folded-in status block and the
|
|
82
|
+
* footer actions (Discard / Reset / Save).
|
|
83
|
+
* @param props - slot-delivered injected dependencies and the synthesized t seat.
|
|
84
|
+
* @returns the card.
|
|
85
|
+
*/
|
|
86
|
+
export declare function FallbacksCard({ controller, useSnapshot, t }: FallbacksCardProps): ReactNode;
|