@goodandready/dsh-key-rotation 0.7.35 → 0.7.37
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 +16 -0
- package/README.ru.md +16 -0
- package/README.zh.md +16 -0
- package/lib/client.js +27 -6
- package/lib/index.js +136 -334
- package/lib/pool.js +110 -1
- package/package.json +1 -1
- package/lib/agent-budget.js +0 -68
- package/lib/canary.js +0 -63
- package/lib/incident.js +0 -76
- package/lib/maintenance.js +0 -64
- package/lib/region.js +0 -50
- package/lib/shadow.js +0 -81
package/README.md
CHANGED
|
@@ -253,6 +253,22 @@ All management routes require loopback authentication (`127.0.0.1` / `::1`) with
|
|
|
253
253
|
|
|
254
254
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
255
|
|
|
256
|
+
### v0.7.37
|
|
257
|
+
- **Request Context Isolation via AsyncLocalStorage**: Scoped active key resolution (`pickedRef`), start timestamps, and retry counts strictly to each async dispatch context using Node's `node:async_hooks`. Eliminates race conditions where concurrent streaming requests could penalize healthy keys.
|
|
258
|
+
- **Failover on Pre-Yield Stream Exceptions**: Fixed fatal stream termination where transport errors (e.g. HTTP 429 thrown before headers, socket hang-up) aborted the generator. The catch block now inspects `isSwitchableError` and cascades seamlessly to the next key if no content tokens have been yielded.
|
|
259
|
+
- **Pure Local Candidate List in rotate()**: Generator uses an isolated local slice of keys (`attemptList`), eliminating shared mutations on `pool.weightedRefs`.
|
|
260
|
+
- **Automatic Quarantine Release on Probe Success**: Successful sandbox model tests via Settings UI (`/dsh-key-rotation/test`) automatically lift `failedUntil` and `brokenUntil` quarantine flags.
|
|
261
|
+
- **Long-Term Memory Compaction**: Wired `compactUsage(pool, 30, now)` into the periodic 30-second maintenance sweep to prevent memory growth on high-uptime servers.
|
|
262
|
+
- **Request-Scoped Latency Recording**: Replaced module-global start timestamp with context-scoped `startMs` for accurate p50/p95 latency metrics under concurrent load.
|
|
263
|
+
|
|
264
|
+
### v0.7.36
|
|
265
|
+
- **Architecture de-bloat & hardening**: Removed 6 unused/overengineered modules (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) and dead route registrations.
|
|
266
|
+
- **High-throughput buildRuntime memoization**: Eliminates per-token deep-cloning and schema validation on every streaming chunk.
|
|
267
|
+
- **Atomic round-robin pointer rotation**: Concurrent requests advance pointer immediately on candidate selection, eliminating race conditions on simultaneous tool calls.
|
|
268
|
+
- **Enhanced switchable error detection**: Direct parsing of HTTP status codes (`429`, `401`, `403`, `5xx`) and gRPC codes (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) alongside regex fallback.
|
|
269
|
+
- **Informative user exhaustion messaging**: Clear countdown notice with next key recovery ETA when all keys in a pool are cooling down.
|
|
270
|
+
- **Smart polling**: Client background polling paused when browser tab is inactive (`document.visibilityState`).
|
|
271
|
+
|
|
256
272
|
### v0.7.35
|
|
257
273
|
- **Lifecycle Cleanups**: Wrapped `credentials.resolve` patch and `ctx.on` event handlers (`llm/stream`, `agent/request-error`) in `ctx.effect` scopes with guaranteed unmount cleanup (#238, #239).
|
|
258
274
|
- **Settings & Secret Roles**: Added `.role('secret')` to `incidentGitHubToken` and `webhookActionToken` in `Config` schema for automatic UI masking (#237).
|
package/README.ru.md
CHANGED
|
@@ -253,6 +253,22 @@ dsh-key-rotation:
|
|
|
253
253
|
|
|
254
254
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
255
255
|
|
|
256
|
+
### v0.7.37
|
|
257
|
+
- **Изоляция контекста запросов через AsyncLocalStorage**: Полная привязка активного ключа (`pickedRef`), времени старта и попыток к асинхронному контексту вызова через `node:async_hooks`. Устранена гонка, при которой параллельные запросы могли ошибочно штрафовать здоровый ключ соседа.
|
|
258
|
+
- **Failover при исключениях в потоке до первого чанка**: Устранено аварийное прерывание потока при транспортных сбоях (например, выброс HTTP 429 до отправки заголовков). Теперь блок перехвата проверяет `isSwitchableError` и прозрачно переключает поток на запасной ключ, если клиенту ещё не было отдано полезных данных.
|
|
259
|
+
- **Чистый локальный список кандидатов в rotate()**: Устранена прямая мутация разделяемого массива `pool.weightedRefs` во время выполнения ротации.
|
|
260
|
+
- **Автоматический вывод из карантина при успехе проверки**: Успешная проверка ключа через песочницу в настройках (`/dsh-key-rotation/test`) моментально сбрасывает флаги карантина (`failedUntil` и `brokenUntil`).
|
|
261
|
+
- **Регулярная компактизация памяти**: Функция `compactUsage(pool, 30, now)` подключена в 30-секундный фоновый таймер пула, предотвращая утечки памяти при непрерывной многомесячной работе.
|
|
262
|
+
- **Точные метрики задержки при параллельных вызовах**: Переход с глобальной переменной времени старта на контекстный замер `startMs` гарантирует достоверность p50/p95 latency под нагрузкой.
|
|
263
|
+
|
|
264
|
+
### v0.7.36
|
|
265
|
+
- **Глубокий рефакторинг и деблоатинг**: Удалены 6 неиспользуемых модулей оверинжиниринга (`shadow`, `incident`, `agent-budget`, `region`, `canary`, `maintenance`) и сопутствующие устаревшие эндпоинты.
|
|
266
|
+
- **Мемоизация buildRuntime**: Устранено глубокое клонирование и повторный парсинг схемы на каждом токене/чанге стрима.
|
|
267
|
+
- **Атомарная ротация указателя**: Устранено состояние гонки при параллельных вызовах инструментов — указатель смещается сразу при выборе кандидата.
|
|
268
|
+
- **Точная детекция ошибок**: Прямая проверка кодов HTTP (`429`, `401`, `403`, `5xx`) и gRPC (`RESOURCE_EXHAUSTED`, `UNAVAILABLE`) с фоллбеком на регулярные выражения.
|
|
269
|
+
- **Понятные сообщения при исчерпании пула**: Информативное уведомление с таймером восстановления ближайшего ключа.
|
|
270
|
+
- **Энергоэффективный опрос (Smart Polling)**: Автоматическая приостановка фонового опроса при неактивной вкладке браузера (`document.visibilityState`).
|
|
271
|
+
|
|
256
272
|
### v0.7.35
|
|
257
273
|
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
258
274
|
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
package/README.zh.md
CHANGED
|
@@ -214,6 +214,22 @@ dsh-key-rotation:
|
|
|
214
214
|
|
|
215
215
|
MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
216
216
|
|
|
217
|
+
### v0.7.37
|
|
218
|
+
- **通过 AsyncLocalStorage 隔离请求上下文**:使用 Node.js 的 `node:async_hooks` 将解析后的密钥 (`pickedRef`)、启动时间和重试严格限定在单个请求上下文内,彻底消除并发请求间的竞态条件与误罚。
|
|
219
|
+
- **流异常自动故障转移**:修复流在首个 token 返回前抛出传输异常(如 HTTP 429)直接终止的问题。若未发送内容块,现在会自动触发 `isSwitchableError` 并顺畅切换到备用密钥。
|
|
220
|
+
- **避免在 rotate() 中直接修改共享状态**:遍历候选列表改用纯净的局部切片,不再直接覆盖修改 `pool.weightedRefs`。
|
|
221
|
+
- **测试成功后自动解除隔离**:在设置界面通过沙箱成功验证密钥有效性后,自动清除 `failedUntil` 和 `brokenUntil` 惩罚标记。
|
|
222
|
+
- **定期内存压缩清理**:将 `compactUsage(pool, 30, now)` 接入 30 秒后台巡检定时器,杜绝超长运行环境下的内存增长。
|
|
223
|
+
- **并发环境下的精确延迟统计**:为每个请求独立计时,避免全局变量被并发请求覆盖导致 p50/p95 延迟失真。
|
|
224
|
+
|
|
225
|
+
### v0.7.36
|
|
226
|
+
- **架构精简与稳定性加固**:移除 6 个过度设计的模块(`shadow`、`incident`、`agent-budget`、`region`、`canary`、`maintenance`)与废弃端点。
|
|
227
|
+
- **buildRuntime 高性能记忆化**:消除每个流式 token/chunk 上的深拷贝与模式重解析开销。
|
|
228
|
+
- **原子轮询指针推进**:并发请求在选定候选密钥时立即推进指针,消除并发工具调用中的竞争条件。
|
|
229
|
+
- **增强的可切换错误检测**:直接解析 HTTP 状态码(`429`, `401`, `403`, `5xx`)与 gRPC 状态码(`RESOURCE_EXHAUSTED`, `UNAVAILABLE`)。
|
|
230
|
+
- **用户友好的耗尽提示**:密钥池耗尽时返回带恢复倒计时的清晰通知。
|
|
231
|
+
- **智能轮询(Smart Polling)**:标签页不活动时暂停客户端后台轮询。
|
|
232
|
+
|
|
217
233
|
### v0.7.35
|
|
218
234
|
- **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
|
|
219
235
|
- **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
|
package/lib/client.js
CHANGED
|
@@ -240,12 +240,13 @@ window.__ModuleLoader__.load({
|
|
|
240
240
|
// отдельными отмеченными галочками, чтобы правило нельзя было потерять.
|
|
241
241
|
const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
|
|
242
242
|
|
|
243
|
-
/** Опрос статуса ротации, пока раздел настроек
|
|
243
|
+
/** Опрос статуса ротации, пока раздел настроек открыт (smart polling). */
|
|
244
244
|
function useRotationStatus() {
|
|
245
245
|
const [byProvider, setByProvider] = React.useState({});
|
|
246
246
|
React.useEffect(() => {
|
|
247
247
|
let alive = true;
|
|
248
248
|
const pull = () => {
|
|
249
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
249
250
|
fetch('/dsh-key-rotation/status', { headers: { accept: 'application/json' } })
|
|
250
251
|
.then((r) => (r.ok ? r.json() : null))
|
|
251
252
|
.then((data) => {
|
|
@@ -258,17 +259,24 @@ window.__ModuleLoader__.load({
|
|
|
258
259
|
};
|
|
259
260
|
pull();
|
|
260
261
|
const id = setInterval(pull, 4000);
|
|
261
|
-
|
|
262
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
|
|
263
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
264
|
+
return () => {
|
|
265
|
+
alive = false;
|
|
266
|
+
clearInterval(id);
|
|
267
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
268
|
+
};
|
|
262
269
|
}, []);
|
|
263
270
|
return byProvider;
|
|
264
271
|
}
|
|
265
272
|
|
|
266
|
-
/** Последний probe-результат по каждому ключу (#219): /sandbox-cache. */
|
|
273
|
+
/** Последний probe-результат по каждому ключу (#219): /sandbox-cache (smart polling). */
|
|
267
274
|
function useProbeCache() {
|
|
268
275
|
const [cache, setCache] = React.useState({});
|
|
269
276
|
React.useEffect(() => {
|
|
270
277
|
let alive = true;
|
|
271
278
|
const pull = () => {
|
|
279
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
272
280
|
fetch('/dsh-key-rotation/sandbox-cache', { headers: { accept: 'application/json' } })
|
|
273
281
|
.then((r) => (r.ok ? r.json() : null))
|
|
274
282
|
.then((data) => { if (alive && data) setCache(data); })
|
|
@@ -276,7 +284,13 @@ window.__ModuleLoader__.load({
|
|
|
276
284
|
};
|
|
277
285
|
pull();
|
|
278
286
|
const id = setInterval(pull, 4000);
|
|
279
|
-
|
|
287
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') pull(); };
|
|
288
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
289
|
+
return () => {
|
|
290
|
+
alive = false;
|
|
291
|
+
clearInterval(id);
|
|
292
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
293
|
+
};
|
|
280
294
|
}, []);
|
|
281
295
|
return cache;
|
|
282
296
|
}
|
|
@@ -1076,14 +1090,21 @@ window.__ModuleLoader__.load({
|
|
|
1076
1090
|
React.useEffect(() => {
|
|
1077
1091
|
let alive = true;
|
|
1078
1092
|
const load = () => {
|
|
1093
|
+
if (typeof document !== 'undefined' && document.visibilityState !== 'visible') return;
|
|
1079
1094
|
fetch('/dsh-key-rotation/health', { headers: { accept: 'application/json' }, credentials: 'same-origin' })
|
|
1080
1095
|
.then((r) => (r.ok ? r.json() : null))
|
|
1081
1096
|
.then((d) => { if (alive) setSnap(d); })
|
|
1082
1097
|
.catch(() => {});
|
|
1083
1098
|
};
|
|
1084
1099
|
load();
|
|
1085
|
-
const id = setInterval(load,
|
|
1086
|
-
|
|
1100
|
+
const id = setInterval(load, 5000);
|
|
1101
|
+
const onVis = () => { if (typeof document !== 'undefined' && document.visibilityState === 'visible') load(); };
|
|
1102
|
+
if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
|
|
1103
|
+
return () => {
|
|
1104
|
+
alive = false;
|
|
1105
|
+
clearInterval(id);
|
|
1106
|
+
if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
|
|
1107
|
+
};
|
|
1087
1108
|
}, []);
|
|
1088
1109
|
|
|
1089
1110
|
React.useEffect(() => {
|