@goodandready/dsh-key-rotation 0.7.31 → 0.7.33

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
@@ -28,6 +28,15 @@
28
28
 
29
29
  ## ⚡ Overview & The Problem
30
30
 
31
+ ### 🛠️ What's New in v0.7.33 (Stability & Bugfix Release)
32
+ - **🔍 Resolved Key Probing BaseURL**: Fixed `resolveBaseUrl` to map key credential refs to owning provider pools, restoring live `probeModels` testing.
33
+ - **🛡️ Guarded Cascade Recursion**: Prevented call stack overflow in cross-provider failover when circular cascade chains occur.
34
+ - **🕒 Accurate Midnight PST Resets**: Corrected UTC-8 timezone calculation offset sign for calendar quota reset windows.
35
+ - **🧹 Lifecycle Timer Cleanup**: Wrapped `canaryTimer` and `selfHealTimer` in Cordis effect scopes, eliminating background orphaned intervals on hot reload.
36
+ - **⚡ Stale Lock Recovery in Load Balancer**: Added expired lock detection to `pickLeastLoaded` for uninterrupted least-connections routing.
37
+ - **🌐 Full Chinese Localization**: Added complete `zh` locale dictionary to the React settings dashboard for comprehensive 3-language parity.
38
+
39
+
31
40
  ### 🚀 What's New in v0.7.31
32
41
  - **⚡ O(1) TokenBucket Accumulator**: Upgraded rate limiting math to O(1) time and zero-allocation memory with adaptive header synchronization.
33
42
  - **🛡️ Soft vs Hard Backoff**: Differentiates transient infrastructure drops (502/503/timeouts: 10s flat cooldown) from hard quota errors (progressive doubling).
package/README.ru.md CHANGED
@@ -28,6 +28,15 @@
28
28
 
29
29
  ## ⚡ Обзор и решаемая проблема
30
30
 
31
+ ### 🛠️ Что нового в версии 0.7.33 (Хотфикс и повышение стабильности)
32
+ - **🔍 Исправление проверки ключей**: Функция `resolveBaseUrl` теперь сопоставляет ref ключа с его пулом, восстанавливая работу живого тестирования моделей.
33
+ - **🛡️ Защита от зацикливания каскада**: Устранена возможность переполнения стека при взаимных кольцевых цепочках failover.
34
+ - **🕒 Точный сброс квот по PST**: Исправлен знак часового смещения UTC-8 для корректного сброса лимитов в полночь по тихоокеанскому времени.
35
+ - **🧹 Очистка таймеров в жизненном цикле**: Таймеры `canaryTimer` и `selfHealTimer` переведены на эффекты Cordis, исключая утечки при горячей перезагрузке.
36
+ - **⚡ Сброс зависших блокировок**: Алгоритм `pickLeastLoaded` теперь учитывает протухшие блокировки при выборе наименее нагруженного ключа.
37
+ - **🌐 Полная китайская локализация**: Добавлен словарь `zh` в веб-интерфейс настроек для соблюдения стандарта трёх языков (EN/RU/ZH).
38
+
39
+
31
40
  ### 🚀 Что нового в версии 0.7.31
32
41
  - **⚡ $O(1)$ Накопитель TokenBucket**: Оптимизация математики лимитов до $O(1)$ по времени и без аллокаций памяти с адаптивной синхронизацией по заголовкам.
33
42
  - **🛡️ Разделение Soft / Hard сбоев**: Кратковременные сетевые сбои (502/503/таймауты) получают короткий 10-секундный кулдаун без штрафного удвоения.
package/README.zh.md CHANGED
@@ -28,6 +28,15 @@
28
28
 
29
29
  ## ⚡ 概述与核心痛点
30
30
 
31
+ ### 🛠️ v0.7.33 版本新特性 (稳定性与问题修复)
32
+ - **🔍 修复密钥探测 BaseURL 解析**:`resolveBaseUrl` 现已支持从密钥 ref 反查归属提供商池,恢复在线模型连通性探测。
33
+ - **🛡️ 防御级联无限递归**:在跨提供商故障转移中增加递归深度防护,彻底杜绝循环级联导致的堆栈溢出。
34
+ - **🕒 纠正 PST 太平洋时间配额重置**:修复 UTC-8 时区偏移符号,确保日配额在太平洋时间午夜准时重置。
35
+ - **🧹 定时器生命周期自动回收**:将金丝雀探测与自愈定时器纳入 Cordis 效应生命周期,消除热重载遗留孤儿定时器。
36
+ - **⚡ 负载均衡超时锁自动释放**:`pickLeastLoaded` 算法现已检测过期连接锁,确保最小连接调度不发生偏移。
37
+ - **🌐 完整中文界面本地化**:为 React 设置面板补充全部 `zh` 语言包,实现标准的三语(英/俄/中)无缝对齐。
38
+
39
+
31
40
  ### 🚀 v0.7.31 版本新特性
32
41
  - **⚡ O(1) 令牌桶累加器**:将速率限制计算升级为 O(1) 时间复杂度与零内存分配,并支持响应头自适应同步。
33
42
  - **🛡️ 软/硬故障分级退避**:区分临时网络抖动(502/503/超时获得 10 秒平缓冷却)与硬性配额超限(指数退避倍增)。
package/lib/client.js CHANGED
@@ -164,6 +164,76 @@ window.__ModuleLoader__.load({
164
164
  resetting: 'Сброс…',
165
165
  keyLabel: 'Ключ {n}',
166
166
  };
167
+ const zh = {
168
+ title: '密钥轮换',
169
+ filterAll: '全部',
170
+ filterReady: '就绪',
171
+ filterCooldown: '冷却中',
172
+ filterErrors: '故障',
173
+ subtitle: '按提供商轮换 API 密钥:密钥池管理、配额/限流自动故障转移、平滑冷却与自愈。',
174
+ cardDesc: '按提供商轮换 API 密钥:密钥池管理、配额/限流自动故障转移、平滑冷却与自愈。',
175
+ loading: '加载中…',
176
+ notRegistered: '未注册',
177
+ removeKey: '删除密钥',
178
+ removeProvider: '删除提供商',
179
+ addKey: '+ 添加密钥',
180
+ addKeyTitle: '添加 API 密钥',
181
+ noProviders: 'DSH 尚未注册任何提供商 — 暂无可选项目。',
182
+ addProvider: '+ 添加提供商',
183
+ desc: '按提供商轮换 API 密钥。为每个提供商配置其 API 密钥(环境变量名,存储于 DSH 凭据中)。插件按序调度请求,遇配额或速率超限自动切换至下一密钥。',
184
+ cooldown: '故障后冷却时间 (毫秒)',
185
+ scheduleDays: '轮换排期 (天数,0=关闭)',
186
+ switchCodes: '切换触发错误码 (逗号分隔)',
187
+ providersTitle: '提供商与密钥池',
188
+ save: '保存',
189
+ discard: '放弃',
190
+ saving: '保存中…',
191
+ moveUp: '上移',
192
+ moveDown: '下移',
193
+ keyActive: '正在使用',
194
+ keyReady: '就绪',
195
+ keyCooling: '冷却中,{s}秒',
196
+ keyMissing: '凭据未找到',
197
+ switchesNone: '暂无切换',
198
+ switchesSome: '已切换:{n} 次 · 最近:{reason},{ago}',
199
+ justNow: '刚刚',
200
+ minutesAgo: '{n} 分钟前',
201
+ hoursAgo: '{n} 小时前',
202
+ codesTitle: '遇以下故障自动切换',
203
+ keyValuePlaceholder: '粘贴密钥,然后点击保存',
204
+ keySave: '保存密钥',
205
+ keySaved: '已保存',
206
+ keyFromEnv: '来自环境变量,此处只读',
207
+ keyWriteFailed: '无法保存密钥:{msg}',
208
+ notSecretShape: '已保存,但格式不像常见 API 密钥 — 请检查是否有拼写错误',
209
+ rpmTitle: '请求数/分钟:已用 {u},剩余 {r}',
210
+ budgetLabel: '预算:',
211
+ exportCsv: '导出 CSV',
212
+ weightHint: '轮询权重:该密钥参与循环的轮次(1 = 均等分担)',
213
+ retestBroken: '重新测试',
214
+ retestFail: '重试失败 — 密钥仍不可用',
215
+ snapshotExport: '快照 ⬇',
216
+ snapshotImport: '恢复快照',
217
+ keyHint: '密钥安全保存在 DSH 凭据中,绝不返回浏览器 — 界面仅展示末尾 5 位字符。变量名自动生成,鼠标悬停可查看。',
218
+ brokenKey: '已损坏 (连续 3 次鉴权失败)',
219
+ keyExpired: '已过期',
220
+ keyExpiringSoon: '{n} 天后过期',
221
+ exportPools: '导出',
222
+ exportOne: '⬇',
223
+ usedAgo: '{ago}',
224
+ importPools: '导入',
225
+ importEnv: '导入 .env',
226
+ resetCooldown: '重置冷却',
227
+ testAll: '测试全部密钥',
228
+ testing: '测试中…',
229
+ testKey: '测试',
230
+ testOk: '正常',
231
+ testFail: '失败',
232
+ poolExhausted: '密钥池耗尽 — 所有密钥均处于冷却状态',
233
+ resetting: '重置中…',
234
+ keyLabel: '密钥 {n}',
235
+ };
236
+
167
237
 
168
238
  // Коды, на которых имеет смысл переключать ключ. Список из хоста
169
239
  // (DEFAULT_SWITCH_CODES); конфиг может содержать и свои — они показываются
@@ -318,7 +388,7 @@ window.__ModuleLoader__.load({
318
388
  }
319
389
 
320
390
  function KeyRotationSection(props) {
321
- const DICT = props.locale === 'ru' ? ru : en;
391
+ const DICT = props.locale === 'ru' ? ru : (props.locale === 'zh' ? zh : en);
322
392
  const t = makeT(DICT, en);
323
393
  const [state, setState] = React.useState({ status: 'loading', value: null, revision: 0, error: '', providers: [] });
324
394
  const [draft, setDraft] = React.useState(null);
@@ -1050,7 +1120,7 @@ window.__ModuleLoader__.load({
1050
1120
  // key MUST equal the settings namespace (NS), else the tab silently skips it.
1051
1121
  function KeyRotationCard(props) {
1052
1122
  const locale = useLocale();
1053
- const t = makeT(locale === 'ru' ? ru : en, en);
1123
+ const t = makeT(locale === 'ru' ? ru : (locale === 'zh' ? zh : en), en);
1054
1124
  const [open, setOpen] = React.useState(false);
1055
1125
  return h('div', { className: 'krot-card' + (open ? ' krot-card-open' : '') },
1056
1126
  h('button', { type: 'button', className: 'krot-card-header', 'aria-expanded': open, onClick: () => setOpen((v) => !v) },
@@ -1,72 +1,73 @@
1
- // concurrency.js — per-key in-flight counter + least-connections picking (issue #193).
2
-
3
- export const CONCURRENCY_DEFAULT_LIMIT = 0;
4
- export const CONCURRENCY_STALE_LOCK_MS = 5 * 60 * 1000;
5
-
6
- export class ConcurrencyTracker {
7
- constructor(opts) {
8
- opts = opts || {};
9
- const limit = opts.limit !== undefined ? opts.limit : CONCURRENCY_DEFAULT_LIMIT;
10
- this._limit = (Number.isFinite(limit) && limit >= 0) ? Math.floor(limit) : 0;
11
- this._staleMs = opts.staleMs || CONCURRENCY_STALE_LOCK_MS;
12
- this._inFlight = new Map();
13
- }
14
-
15
- isEnabled() { return this._limit > 0; }
16
- get limit() { return this._limit; }
17
-
18
- acquire(ref, now) {
19
- now = now || Date.now();
20
- if (!this.isEnabled()) return true;
21
- let e = this._inFlight.get(ref);
22
- if (!e) {
23
- e = { count: 0, lastAcquired: now };
24
- this._inFlight.set(ref, e);
25
- }
26
- if (now - e.lastAcquired > this._staleMs) {
27
- e.count = 0;
28
- }
29
- if (e.count >= this._limit) return false;
30
- e.count += 1;
31
- e.lastAcquired = now;
32
- return true;
33
- }
34
-
35
- release(ref, now) {
36
- now = now || Date.now();
37
- const e = this._inFlight.get(ref);
38
- if (!e) return;
39
- e.count = Math.max(0, e.count - 1);
40
- e.lastAcquired = now;
41
- }
42
-
43
- snapshot() {
44
- const out = {};
45
- for (const [k, v] of this._inFlight) out[k] = { count: v.count };
46
- return out;
47
- }
48
-
49
- pickLeastLoaded(candidates, now) {
50
- now = now || Date.now();
51
- if (!Array.isArray(candidates) || candidates.length === 0) return null;
52
- let best = null;
53
- let bestCount = Infinity;
54
- for (const ref of candidates) {
55
- const e = this._inFlight.get(ref);
56
- const count = e ? e.count : 0;
57
- if (this.isEnabled() && count >= this._limit) continue;
58
- if (count < bestCount) {
59
- best = ref;
60
- bestCount = count;
61
- }
62
- }
63
- return best;
64
- }
65
-
66
- clear(ref) {
67
- if (ref) this._inFlight.delete(ref);
68
- else this._inFlight.clear();
69
- }
70
-
71
- get size() { return this._inFlight.size; }
72
- }
1
+ // concurrency.js — per-key in-flight counter + least-connections picking (issue #193).
2
+
3
+ export const CONCURRENCY_DEFAULT_LIMIT = 0;
4
+ export const CONCURRENCY_STALE_LOCK_MS = 5 * 60 * 1000;
5
+
6
+ export class ConcurrencyTracker {
7
+ constructor(opts) {
8
+ opts = opts || {};
9
+ const limit = opts.limit !== undefined ? opts.limit : CONCURRENCY_DEFAULT_LIMIT;
10
+ this._limit = (Number.isFinite(limit) && limit >= 0) ? Math.floor(limit) : 0;
11
+ this._staleMs = opts.staleMs || CONCURRENCY_STALE_LOCK_MS;
12
+ this._inFlight = new Map();
13
+ }
14
+
15
+ isEnabled() { return this._limit > 0; }
16
+ get limit() { return this._limit; }
17
+
18
+ acquire(ref, now) {
19
+ now = now || Date.now();
20
+ if (!this.isEnabled()) return true;
21
+ let e = this._inFlight.get(ref);
22
+ if (!e) {
23
+ e = { count: 0, lastAcquired: now };
24
+ this._inFlight.set(ref, e);
25
+ }
26
+ if (now - e.lastAcquired > this._staleMs) {
27
+ e.count = 0;
28
+ }
29
+ if (e.count >= this._limit) return false;
30
+ e.count += 1;
31
+ e.lastAcquired = now;
32
+ return true;
33
+ }
34
+
35
+ release(ref, now) {
36
+ now = now || Date.now();
37
+ const e = this._inFlight.get(ref);
38
+ if (!e) return;
39
+ e.count = Math.max(0, e.count - 1);
40
+ e.lastAcquired = now;
41
+ }
42
+
43
+ snapshot() {
44
+ const out = {};
45
+ for (const [k, v] of this._inFlight) out[k] = { count: v.count };
46
+ return out;
47
+ }
48
+
49
+ pickLeastLoaded(candidates, now) {
50
+ now = now || Date.now();
51
+ if (!Array.isArray(candidates) || candidates.length === 0) return null;
52
+ let best = null;
53
+ let bestCount = Infinity;
54
+ for (const ref of candidates) {
55
+ const e = this._inFlight.get(ref);
56
+ let count = e ? e.count : 0;
57
+ if (e && now - e.lastAcquired > this._staleMs) count = 0;
58
+ if (this.isEnabled() && count >= this._limit) continue;
59
+ if (count < bestCount) {
60
+ best = ref;
61
+ bestCount = count;
62
+ }
63
+ }
64
+ return best;
65
+ }
66
+
67
+ clear(ref) {
68
+ if (ref) this._inFlight.delete(ref);
69
+ else this._inFlight.clear();
70
+ }
71
+
72
+ get size() { return this._inFlight.size; }
73
+ }