@omdp/dsh-key-fallback 3.1.7 → 3.2.0

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
@@ -4,13 +4,54 @@
4
4
 
5
5
  **Multi-key API key pool with automatic rotation for [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness)** — sits between the LLM adapter and the credential store. Before each request the plugin picks a key from the per-provider pool and pre-writes it into the provider's credential reference; when a configured trigger error occurs it marks the failed key cooling (fixed `cooldownMs`, no exponential backoff) and advances to the next key. **Re-sending is left entirely to DSH's own `dsh-llm-retry`** — this plugin never re-sends on its own; it only switches the key and lets the retry policy decide.
6
6
 
7
- Current version: **v3.1.7** (`v6` UI generation).
7
+ Current version: **v3.2.0** (`v7` UI generation).
8
8
 
9
9
  ## Requirements
10
10
 
11
11
  - DeepSeek Harness with a `web`-profile GUI (`npx @deepseek-ai/dsh web`)
12
12
  - Node.js `^22.19` or `>=24`
13
- - Peer ranges strictly enumerate **only compatibility-tested versions** — `@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1`, `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1`, `@deepseek-ai/cordis` `4.0.1 || 4.0.2`, `@deepseek-ai/schemastery` `3.18.1 || 3.18.2`. No open-ended ranges (`<0.2.0`, caret): untested versions are deliberately excluded until verified. The plugin only uses the credential-reference half (`resolve`/`describe`/`set`/`unset`/`credentialRef` — stable since `0.1.0-rc.6`) and the `agent/request` + `agent/request-error` waterfall (payload unchanged across the enumerated versions); `isCredentialRefName` (added `rc.8`) is implemented locally for compatibility. The `0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` companion packages are byte-identical (2026-09-03 verified), so DSH `0.1.2-rc.1` (`next`) needs no plugin change.
13
+ - Peer ranges strictly enumerate **only compatibility-tested versions** — `@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1`, `@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1`, `@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`, `@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`. No open-ended ranges (`<0.2.0`, caret): untested versions are deliberately excluded until verified. The plugin only uses the credential-reference half (`resolve`/`describe`/`set`/`unset`/`credentialRef` — stable since `0.1.0-rc.6`) and the `agent/request` + `agent/request-error` waterfall (payload unchanged across the enumerated versions); `isCredentialRefName` (added `rc.8`) is implemented locally for compatibility. The `0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` companion packages are byte-identical (2026-09-03 verified), so DSH `0.1.2-rc.1` (`next`) needs no plugin change.
14
+
15
+ ## What v3.2.0 offers
16
+
17
+ - **DSH `0.1.7-rc.1` support declared, with `0.1.5-rc.3` kept** (2026-09-24). This release is the first to need an
18
+ actual code change, because DSH `0.1.7` **relocates plugin configuration**: see the next section.
19
+
20
+ ### Two config backends, one build
21
+
22
+ Since DSH `0.1.7`, plugin configuration stopped living in `<DSH_HOME>/settings.yaml`. On first boot, `0.1.7`
23
+ **renames that file to `settings.yaml.imported`** and copies each section into the profile patch
24
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`) as the `config:` of the matching loader entry; from then on the
25
+ `settings` service owns it, and plugins read it through an injected `config` parameter instead of touching the
26
+ YAML file. `ctx.settings.get()` / `register()` / `installSection()` — the rc.x API — **no longer exist** on `0.1.7`.
27
+
28
+ This plugin therefore auto-detects the backend at startup and keeps both paths working:
29
+
30
+ | | DSH `0.1.5-rc.3` (and older rc.x) | DSH `0.1.7-rc.1` (and newer) |
31
+ |---|---|---|
32
+ | Where pools live | `<DSH_HOME>/settings.yaml`, section `key-fallback:` | profile patch, the `key-fallback` entry's `config` |
33
+ | How the plugin reads/writes | reads/writes that YAML file directly (with timestamped backups) | `config.providers` is a live volatile ref; writes go through `settings.replace()` |
34
+ | Detection | no volatile-schema support ⇒ file backend | `Config` declares a volatile field ⇒ backend comes from `config` |
35
+
36
+ Two implementation notes that matter if you port this pattern elsewhere:
37
+
38
+ 1. **`Config` must exist, or the migration silently drops your config.** `0.1.7`'s import path calls
39
+ `settings.update(ns, values)`, which throws unless the entry declares a **volatile** field; on failure it logs
40
+ `settings: section ... was not imported` and the section survives only inside `settings.yaml.imported`. So
41
+ `v3.2.0` exports `Config = z.object({ providers: <dict>.volatile() })`.
42
+ 2. **The volatile API differs between the two schemastery builds.** `.volatile()` is available in schemastery
43
+ `3.18.4` (`0.1.7`) but **not** in `3.18.2` (`rc.3`) — calling it there returns `undefined` and registering a
44
+ schema without it would change rc.3 behaviour. The export is therefore guarded:
45
+ `typeof field.volatile === 'function' ? z.object({ providers: field.volatile() }) : undefined` — rc.3 gets
46
+ **no** `Config` (byte-identical behaviour to v3.1.x) while `0.1.7` gets the volatile one.
47
+
48
+ The values from `config.providers` are deep-frozen by DSH, so `readSettings()` hands callers a mutable deep copy;
49
+ writes are applied optimistically to an in-memory copy and flushed via `settings.replace()` (an unresolved-write
50
+ counter prevents the UI from briefly re-reading the pre-write tree).
51
+
52
+ Both backends were exercised end-to-end on real installs: read (both providers appear in `/dsh-key-fallback/pools`
53
+ with live env-key values), create/update/delete (POST/DELETE round-trip, persisted), and the rc.3 regression
54
+ (`settings.yaml` untouched and *not* renamed, `cordis.patch.yml` still `[]`, backups still written).
14
55
 
15
56
  ## What v3.1.7 offers
16
57
 
@@ -81,7 +122,7 @@ dsh plugin --profile web add link:D:/WorkSpace/omdp/dsh-key-fallback
81
122
  ## Known limitations
82
123
 
83
124
  - Cooldown is a fixed per-pool `cooldownMs` (no exponential backoff); a cooled key becomes live again when the timer expires.
84
- - The plugin manages rotation/cooldown state in-memory plus persisted pool config (`keyFallback.providers` in `settings.yaml`); a DSH restart re-reads config and recomputes live status.
125
+ - The plugin manages rotation/cooldown state in-memory plus persisted pool config; a DSH restart re-reads config and recomputes live status. On DSH `0.1.5-rc.3` and older that config is the `key-fallback` section of `<DSH_HOME>/settings.yaml`; on `0.1.7`+ it is the `key-fallback` entry's `config` inside `~/.dsh/profiles/<profile>/cordis.patch.yml`.
85
126
  - The env key cannot be deleted through the UI (it is the pool's identity).
86
127
 
87
128
  ## License
package/README.zh-CN.md CHANGED
@@ -1,16 +1,56 @@
1
1
  # @omdp/dsh-key-fallback
2
2
 
3
- English | [简体中文](README.md)
3
+ [English](README.md) | 简体中文
4
4
 
5
5
  **为 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 提供多 key 池 + 自动轮换**——插件位于 LLM 适配器与凭证存储之间:每次请求前从按 provider 分组的 key 池里选一把,预写入该 provider 的凭证引用;遇到配置的触发错误时,把失败 key 标记为冷却(固定 `cooldownMs`,无指数退避)并前进到下一把。**重发完全交给 DSH 自带的 `dsh-llm-retry`**——本插件从不自行重发,只负责换 key,重试策略由 retry policy 决定。
6
6
 
7
- 当前版本:**v3.1.7**(v6 UI 代)。
7
+ 当前版本:**v3.2.0**(v7 UI 代)。
8
8
 
9
9
  ## 环境要求
10
10
 
11
11
  - DeepSeek Harness 带 `web` profile GUI(`npx @deepseek-ai/dsh web`)
12
12
  - Node.js `^22.19` 或 `>=24`
13
- - peer 范围**只枚举已实际进行过兼容测试的版本**——`@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1`、`@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1`、`@deepseek-ai/cordis` `4.0.1 || 4.0.2`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2`。不使用 `<0.2.0`、caret 之类的开放范围:未测试版本在核查通过前刻意排除。插件只使用 credential-reference 半边(`resolve`/`describe`/`set`/`unset`/`credentialRef`,自 `0.1.0-rc.6` 起稳定)与 `agent/request` + `agent/request-error` waterfall(载荷跨上述枚举版本未变);`isCredentialRefName`(rc.8 新增)本地实现兜底。`0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` 配套包逐字节一致(2026-09-03 复核),故 DSH `0.1.2-rc.1`(`next`)无需改动插件。
13
+ - peer 范围**只枚举已实际进行过兼容测试的版本**——`@deepseek-ai/dsh-credentials` `0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1`、`@deepseek-ai/dsh-llm` / `@deepseek-ai/dsh-settings` `0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1`、`@deepseek-ai/cordis` `4.0.1 || 4.0.2 || 4.0.4`、`@deepseek-ai/schemastery` `3.18.1 || 3.18.2 || 3.18.4`。不使用 `<0.2.0`、caret 之类的开放范围:未测试版本在核查通过前刻意排除。插件只使用 credential-reference 半边(`resolve`/`describe`/`set`/`unset`/`credentialRef`,自 `0.1.0-rc.6` 起稳定)与 `agent/request` + `agent/request-error` waterfall(载荷跨上述枚举版本未变);`isCredentialRefName`(rc.8 新增)本地实现兜底。`0.1.2-alpha.2 → alpha.5 → 0.1.2-rc.1` 配套包逐字节一致(2026-09-03 复核),故 DSH `0.1.2-rc.1`(`next`)无需改动插件。
14
+
15
+ ## v3.2.0 新增
16
+
17
+ - **声明支持 DSH `0.1.7-rc.1`,同时保留 `0.1.5-rc.3`**(2026-09-24)。这是本插件首个需要改动代码的版本,原因是 DSH `0.1.7`
18
+ **改变了插件配置的存放位置**——详见下节。
19
+
20
+ ### 两种配置后端,同一份构建
21
+
22
+ 自 DSH `0.1.7` 起,插件配置不再位于 `<DSH_HOME>/settings.yaml`。首次启动时 `0.1.7` 会把该文件
23
+ **改名成 `settings.yaml.imported`**,并把每个段的内容写入 profile 补丁
24
+ (`~/.dsh/profiles/<profile>/cordis.patch.yml`)中对应 loader 条目的 `config:`;此后由 `settings` 服务托管,
25
+ 插件通过注入的 `config` 参数读取,而不再自行读写该 YAML。rc.x 时代的 `ctx.settings.get()` /
26
+ `register()` / `installSection()` 在 `0.1.7` 上**已不存在**。
27
+
28
+ 因此本插件在启动时自动判别后端,两条路径同时可用:
29
+
30
+ | | DSH `0.1.5-rc.3`(及更早 rc.x) | DSH `0.1.7-rc.1`(及更新) |
31
+ |---|---|---|
32
+ | 池配置位置 | `<DSH_HOME>/settings.yaml` 的 `key-fallback:` 段 | profile 补丁中 `key-fallback` 条目的 `config` |
33
+ | 插件如何读写 | 直接读写该 YAML 文件(附时间戳备份) | `config.providers` 是活的 volatile ref;写入走 `settings.replace()` |
34
+ | 判别方式 | 无 volatile schema 支持 ⇒ 文件后端 | `Config` 声明了 volatile 字段 ⇒ 后端来自 `config` |
35
+
36
+ 移植该模式时有两个关键点:
37
+
38
+ 1. **必须声明 `Config`,否则迁移会静默丢弃配置。** `0.1.7` 的导入路径调用
39
+ `settings.update(ns, values)`,若该条目没有声明 **volatile** 字段就会抛错,仅打印
40
+ `settings: section ... was not imported`,配置只残留在 `settings.yaml.imported` 里。因此
41
+ `v3.2.0` 导出 `Config = z.object({ providers: <dict>.volatile() })`。
42
+ 2. **volatile API 在两种 schemastery 构建上不同。** `.volatile()` 在 schemastery `3.18.4`(`0.1.7`)上可用,
43
+ 但在 `3.18.2`(`rc.3`)上**不存在**——在那里调用会得到 `undefined`,而若据此注册一个无 volatile 的
44
+ schema 就会改变 rc.3 的行为。故导出做了守卫:
45
+ `typeof field.volatile === 'function' ? z.object({ providers: field.volatile() }) : undefined`——
46
+ rc.3 拿到 **没有** `Config`(行为与 v3.1.x 逐字节一致),`0.1.7` 拿到含 volatile 的那一份。
47
+
48
+ `config.providers` 的值被 DSH 深度冻结,故 `readSettings()` 向调用方返回可变深拷贝;写入先乐观地更新内存副本
49
+ 再经 `settings.replace()` 落盘(未决写入计数避免 UI 短暂读回写入前的旧树)。
50
+
51
+ 两条后端均已在真实安装上端到端实测:读取(两个 provider 都出现在 `/dsh-key-fallback/pools`,含 env key 实时值)、
52
+ 增删改(POST/DELETE 往返并落盘)、以及 rc.3 回归(`settings.yaml` 未被改名也未改内容、`cordis.patch.yml` 仍为
53
+ `[]`、备份照旧生成)。
14
54
 
15
55
  ## v3.1.7 新增
16
56
 
@@ -81,7 +121,7 @@ dsh plugin --profile web add link:D:/WorkSpace/omdp/dsh-key-fallback
81
121
  ## 已知限制
82
122
 
83
123
  - 冷却为每池固定 `cooldownMs`(无指数退避);冷却到期 key 自动恢复 live。
84
- - 轮换/冷却状态为内存态 + 持久化池配置(`settings.yaml` 的 `keyFallback.providers`);DSH 重启会重读配置并重算 live 状态。
124
+ - 轮换/冷却状态为内存态 + 持久化池配置;DSH 重启会重读配置并重算 live 状态。在 DSH `0.1.5-rc.3` 及更早版本上,该配置是 `<DSH_HOME>/settings.yaml` 的 `key-fallback` 段;在 `0.1.7`+ 上则是 `~/.dsh/profiles/<profile>/cordis.patch.yml` 中 `key-fallback` 条目的 `config`。
85
125
  - env key 不能通过 UI 删除(它是池的身份标识)。
86
126
 
87
127
  ## License
package/lib/index.js CHANGED
@@ -32,9 +32,16 @@ function deriveEnvName(name) {
32
32
  export const name = 'key-fallback'
33
33
  export const inject = ['llm', 'settings', 'webServer', 'credentials']
34
34
  const API_BASE = '/dsh-key-fallback'
35
- const SETTINGS_FILE = join(homedir(), '.dsh', 'settings.yaml')
36
- const CRED_FILE = join(homedir(), '.dsh', '.credentials.yaml')
37
- const BACKUP_DIR = join(homedir(), '.dsh', 'backups')
35
+ // 遵循 DSH_HOME(未设置时回落 ~/.dsh):与 dsh-connector / dsh-archived-sessions 的
36
+ // 路径推导保持一致,也让隔离冒烟环境可以直接切换整个 home。
37
+ const DSH_HOME = (() => {
38
+ const h = process.env.DSH_HOME
39
+ return (typeof h === 'string' && h) ? h : join(homedir(), '.dsh')
40
+ })()
41
+ const SETTINGS_FILE = join(DSH_HOME, 'settings.yaml')
42
+ const SETTINGS_FILE_IMPORTED = SETTINGS_FILE + '.imported'
43
+ const CRED_FILE = join(DSH_HOME, '.credentials.yaml')
44
+ const BACKUP_DIR = join(DSH_HOME, 'backups')
38
45
  if (!existsSync(BACKUP_DIR)) try { mkdirSync(BACKUP_DIR, { recursive: true }) } catch (e) {}
39
46
  let _backupCounter = 0
40
47
  function tsName(prefix) {
@@ -66,21 +73,80 @@ const profileSchema = z.object({
66
73
  refsCompacted: z.boolean().default(false),
67
74
  })
68
75
 
69
- function readSettings() {
70
- if (!existsSync(SETTINGS_FILE)) return { providers: {} }
76
+ // ── 配置后端(双版本兼容)─────────────────────────────────────────────
77
+ // 0.1.5-rc.3:配置在 <home>/settings.yaml 的 `key-fallback:` 段,插件自己读写该文件。
78
+ // 0.1.7+ :DSH 启动时把 settings.yaml 改名为 settings.yaml.imported,段内容成为该 entry 的
79
+ // config override(写进 profiles/<p>/cordis.patch.yml),由 ctx.settings 服务托管;
80
+ // 插件通过「volatile Config + describe/replace」读写,不再碰 settings.yaml。
81
+ //
82
+ // 关键前提:0.1.7 的迁移走 settings.update(ns, values) → write(),要求该 entry 存在且其
83
+ // Config 含 volatile 字段,否则抛错并打印「section ... was not imported」——配置静默丢失。
84
+ // 因此必须声明 Config(下面),且只在本版本 schemastery 支持 .volatile() 时声明,
85
+ // 使 rc.3 行为与改造前逐字节一致。
86
+ const _providersField = z.dict(z.any()).default({})
87
+ export const Config = (typeof _providersField.volatile === 'function')
88
+ ? z.object({ providers: _providersField.volatile() })
89
+ : undefined
90
+
91
+ const ENTRY_ID = 'key-fallback'
92
+ const diagLog = []
93
+ const diag = (kind, msg) => {
71
94
  try {
72
- const raw = readFileSync(SETTINGS_FILE, 'utf8')
73
- const doc = loadYaml(raw) || {}
74
- const block = (doc && (doc['key-fallback'] || doc.keyFallback)) || {}
75
- return block && block.providers ? block : { providers: {} }
76
- } catch (e) { return { providers: {} } }
95
+ diagLog.push({ kind, msg: String(msg).slice(0, 300), at: Date.now() })
96
+ if (diagLog.length > 200) diagLog.splice(0, diagLog.length - 200)
97
+ } catch (e) {}
98
+ }
99
+ let _settingsSvc = null // ctx.settings(0.1.7+)
100
+ let _liveRef = null // 0.1.7+: config.providers 的 volatile Ref(原地热更新)
101
+ let _cache = null // 0.1.7+: 内存权威副本(乐观写,避免异步落盘窗口内读到旧值)
102
+ let _writing = 0 // 未确认写入计数
103
+
104
+ function readSettingsFromFile() {
105
+ // settings.yaml 优先;0.1.7 迁移后已改名,回退读 .imported(只读引导,避免配置真空)
106
+ for (const file of [SETTINGS_FILE, SETTINGS_FILE_IMPORTED]) {
107
+ if (!existsSync(file)) continue
108
+ try {
109
+ const doc = loadYaml(readFileSync(file, 'utf8')) || {}
110
+ const block = (doc && (doc[ENTRY_ID] || doc.keyFallback)) || {}
111
+ if (block && block.providers) return block
112
+ } catch (e) {}
113
+ }
114
+ return { providers: {} }
115
+ }
116
+
117
+ function cloneJson(v) {
118
+ try { return v === undefined ? {} : JSON.parse(JSON.stringify(v)) } catch (e) { return v }
119
+ }
120
+
121
+ function readSettings() {
122
+ if (_liveRef) {
123
+ // _liveRef 就是 config.providers 这个 Ref:.get() 直接返回 providers 字典。
124
+ // 它由 DSH deepFreeze,调用方却要就地改(新增/删除 provider、compactRefs 改 keyRefs),
125
+ // 所以这里始终返回「可变深拷贝」。未决写入期间以内存副本为源(replace 是异步的,
126
+ // 否则会读到落盘前的旧树,UI 立刻刷新会闪回旧值)。
127
+ let src = _cache
128
+ if (_writing === 0) { try { src = { providers: _liveRef.get() || {} } ; _cache = src } catch (e) {} }
129
+ if (!src) { try { src = { providers: _liveRef.get() || {} }; _cache = src } catch (e) { src = { providers: {} } } }
130
+ return { providers: cloneJson(src.providers) || {} }
131
+ }
132
+ return readSettingsFromFile()
77
133
  }
78
134
 
79
135
  function writeSettings(data) {
136
+ if (_liveRef) {
137
+ _cache = { providers: cloneJson((data && data.providers) || {}) }
138
+ if (!_settingsSvc || typeof _settingsSvc.replace !== 'function') return
139
+ _writing++
140
+ Promise.resolve()
141
+ .then(() => _settingsSvc.replace(ENTRY_ID, { providers: cloneJson(_cache.providers) }))
142
+ .catch((e) => diag('settings-write', 'replace failed: ' + ((e && e.message) || e)))
143
+ .finally(() => { _writing-- })
144
+ return
145
+ }
80
146
  backupFile(SETTINGS_FILE, 'settings')
81
147
  let root = {}
82
148
  try { if (existsSync(SETTINGS_FILE)) root = loadYaml(readFileSync(SETTINGS_FILE, 'utf8')) || {} } catch (e) {}
83
- root['key-fallback'] = data
149
+ root[ENTRY_ID] = data
84
150
  writeFileSync(SETTINGS_FILE, dumpYaml(root, { lineWidth: 120 }), 'utf8')
85
151
  }
86
152
 
@@ -168,11 +234,26 @@ function displayNameOf(entry) {
168
234
  return ref
169
235
  }
170
236
 
171
- export function apply(ctx) {
172
- try { ctx.logger.info('key-fallback: v6 host apply() starting (config = settings.yaml)') } catch (e) {}
173
- const diagLog = []
174
- const diag = (kind, msg) => { try { diagLog.push({ kind, msg: String(msg).slice(0, 300), at: Date.now() }); if (diagLog.length > 200) diagLog.splice(0, diagLog.length - 200) } catch (e) {} }
237
+ export function apply(ctx, config) {
238
+ try { ctx.logger.info('key-fallback: v7 host apply() starting') } catch (e) {}
175
239
  diag('apply', 'host loaded')
240
+
241
+ // ── 0.1.7+:把配置接到 settings 服务托管的那份树 ──
242
+ // config.providers 平时是 volatile Ref(.get() 随编辑热更新);无 config 或非 Ref 时
243
+ // 保持文件后端。两者都在这里判定一次,之后 readSettings/writeSettings 自动分流。
244
+ try {
245
+ const svc = ctx.get('settings')
246
+ const prov = config && config.providers
247
+ if (svc && typeof svc.replace === 'function' && prov && typeof prov.get === 'function') {
248
+ _settingsSvc = svc
249
+ _liveRef = prov // = config.providers,Ref.get() 直接给 providers 字典
250
+ _cache = { providers: prov.get() || {} }
251
+ diag('settings', 'backend = profile entry (0.1.7+), providers=' + Object.keys(_cache.providers).join(','))
252
+ // 无需额外订阅:readSettings() 每次(无未决写入时)都从 Ref 重取,外部编辑自动生效
253
+ } else {
254
+ diag('settings', 'backend = settings.yaml file (rc.x)')
255
+ }
256
+ } catch (e) { diag('settings', 'probe failed: ' + ((e && e.message) || e)) }
176
257
  // ── 包装 credentials.resolve:ref 命中池 env 时返回池当前 key(llm-pi-ai 唯一读取入口)──
177
258
  const poolsByEnv = new Map()
178
259
  const credSvc = ctx.credentials
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@omdp/dsh-key-fallback",
3
- "description": "API Key 回退轮换插件:多 key 池 + 自动轮换(触发码可配置,含 LlmError 标准码全集)+ 环境密钥编辑 + 明文揭示 + 短 ref 迁移。Settings → API Key 回退 UI. v6.1.",
4
- "version": "3.1.7",
3
+ "description": "API Key 回退轮换插件:多 key 池 + 自动轮换(触发码可配置,含 LlmError 标准码全集)+ 环境密钥编辑 + 明文揭示 + 短 ref 迁移。同时兼容 DSH 0.1.5-rc.3(settings.yaml 后端)与 0.1.7+(profile entry config 后端)。Settings → API Key 回退 UI. v7.",
4
+ "version": "3.2.0",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -38,10 +38,10 @@
38
38
  }
39
39
  },
40
40
  "peerDependencies": {
41
- "@deepseek-ai/cordis": "4.0.1 || 4.0.2",
42
- "@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
43
- "@deepseek-ai/dsh-llm": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
44
- "@deepseek-ai/dsh-settings": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
45
- "@deepseek-ai/schemastery": "3.18.1 || 3.18.2"
41
+ "@deepseek-ai/cordis": "4.0.1 || 4.0.2 || 4.0.4",
42
+ "@deepseek-ai/dsh-credentials": "0.1.0-rc.6 || 0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1",
43
+ "@deepseek-ai/dsh-llm": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1",
44
+ "@deepseek-ai/dsh-settings": "0.1.1-rc.2 || 0.1.2-alpha.1 || 0.1.2-alpha.2 || 0.1.2-alpha.3 || 0.1.2-alpha.4 || 0.1.2-alpha.5 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.5-rc.3 || 0.1.6-alpha.1 || 0.1.7-rc.1",
45
+ "@deepseek-ai/schemastery": "3.18.1 || 3.18.2 || 3.18.4"
46
46
  }
47
47
  }