dsh-llm-fallbacks 0.3.1 → 0.3.3

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 CHANGED
@@ -6,12 +6,12 @@
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
8
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
9
- ![dsh](https://img.shields.io/badge/DSH-0.1.0--rc.8-4B32C3.svg)
9
+ ![dsh](https://img.shields.io/badge/DSH-0.1.1--rc.1-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=en)](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 → Plugins → Fallbacks card) and the **dsh-tui** terminal profile (`/fallbacks` + `/fallbacks config`).
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
 
@@ -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 dsh settings document (default `$DSH_HOME/settings.yaml`):
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; defaults to false — set explicitly to enable
67
- rootChain: # all-day chain: LAST entry is Default model (official V4);
68
- - anthropic/claude-3-5-sonnet # leading entries = Default fallback chain (walked first)
69
- - deepseek-official/deepseek-v4-flash # last = Flash XOR Pro last-resort fallback
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 windows; only the model chain is editable (locks tz to Asia/Shanghai)
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,24 +91,36 @@ 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: # block 2: declare role entities first, then let rules reference them
94
+ roles: # optional: declare role entities, then reference them from rules
83
95
  list:
84
- - id: reviewer # unique id (/^[a-z0-9-]{1,32}$/); "inherit" is reserved
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 # default: role chain, then append rootChain
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 (own chain + inherited root)
102
+ - role: reviewer # all subagents → the reviewer role
91
103
  ```
92
104
 
93
- No rule match (or a root request) → the built-in `inherit` → `rootChain`. `enabled` defaults to **off** — with no chains configured the plugin is a complete no-op. The all-day `rootChain` must END with exactly one official V4 model (`deepseek-official/deepseek-v4-flash` or `deepseek-official/deepseek-v4-pro`) — the settings card and gateway reject any other tail on save (a legacy non-official-tail chain warns at startup and keeps working as a fallback-only walk, but cannot be saved as-is). Full reference (role entities, fallback strategies, rules, selectors, preset roles, time-slot presets) → [docs/configuration.md](docs/configuration.md).
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 `fallbacks/switch` events, cooldown status). The plugin no longer writes durable `fallbacks/switch` session events (issue #52 — the apply()-time registration was proven ineffective), so new switches show up in the info logs, not in the recent-switch surfaces; sessions written by older plugin versions that contain `fallbacks/switch` events are repaired with `scripts/repair-fallbacks-switch-logs.ts`, which marks legacy events ignorable so those sessions load again (see the Features note below). In a dsh-tui profile, `/fallbacks config` additionally reads back the composed configuration (the TUI has no settings page — config is file-only; see [docs/configuration.md](docs/configuration.md)).
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
 
@@ -108,11 +132,23 @@ Save and restart the session, then type `/fallbacks` — the read-only in-sessio
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**: `enabled` defaults to off; with no chains configured the plugin behaves exactly like not being installed.
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 TUI settings page or host patch is involved. 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).
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
 
package/README.zh-CN.md CHANGED
@@ -6,12 +6,12 @@
6
6
  ![node](https://img.shields.io/badge/node-%3E%3D22-339933.svg)
7
7
  ![pnpm](https://img.shields.io/badge/pnpm-%3E%3D10-f69220.svg)
8
8
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
9
- ![dsh](https://img.shields.io/badge/DSH-0.1.0--rc.8-4B32C3.svg)
9
+ ![dsh](https://img.shields.io/badge/DSH-0.1.1--rc.1-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-llm-fallbacks?lang=zh)](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` + `/fallbacks config`)。
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
- 在 dsh 的设置文档(默认 `$DSH_HOME/settings.yaml`)中添加 `fallbacks:` 分节:
74
+ 在共享设置文档(`$DSH_HOME/settings.yaml`——见 [配置界面](#配置界面))中添加 `fallbacks:` 分节:
63
75
 
64
76
  ```yaml
65
77
  fallbacks:
66
- enabled: true # 功能级开关;默认关闭(false),需显式打开后生效
67
- rootChain: # 全时段链:最后一项是默认模型(官方 V4)
68
- - anthropic/claude-3-5-sonnet # 前面是默认降级链(先走)
69
- - deepseek-official/deepseek-v4-flash # 最后一档:Flash 或 Pro
70
- timeSlots: # 可选:按墙钟时段轮换 root 生效链
71
- - kind: preset # 冻结的 UTC+8 窗口;仅模型链可编辑(锁定 tz 为 Asia/Shanghai)
72
- preset: liang-peak # 09:00–12:00 与 14:00–18:00,每天
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] # 可选;缺省/空 = 每天(0=周日…6=周六)
91
+ days: [1, 5] # 可选;缺省/空 = 每天(0=周日…6=周六)
80
92
  chain:
81
93
  - openai/gpt-4o
82
- roles: # 块 2:先声明角色,再让规则引用
94
+ roles: # 可选:先声明角色实体,再由规则引用
83
95
  list:
84
- - id: reviewer # 角色实体:id 唯一(/^[a-z0-9-]{1,32}$/);inherit 为保留字
96
+ - id: reviewer # 唯一 id;"inherit" 为保留字
85
97
  persona: 代码审查子代理
86
98
  chain:
87
99
  - openai/gpt-4o-mini
88
- fallback: inherit-root # 默认:角色链后追加 rootChain
89
- rules: # 仅对子代理生效:规则不匹配 root 请求
90
- - role: reviewer # 所有 subagent → reviewer 角色(自身链 + 继承 root)
100
+ fallback: inherit-root # 先走角色链,再追加继承的 rootChain
101
+ rules: # 仅对子代理生效:规则不匹配 root 请求
102
+ - role: reviewer # 所有 subagent → reviewer 角色
91
103
  ```
92
104
 
93
- 未命中规则(或 root 请求)→ 内置 `inherit` → `rootChain`。`enabled` **默认关闭(`false`)**——未配置任何链时插件完全 no-op。全时段 `rootChain` 的**最后一项**必须恰好是一个官方 V4 模型(`deepseek-official/deepseek-v4-flash` 或 `deepseek-official/deepseek-v4-pro`)——设置卡与 gateway 在保存时拒绝其它尾巴(遗留非合规尾巴启动时告警并继续按 fallback-only 走原链,但无法原样保存)。完整参考(角色实体、fallback 策略、规则、selector、预设角色、分时槽预设)→ [docs/configuration.md](docs/configuration.md)。
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
- 保存并重启会话后,键入 `/fallbacks`——只读的会话内诊断(来源、解析角色、链、最近的 `fallbacks/switch` 事件、冷却状态)。插件**不再写入** durable `fallbacks/switch` 会话事件(issue #52——apply() 时的注册被证伪无效),因此新切换只出现在 info 日志中,不再出现在 recent-switch 展示面;由旧版插件写入、含 `fallbacks/switch` 事件的会话,可用 `scripts/repair-fallbacks-switch-logs.ts` 修复——脚本把旧事件标记为 ignorable,会话即可重新加载(见下方「能力一览」说明)。在 dsh-tui profile 中,`/fallbacks config` 额外回读组合配置(TUI 无设置页——配置仅文件,见 [docs/configuration.md](docs/configuration.md))。
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)**:`enabled` 默认关闭;未配置任何链时行为与未安装插件完全一致。
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,无需 TUI 设置页或宿主补丁。该行只要插件启用就可见——遗留多模型或空的 all-day 链**不会**隐藏它(只是覆盖不会生效)。
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
 
@@ -12,8 +12,10 @@
12
12
  * `/api/fallbacks/get|set|reset`), while `settings.describe` (writable +
13
13
  * namespace directory) and the provider/model catalog stay on
14
14
  * `connection.api` (see `fallbacks-store.ts`).
15
- * - Registers the `settings.plugin.item` card `key: 'fallbacks'` (the rc.7
16
- * keyed slot — no `id`/`order`) with a business-only inject face
15
+ * - Registers the `settings.plugin.item` card `key: 'fallbacks'` with a
16
+ * matching `id: 'fallbacks'` (the rc.7 keyed slot — no `order`; the id
17
+ * keeps the card mountable on hosts that still declare the slot as a list,
18
+ * which requires `options.id`) with a business-only inject face
17
19
  * ({@link FallbacksSettingsController} + the snapshot-selector hook); the
18
20
  * old Settings-nav section registration is removed — deleting the section
19
21
  * registration deletes the nav entry.
@@ -180,7 +180,8 @@ window.__ModuleLoader__.load({
180
180
  * Parse a chain key or entry selector.
181
181
  *
182
182
  * Accepts `provider/model` and `provider/*`; throws {@link SelectorError}
183
- * on anything else (missing separator, empty parts, extra separators).
183
+ * on anything else (missing separator, empty parts, wildcard inside the
184
+ * model segment).
184
185
  */
185
186
  function parseSelector(input) {
186
187
  if (typeof input !== "string") throw new SelectorError(`invalid selector ${String(input)}: expected "provider/model" or "provider/*"`);
@@ -190,7 +191,7 @@ window.__ModuleLoader__.load({
190
191
  const provider = trimmed.slice(0, slash).trim();
191
192
  const modelPart = trimmed.slice(slash + 1).trim();
192
193
  if (!provider || !modelPart) throw new SelectorError(`invalid selector "${input}": empty provider or model`);
193
- if (modelPart.includes("/")) throw new SelectorError(`invalid selector "${input}": unexpected extra separator`);
194
+ if (modelPart !== "*" && modelPart.includes("*")) throw new SelectorError(`invalid selector "${input}": unexpected wildcard in model`);
194
195
  return {
195
196
  provider,
196
197
  model: modelPart === "*" ? void 0 : modelPart,
@@ -4385,15 +4386,17 @@ window.__ModuleLoader__.load({
4385
4386
  };
4386
4387
  }, "llm-fallbacks: pushed invalidations");
4387
4388
  ctx.slots.inject("settings.plugin.item", function* () {
4388
- yield ctx.slots.register({
4389
+ const cardOptions = {
4389
4390
  name: "settings.plugin.item",
4390
4391
  key: "fallbacks",
4392
+ id: "fallbacks",
4391
4393
  locale: NS,
4392
4394
  inject: () => ({
4393
4395
  controller,
4394
4396
  useSnapshot
4395
4397
  })
4396
- }, FallbacksCard);
4398
+ };
4399
+ yield ctx.slots.register(cardOptions, FallbacksCard);
4397
4400
  });
4398
4401
  ctx.slots.inject("settings.general.item", function* () {
4399
4402
  yield ctx.slots.register({
@@ -11,12 +11,17 @@
11
11
  * the command is silently absent when no command registry is composed (no
12
12
  * top-level inject pollution — advisor T1 fix).
13
13
  *
14
- * The handler is **read-only**: it never mutates fallback state (no cooldown
15
- * reset, no pending-switch writes). zh/en copy lives in this file — the
16
- * client half's `src/client/locales.ts` is a separate client-side dictionary.
17
- * The host carries no per-session locale signal (the `locale` service is
18
- * client-side), so the wiring picks a deterministic default (`zh`, this
19
- * repo's primary language); both dictionaries are unit-tested.
14
+ * The bare diagnostic and the config readback are **read-only**: they never
15
+ * mutate fallback state (no cooldown reset, no pending-switch writes). The
16
+ * `config revert-seed <role-id>` subcommand is the one write action (plan
17
+ * fallbacks-tui-settings Task 2, AC-3): it delegates to the controller's
18
+ * `revertSeed` (wired to the seeds service) and prints the outcome — a
19
+ * web-card action capability the settings seam cannot express. zh/en copy
20
+ * lives in this file — the client half's `src/client/locales.ts` is a
21
+ * separate client-side dictionary. The host carries no per-session locale
22
+ * signal (the `locale` service is client-side), so the wiring picks a
23
+ * deterministic default (`zh`, this repo's primary language); both
24
+ * dictionaries are unit-tested.
20
25
  *
21
26
  * @module dsh-llm-fallbacks/commands
22
27
  */
@@ -24,7 +29,8 @@ import type { CommandDefinition } from '@deepseek-ai/dsh-commands';
24
29
  import { type FallbacksRole } from './config.ts';
25
30
  import type { Origin } from './roles.ts';
26
31
  import type { FallbacksSwitchEventData } from './events.ts';
27
- import type { SlotRowConfig } from './time-slots.ts';
32
+ import type { SeedRevertFailReason } from './seeds.ts';
33
+ import { type SlotRowConfig } from './time-slots.ts';
28
34
  /** How many recent `fallbacks/switch` events `/fallbacks` shows (newest first). */
29
35
  export declare const RECENT_SWITCHES_LIMIT = 5;
30
36
  /** Minimal command registry surface (satisfied by the dsh `CommandService`). */
@@ -90,15 +96,40 @@ export interface FallbacksCommandSnapshot {
90
96
  * entities carry id/persona/chain/fallback (NO per-role `model`;
91
97
  * `provider`/`model` live on `roles.rules`), so the readback line is id +
92
98
  * chain count, never a rules dump.
99
+ *
100
+ * Enriched readback (plan fallbacks-tui-settings Task 2, AC-4): timeSlots /
101
+ * tz / roles.rules join the summary so TUI operators can verify `/settings`
102
+ * edits. One summarized time-slot row: preset rows carry `{ preset,
103
+ * chainCount }` (windows are frozen code constants in {@link PRESETS} and
104
+ * never stored on the row — the render resolves the window text from
105
+ * PRESETS); custom rows carry `{ start, end, chainCount }`. Rules rows carry
106
+ * `{ provider, model, role }` — an omitted provider/model (wildcard match at
107
+ * runtime) is summarized as `''` and rendered as `*`.
93
108
  */
94
109
  export interface FallbacksConfigSummary {
95
110
  readonly enabled: boolean;
96
111
  readonly triggerCodes: readonly string[];
97
112
  readonly rootChain: readonly string[];
113
+ /** Summarized time-slot rows (preset rows carry `preset`, custom rows carry `start`/`end`; both may carry a day mask). */
114
+ readonly timeSlots: readonly {
115
+ preset?: string;
116
+ start?: string;
117
+ end?: string;
118
+ days?: readonly number[];
119
+ chainCount: number;
120
+ }[];
121
+ /** Config-level timezone for slot matching (default `Asia/Shanghai`). */
122
+ readonly tz: string;
98
123
  readonly roles: readonly {
99
124
  id: string;
100
125
  chainCount: number;
101
126
  }[];
127
+ /** Summarized role rules: provider/model patterns → declared role (or the built-in `'inherit'`). */
128
+ readonly rules: readonly {
129
+ provider: string;
130
+ model: string;
131
+ role: string;
132
+ }[];
102
133
  readonly cooldownMs: number;
103
134
  readonly revertPolicy: string;
104
135
  readonly maxSwitchesPerStep: number;
@@ -122,6 +153,23 @@ export interface FallbacksCommandController {
122
153
  * same live config source the runtime reads. Never mutates state.
123
154
  */
124
155
  getConfig(): FallbacksConfigSummary;
156
+ /**
157
+ * Revert one role's persona to its CURRENT declared seed default (AC-3).
158
+ * Surfaces the outcome as VALUES, not copy (qc1 F-003 / qc2 F-006 / qc3
159
+ * F-003): `ok: true` when reverted; `ok: false` with the
160
+ * {@link SeedRevertFailReason} code explaining why not (not a seeded role
161
+ * / role row absent). The command handler localizes the code per its
162
+ * registration locale — the controller never pre-localizes. A
163
+ * settings-write failure propagates loudly as a rejected promise (the
164
+ * same contract as the seeds service — never swallowed into an
165
+ * `ok: false`); the handler maps it to a structured error. Implemented by
166
+ * the wiring against the per-apply seed manager
167
+ * (`seeds.revert(roleId, seedsIo)`), not the typert gateway.
168
+ */
169
+ revertSeed(roleId: string): Promise<{
170
+ ok: boolean;
171
+ reason?: SeedRevertFailReason;
172
+ }>;
125
173
  }
126
174
  /** zh/en dictionaries for the `/fallbacks` output. */
127
175
  export declare const FALLBACKS_COMMAND_LOCALES: {
@@ -129,7 +177,8 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
129
177
  readonly title: "当前会话 fallback 诊断(只读)";
130
178
  readonly description: "查看当前会话的降级链、最近降级切换与冷却状态(只读)";
131
179
  readonly usageConfig: "查看组合后的 fallbacks 配置(设置回读)";
132
- readonly usage: " /fallbacks config 查看组合后的 fallbacks 配置(设置回读)";
180
+ readonly usageRevertSeed: "将角色的 persona 还原为已声明的 Seed 默认";
181
+ readonly usage: " /fallbacks config 查看组合后的 fallbacks 配置(设置回读)\n /fallbacks config revert-seed <role-id> 将角色的 persona 还原为已声明的 Seed 默认";
133
182
  readonly origin: "会话来源";
134
183
  readonly role: "角色";
135
184
  readonly chain: "链";
@@ -153,22 +202,37 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
153
202
  readonly configTriggerCodes: "触发码";
154
203
  readonly configRootChain: "根链";
155
204
  readonly configEmpty: "(空)";
205
+ readonly configTimeSlots: "分时槽";
206
+ readonly configSlotPresetItem: "{preset}(chain: {n}, window {window})";
207
+ readonly configSlotPresetBare: "{preset}(chain: {n})";
208
+ readonly configSlotCustomItem: "custom {start}-{end}(chain: {n})";
209
+ readonly configTz: "时区";
156
210
  readonly configRoles: "角色";
157
211
  readonly configRoleItem: "{id}(chain: {n})";
212
+ readonly configRules: "角色规则";
158
213
  readonly configCooldown: "冷却";
159
214
  readonly configRevert: "回主策略";
160
215
  readonly configMaxSwitches: "单步最大切换";
161
216
  readonly configAlwaysCap: "always 上限";
162
217
  readonly configPresets: "预置";
163
218
  readonly configRoleAutoMatch: "角色自动匹配";
164
- readonly configEdit: "编辑:~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)或 $DSH_HOME/settings.yaml(fallbacks: 分节)";
165
- readonly configEditHint: "TUI 无法修改配置——只能编辑文件";
219
+ readonly configEdit: "编辑:/settings(TUI 设置界面)或 ~/.dsh/profiles/<profile>/cordis.patch.yml(插件行)/ $DSH_HOME/settings.yaml(fallbacks: 分节)";
220
+ readonly configEditHint: "TUI 通过 /settings 修改配置;文件编辑仍然可用";
221
+ readonly revertSeedOk: "角色 {id} 已还原为 Seed 默认";
222
+ readonly revertSeedFail: "角色 {id} 未还原({reason})";
223
+ readonly revertSeedError: "角色 {id} 还原失败(设置写入失败)";
224
+ readonly revertSeedReason: {
225
+ readonly 'not-seeded': "未声明种子";
226
+ readonly 'row-absent': "角色行不存在";
227
+ readonly 'settings-unavailable': "设置通道不可用";
228
+ };
166
229
  };
167
230
  readonly en: {
168
231
  readonly title: "Session fallback diagnostics (read-only)";
169
232
  readonly description: "Inspect fallback chain, recent fallback switches, and cooldown for this session (read-only)";
170
233
  readonly usageConfig: "show the composed fallbacks config (settings readback)";
171
- readonly usage: " /fallbacks config show the composed fallbacks config (settings readback)";
234
+ readonly usageRevertSeed: "revert a role's persona to its declared seed default";
235
+ readonly usage: " /fallbacks config show the composed fallbacks config (settings readback)\n /fallbacks config revert-seed <role-id> revert a role's persona to its declared seed default";
172
236
  readonly origin: "Session origin";
173
237
  readonly role: "Role";
174
238
  readonly chain: "Chain";
@@ -192,29 +256,53 @@ export declare const FALLBACKS_COMMAND_LOCALES: {
192
256
  readonly configTriggerCodes: "Trigger codes";
193
257
  readonly configRootChain: "Root chain";
194
258
  readonly configEmpty: "(empty)";
259
+ readonly configTimeSlots: "Time slots";
260
+ readonly configSlotPresetItem: "{preset} (chain: {n}, window {window})";
261
+ readonly configSlotPresetBare: "{preset} (chain: {n})";
262
+ readonly configSlotCustomItem: "custom {start}-{end} (chain: {n})";
263
+ readonly configTz: "TZ";
195
264
  readonly configRoles: "Roles";
196
265
  readonly configRoleItem: "{id} (chain: {n})";
266
+ readonly configRules: "Rules";
197
267
  readonly configCooldown: "Cooldown";
198
268
  readonly configRevert: "Revert";
199
269
  readonly configMaxSwitches: "Max switches/step";
200
270
  readonly configAlwaysCap: "Always-mode cap";
201
271
  readonly configPresets: "Presets";
202
272
  readonly configRoleAutoMatch: "Auto-match";
203
- readonly configEdit: "Edit: ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) or $DSH_HOME/settings.yaml (fallbacks: section)";
204
- readonly configEditHint: "TUI cannot change config — edit files only";
273
+ readonly configEdit: "Edit: /settings (TUI settings screen) or ~/.dsh/profiles/<profile>/cordis.patch.yml (plugin row) / $DSH_HOME/settings.yaml (fallbacks: section)";
274
+ readonly configEditHint: "TUI edits config via /settings; file editing still works";
275
+ readonly revertSeedOk: "role {id} reverted to its seed default";
276
+ readonly revertSeedFail: "role {id} not reverted ({reason})";
277
+ readonly revertSeedError: "role {id} revert failed (settings write failed)";
278
+ readonly revertSeedReason: {
279
+ readonly 'not-seeded': "not a seeded role";
280
+ readonly 'row-absent': "role row absent";
281
+ readonly 'settings-unavailable': "settings channel unavailable";
282
+ };
205
283
  };
206
284
  };
207
285
  /** A locale id supported by {@link FALLBACKS_COMMAND_LOCALES}. */
208
286
  export type FallbacksCommandLocale = keyof typeof FALLBACKS_COMMAND_LOCALES;
209
- /** The `/fallbacks` subcommands: `'config'` (composed-config readback) or
210
- * `''` (the bare session snapshot). */
211
- export type FallbacksSubcommand = '' | 'config';
287
+ /** The `/fallbacks` subcommands: `'config'` (composed-config readback),
288
+ * `'revert-seed'` (persona revert action, requires a role id), or `''` (the
289
+ * bare session snapshot). */
290
+ export type FallbacksSubcommand = '' | 'config' | 'revert-seed';
291
+ /** One parsed subcommand invocation: the kind plus the optional role id
292
+ * carried by `revert-seed`. */
293
+ export interface FallbacksSubcommandParse {
294
+ readonly kind: FallbacksSubcommand;
295
+ /** The role id argument of `revert-seed` (present iff `kind === 'revert-seed'`). */
296
+ readonly arg?: string;
297
+ }
212
298
  /**
213
299
  * Map an invocation's rawInput to a subcommand: trimmed `'config'` →
214
- * `'config'`; everything else (incl. empty) → `''` (bare snapshot). Lenient
215
- * by design — unknown input keeps today's bare behavior, never errors.
300
+ * `{ kind: 'config' }`; `'config revert-seed <id>'` → `{ kind: 'revert-seed',
301
+ * arg: id }`; everything else (incl. empty, a missing id, or an unknown
302
+ * subcommand under `config`) → `{ kind: '' }` (bare snapshot). Lenient by
303
+ * design — unknown input keeps today's bare behavior, never errors.
216
304
  */
217
- export declare function parseFallbacksSubcommand(rawInput: string): FallbacksSubcommand;
305
+ export declare function parseFallbacksSubcommand(rawInput: string): FallbacksSubcommandParse;
218
306
  /**
219
307
  * The newest `limit` `fallbacks/switch` events from a session's raw event
220
308
  * log, newest first. Unknown event shapes and malformed `fallbacks/switch`
@@ -248,18 +336,23 @@ export declare function resolveChainForDiagnostic(roles: readonly FallbacksRole[
248
336
  readonly inherit: boolean;
249
337
  };
250
338
  /**
251
- * Cap for long list lines in the composed-config readback: beyond this many
252
- * items a line truncates with `…` (the `Roles:` count always stays the FULL
253
- * count). Same sanity scale as {@link RECENT_SWITCHES_LIMIT}.
339
+ * Cap for the composed-config readback's LIST lines — Trigger codes, Root
340
+ * chain, Time slots, Roles, and Rules (qc2 Task-2 Minor): beyond this many
341
+ * items a line truncates with `…` while its leading count always stays the
342
+ * FULL count. Same sanity scale as {@link RECENT_SWITCHES_LIMIT}.
254
343
  */
255
344
  export declare const FALLBACKS_CONFIG_LIST_CAP = 5;
256
345
  /**
257
346
  * Render the `/fallbacks config` surface (plan fallbacks-tui-client T2,
258
- * AC-2): the composed `fallbacks` namespace as the runtime reads it + file-only
259
- * edit hints (TUI has no write surface). The FIRST LINE marks the
260
- * composed-config readback — distinct from the diagnostic title and never
261
- * merged into {@link fallbacksCommandText} (two operator surfaces, product
262
- * lock). Locale defaults to `zh` (the command default); en dictionary tested.
347
+ * AC-2 + fallbacks-tui-settings Task 2 AC-4): the composed `fallbacks`
348
+ * namespace as the runtime reads it — enriched with the time-slot rows
349
+ * (preset rows resolve their frozen window from {@link PRESETS}), the
350
+ * config timezone, and the role rules — plus edit hints pointing at the
351
+ * `/settings` TUI edit surface while keeping the file-edit documentation.
352
+ * The FIRST LINE marks the composed-config readback — distinct from the
353
+ * diagnostic title and never merged into {@link fallbacksCommandText} (two
354
+ * operator surfaces, product lock). Locale defaults to `zh` (the command
355
+ * default); en dictionary tested.
263
356
  */
264
357
  export declare function fallbacksConfigText(summary: FallbacksConfigSummary, locale?: FallbacksCommandLocale): string;
265
358
  /**
package/dist/gateway.d.ts CHANGED
@@ -214,6 +214,23 @@ export declare class FallbacksConfigGateway extends TypertRemoteService {
214
214
  */
215
215
  private seedsIo;
216
216
  }
217
+ /**
218
+ * Reject a patch the `Config` schema cannot express: non-object input, unknown
219
+ * top-level keys (schemastery merges them silently — the settings service
220
+ * would accept them), and schema type violations — plus the Task 3 semantic
221
+ * guards the permissive schema deliberately does not carry: nested `roles`
222
+ * keys, `timeSlots` row shapes (unknown preset ids, duplicate presets, preset
223
+ * rows carrying windows, non-`HH:mm` custom bounds, out-of-range days, empty
224
+ * chains), and the all-day `rootChain` head conformance.
225
+ *
226
+ * Exported (plan fallbacks-tui-settings Task 1): the TUI settings section's
227
+ * JSON-field `parse` mirrors the gateway save rules through this single
228
+ * validator (it internally routes `timeSlots` → {@link validateTimeSlotsPatch}),
229
+ * so an invalid draft blocks the TUI save exactly like a rejected gateway
230
+ * patch. No shared-module extraction — the two-keyword diff is the minimal
231
+ * change.
232
+ */
233
+ export declare function validateConfigPatch(patch: unknown): void;
217
234
  /**
218
235
  * The explicit typert contribution for the `fallbacks` gateway endpoints —
219
236
  * registered via `ctx.typert.register(...)` (see `apply` in `src/index.ts`).