@omdp/dsh-key-fallback 3.2.0 → 3.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,13 +4,44 @@
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.2.0** (`v7` UI generation).
7
+ Current version: **v3.2.2** (`v7` UI generation).
8
+
9
+ ## What v3.2.2 offers
10
+
11
+ - **Declares its DSH version support** (2026-09-24): a new `@deepseek-ai/dsh` peer entry
12
+ (`0.1.7-rc.1`, enumerated per version) so the supported runtime is explicit. DSH's own
13
+ `evaluatePluginCompatibility()` (`dsh-app-boot`) enforces it at install time and on boot — an untested
14
+ newer runtime has the bundle **gracefully skipped** (`skipping profile bundle` on stderr, DSH still
15
+ boots) instead of failing in some undefined way. The gate only exists from DSH `0.1.7`, so runtimes
16
+ `≤0.1.6` merely see an unrecognized peer and are unaffected (this plugin already worked there through
17
+ the file backend). Only tested versions are named — no open ranges (repo rule 3).
18
+
19
+ ## What v3.2.1 offers
20
+
21
+ - **Rescues pools stranded in `settings.yaml.imported`** (2026-09-24). `0.1.7`'s one-shot importer
22
+ **renames `settings.yaml` to `settings.yaml.imported` before it writes anything**, so if a section fails to
23
+ import (e.g. the target entry declares no volatile field) it never retries and the pools sit in
24
+ `.imported` forever. The symptom is exactly: badge shows **"尚未启用"** and the page shows
25
+ **"还没有任何池,点上方「启用新 provider 池」开始。"** even though the pools are on disk.
26
+ Note the file fallback was *already* there in v3.2.0 (`readSettingsFromFile()` falls back to
27
+ `settings.yaml.imported`) — but on `0.1.7` it is **never reached**, because `readSettings()` returns from the
28
+ live-ref branch first whenever `_liveRef` is set, and the ref holds `{}`. What was missing was a **bridge
29
+ between the two backends**, not another read fallback.
30
+ `v3.2.1` performs a **one-time explicit migration** on the first HTTP request (not in `apply()` — see below):
31
+ when the live config has zero providers *and* the legacy file has some, it writes them into the profile entry via
32
+ `settings.replace()`. A marker file (`<DSH_HOME>/.key-fallback-migrated`) makes it genuinely once-only, so
33
+ deliberately deleting every pool does not resurrect them on the next boot.
34
+ - **Why not in `apply()`:** `settings.replace()` goes through `configEditor.edit()` and requires the entry's fiber
35
+ to be **ACTIVE** — `describe()` skips entries whose `fiber.state !== 2`, and during `apply()` the fiber is still
36
+ being created, so the call throws `No configurable plugin entry`. The first UI request (`/pools`) is the
37
+ earliest safe moment.
38
+ - The marker is written **only after** the persist succeeds, so a failed write is retried on the next boot.
8
39
 
9
40
  ## Requirements
10
41
 
11
42
  - DeepSeek Harness with a `web`-profile GUI (`npx @deepseek-ai/dsh web`)
12
43
  - 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.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.
44
+ - Peer ranges strictly enumerate **only compatibility-tested versions** — `@deepseek-ai/dsh` `0.1.7-rc.1` (the plugin's claimed DSH runtime; the gate that enforces it only exists from 0.1.7, so older runtimes just see an unrecognized 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`. 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
45
 
15
46
  ## What v3.2.0 offers
16
47
 
@@ -103,7 +134,12 @@ dsh plugin --profile web add link:D:/WorkSpace/omdp/dsh-key-fallback
103
134
  # restart DSH, then open Settings → API Key 回退
104
135
  ```
105
136
 
106
- > Note: the profile currently installs the package from npm (`^3.x`) into `~/.dsh/profiles/web/node_modules/@omdp/dsh-key-fallback`; copying updated `lib/index.js` + `lib/client.js` there and restarting DSH picks up a newer version. The package declares a `dsh.bundle.patch`, so it activates automatically — no manual `cordis.patch.yml` editing.
137
+ > **To upgrade, move the profile pin — do not copy files into `node_modules`.** A hand-copied
138
+ > `lib/index.js` looks like it works until something re-runs `pnpm install` (DSH does this on boot), at
139
+ > which point pnpm restores the version recorded in `pnpm-lock.yaml` and the "fix" silently disappears.
140
+ > Edit the version in `~/.dsh/profiles/web/package.json`, then run `pnpm install` in that directory and
141
+ > restart DSH. The package declares a `dsh.bundle.patch`, so it activates automatically — no manual
142
+ > `cordis.patch.yml` editing.
107
143
 
108
144
  ## HTTP API (host)
109
145
 
package/README.zh-CN.md CHANGED
@@ -4,13 +4,40 @@
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.2.0**(v7 UI 代)。
7
+ 当前版本:**v3.2.2**(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.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`)无需改动插件。
13
+ - peer 范围**只枚举已实际进行过兼容测试的版本**——`@deepseek-ai/dsh` `0.1.7-rc.1`(本插件声明的 DSH 运行时;**执行该声明的门禁本身只有 0.1.7 才有**,更旧的运行时只是看到一条不认识的 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.2 新增
16
+
17
+ - **声明 DSH 版本支持**(2026-09-24):新增 `@deepseek-ai/dsh` peer 条目(`0.1.7-rc.1`,逐版本枚举),
18
+ 把「支持的运行时」写成显式契约。该声明由 DSH 自己的 `evaluatePluginCompatibility()`(`dsh-app-boot` 的
19
+ 公开导出)在**安装时**与**每次启动时**校验:命中未实测的新运行时,bundle 会被**优雅跳过**
20
+ (stderr 打 `skipping profile bundle`,DSH 照常启动),而不是以某种未定义方式崩掉。这个门禁
21
+ **只有 DSH 0.1.7 起才存在**,所以 `≤0.1.6` 的运行时只是看到一条不认识的 peer,不受影响
22
+ (本插件在那些版本上本来就走 `settings.yaml` 文件后端,一直可用)。只列实测版本,不用开放范围(仓库规范 3)。
23
+
24
+ ## v3.2.1 新增
25
+
26
+ - **救回滞留在 `settings.yaml.imported` 里的池**(2026-09-24)。`0.1.7` 的一次性导入器会在写入任何东西**之前**
27
+ 先把 `settings.yaml` 改名为 `settings.yaml.imported`——所以某段导入失败时(典型:目标条目没声明 volatile 字段)
28
+ 它永远不会重试,池子会永久留在 `.imported` 里。表现恰是:徽标显示 **「尚未启用」**、页面显示
29
+ **「还没有任何池,点上方「启用新 provider 池」开始。」**,尽管池子就在磁盘上。
30
+ 注意**文件回退在 v3.2.0 里本来就写了**(`readSettingsFromFile()` 会在 `settings.yaml` 缺失时退读
31
+ `settings.yaml.imported`)——但在 `0.1.7` 上它**永远轮不到**:只要 `_liveRef` 非空,
32
+ `readSettings()` 第一行就从 live-ref 分支返回,而 ref 里是 `{}`。**真正缺的不是又一个读回退,
33
+ 而是两个后端之间的一座桥。**
34
+ `v3.2.1` 在**首个 HTTP 请求**时做一次性显式搬运(不在 `apply()` 里——原因见下):
35
+ 当 live 配置里 provider 数为 0 *且* 旧文件里有池时,经 `settings.replace()` 写进 profile 条目。
36
+ 标记文件(`<DSH_HOME>/.key-fallback-migrated`)保证「真正只做一次」,因此有意删光所有池后不会在下次启动被复活。
37
+ - **为什么不能放在 `apply()`:** `settings.replace()` 走 `configEditor.edit()`,要求本 entry 的 fiber 已
38
+ **ACTIVE**——`describe()` 会跳过 `fiber.state !== 2` 的条目,而 `apply()` 执行时 fiber 还在创建中,
39
+ 调用必抛 `No configurable plugin entry`。首个 UI 请求(`/pools`)才是最早的安全时机。
40
+ - 标记**只在落盘成功之后**才写,因此写失败会在下次启动重试。
14
41
 
15
42
  ## v3.2.0 新增
16
43
 
package/lib/index.js CHANGED
@@ -42,6 +42,8 @@ const SETTINGS_FILE = join(DSH_HOME, 'settings.yaml')
42
42
  const SETTINGS_FILE_IMPORTED = SETTINGS_FILE + '.imported'
43
43
  const CRED_FILE = join(DSH_HOME, '.credentials.yaml')
44
44
  const BACKUP_DIR = join(DSH_HOME, 'backups')
45
+ // 一次性 legacy→profile 迁移的标记(见 apply() 里的迁移块)。
46
+ const MIGRATED_MARKER = join(DSH_HOME, '.key-fallback-migrated')
45
47
  if (!existsSync(BACKUP_DIR)) try { mkdirSync(BACKUP_DIR, { recursive: true }) } catch (e) {}
46
48
  let _backupCounter = 0
47
49
  function tsName(prefix) {
@@ -254,6 +256,39 @@ export function apply(ctx, config) {
254
256
  diag('settings', 'backend = settings.yaml file (rc.x)')
255
257
  }
256
258
  } catch (e) { diag('settings', 'probe failed: ' + ((e && e.message) || e)) }
259
+
260
+ // ── 一次性迁移:老 settings.yaml(.imported) 里的池 → settings 托管的 entry config ──
261
+ // 0.1.7 的 importLegacyDocument() 在首次写入前就把 settings.yaml 改名成
262
+ // settings.yaml.imported,而本机的池恰好躺在 .imported 里 —— 迁移入口已永久失活,
263
+ // 池不会自己回到 UI(表现为徽标「尚未启用」+「还没有任何池」)。
264
+ //
265
+ // 必须在首个 HTTP 请求时做,不能在这里(apply 期间)做:settings.replace() 走
266
+ // configEditor.edit(),要求本 entry 的 fiber 已 ACTIVE(describe() 会跳过
267
+ // state !== 2 的 entry),apply() 执行时 fiber 尚在创建中,必然抛
268
+ // "No configurable plugin entry"。UI 打开设置页就会请求 /pools,时机正好。
269
+ // 标记文件保证「真正只做一次」:否则用户有意清空所有池后,下次启动又会灌回来。
270
+ let _migrationAttempted = false
271
+ async function ensureLegacyMigration() {
272
+ if (_migrationAttempted) return
273
+ _migrationAttempted = true
274
+ try {
275
+ if (!_liveRef || existsSync(MIGRATED_MARKER)) return
276
+ if (Object.keys((_cache && _cache.providers) || {}).length > 0) return
277
+ const migrated = (readSettingsFromFile() || {}).providers || {}
278
+ const names = Object.keys(migrated)
279
+ if (names.length === 0) {
280
+ // 无可搬运内容:也落标记(空文件算「已处理过」),避免每次启动重复探测。
281
+ try { writeFileSync(MIGRATED_MARKER, new Date().toISOString() + '\n', 'utf8') } catch (e) {}
282
+ return
283
+ }
284
+ // 直接 await 真正的 replace:只有落盘成功才写标记,否则下次启动还能重试。
285
+ if (!_settingsSvc || typeof _settingsSvc.replace !== 'function') return
286
+ _cache = { providers: cloneJson(migrated) }
287
+ await _settingsSvc.replace(ENTRY_ID, { providers: cloneJson(migrated) })
288
+ try { writeFileSync(MIGRATED_MARKER, new Date().toISOString() + '\n', 'utf8') } catch (e) {}
289
+ diag('migrate', 'imported ' + names.length + ' provider(s) from settings.yaml(.imported): ' + names.join(','))
290
+ } catch (e) { diag('migrate', 'failed: ' + ((e && e.message) || e)) }
291
+ }
257
292
  // ── 包装 credentials.resolve:ref 命中池 env 时返回池当前 key(llm-pi-ai 唯一读取入口)──
258
293
  const poolsByEnv = new Map()
259
294
  const credSvc = ctx.credentials
@@ -541,6 +576,11 @@ export function apply(ctx, config) {
541
576
  if (webServer) {
542
577
  const route = async (req, res) => {
543
578
  try {
579
+ // 首次请求时补做 legacy 池迁移(此时 fiber 已 ACTIVE,settings.replace 可用)。
580
+ // 放在所有分支之前,保证无论 UI 先请求哪个端点,池都已被搬进 live 配置。
581
+ // 首次请求时补做 legacy 池迁移(此时 fiber 已 ACTIVE,settings.replace 可用)。
582
+ // 放在所有分支之前,保证无论 UI 先请求哪个端点,池都已被搬进 live 配置。
583
+ await ensureLegacyMigration()
544
584
  const url = new URL(req.url, 'http://127.0.0.1')
545
585
  const path = url.pathname.replace(new RegExp('^' + API_BASE + '/?'), '').replace(/\/$/, '')
546
586
  const readBody = () => new Promise((resolve, reject) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@omdp/dsh-key-fallback",
3
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",
4
+ "version": "3.2.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -39,6 +39,7 @@
39
39
  },
40
40
  "peerDependencies": {
41
41
  "@deepseek-ai/cordis": "4.0.1 || 4.0.2 || 4.0.4",
42
+ "@deepseek-ai/dsh": "0.1.7-rc.1",
42
43
  "@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
44
  "@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
45
  "@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",