@goodandready/dsh-key-rotation 0.7.6 → 0.7.9
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/lib/client.js +66 -7
- package/package.json +4 -1
package/lib/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// dsh-key-rotation — Settings section ("Key Rotation" / "Ротация ключей").
|
|
2
|
-
// Renders in
|
|
2
|
+
// Renders in Settings → Plugins → Plugin settings via the settings.plugin.item slot and
|
|
3
3
|
// edits the plugin's `dsh-key-rotation` settings namespace through the
|
|
4
4
|
// loopback-fenced config bridge at /dsh-key-rotation/config.
|
|
5
5
|
//
|
|
@@ -27,6 +27,8 @@ window.__ModuleLoader__.load({
|
|
|
27
27
|
// -------------------------------------------------------------- i18n
|
|
28
28
|
const en = {
|
|
29
29
|
title: 'Key Rotation',
|
|
30
|
+
subtitle: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
|
|
31
|
+
cardDesc: 'Per-provider API key rotation: pools of keys, automatic failover on quota/rate-limit errors, cooldown and recovery.',
|
|
30
32
|
loading: 'Loading…',
|
|
31
33
|
notRegistered: 'not registered',
|
|
32
34
|
removeKey: 'Remove key',
|
|
@@ -67,6 +69,8 @@ window.__ModuleLoader__.load({
|
|
|
67
69
|
importPools: 'Import',
|
|
68
70
|
importEnv: 'Import .env',
|
|
69
71
|
resetCooldown: 'Reset cooldown',
|
|
72
|
+
testAll: 'Test all keys',
|
|
73
|
+
testing: 'Testing…',
|
|
70
74
|
testKey: 'Test',
|
|
71
75
|
testOk: 'OK',
|
|
72
76
|
testFail: 'FAIL',
|
|
@@ -76,6 +80,8 @@ window.__ModuleLoader__.load({
|
|
|
76
80
|
};
|
|
77
81
|
const ru = {
|
|
78
82
|
title: 'Ротация ключей',
|
|
83
|
+
subtitle: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
|
|
84
|
+
cardDesc: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
|
|
79
85
|
loading: 'Загрузка…',
|
|
80
86
|
notRegistered: 'не зарегистрирован',
|
|
81
87
|
removeKey: 'Удалить ключ',
|
|
@@ -116,6 +122,8 @@ window.__ModuleLoader__.load({
|
|
|
116
122
|
importPools: 'Импорт',
|
|
117
123
|
importEnv: 'Импорт .env',
|
|
118
124
|
resetCooldown: 'Сбросить кулдаун',
|
|
125
|
+
testAll: 'Тест всех ключей',
|
|
126
|
+
testing: 'Тестирование…',
|
|
119
127
|
testKey: 'Тест',
|
|
120
128
|
testOk: 'OK',
|
|
121
129
|
testFail: 'FAIL',
|
|
@@ -194,6 +202,13 @@ window.__ModuleLoader__.load({
|
|
|
194
202
|
'.krot-btn:not(:disabled):hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l3,var(--dsw-alias-border-l2))}',
|
|
195
203
|
'.krot-foot{display:flex;gap:8px;align-items:center}',
|
|
196
204
|
'.krot-save{background:var(--dsw-alias-button-info-fill);border-color:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground);padding:5px 14px;font-size:13px}',
|
|
205
|
+
'.krot-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;overflow:hidden}',
|
|
206
|
+
'.krot-card-header{display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;padding:10px 12px;background:transparent;border:none;cursor:pointer;color:var(--dsw-alias-label-primary);font:inherit;text-align:left}',
|
|
207
|
+
'.krot-card-head-text{display:flex;flex-direction:column;gap:2px;min-width:0}',
|
|
208
|
+
'.krot-card-name{font-weight:600;font-size:13px}',
|
|
209
|
+
'.krot-card-description{font-size:11px;color:var(--dsw-alias-label-tertiary)}',
|
|
210
|
+
'.krot-card-chevron{flex:none;opacity:.6}',
|
|
211
|
+
'.krot-card-body{padding:0 12px 12px;border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
197
212
|
].join('');
|
|
198
213
|
const CARD_CSS_ID = 'dsh-key-rotation/section.module.css';
|
|
199
214
|
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin-css="' + CARD_CSS_ID + '"]')) {
|
|
@@ -258,6 +273,7 @@ window.__ModuleLoader__.load({
|
|
|
258
273
|
const undoTimer = React.useRef(null);
|
|
259
274
|
const [testing, setTesting] = React.useState('');
|
|
260
275
|
const [testResult, setTestResult] = React.useState({});
|
|
276
|
+
const [testAllProvider, setTestAllProvider] = React.useState('');
|
|
261
277
|
const [secretDraft, setSecretDraft] = React.useState({});
|
|
262
278
|
const [secretError, setSecretError] = React.useState('');
|
|
263
279
|
const stashUndo = (u) => { setUndo(u); if (undoTimer.current) clearTimeout(undoTimer.current); undoTimer.current = setTimeout(() => setUndo(null), 5000); };
|
|
@@ -275,6 +291,22 @@ window.__ModuleLoader__.load({
|
|
|
275
291
|
.catch((e) => setTestResult((m) => ({ ...m, [ref]: { ok: false, message: String(e?.message ?? e) } })))
|
|
276
292
|
.finally(() => setTesting(''));
|
|
277
293
|
};
|
|
294
|
+
const doTestAll = (providerId) => {
|
|
295
|
+
if (!val || !Array.isArray(val.providers)) return;
|
|
296
|
+
const entry = val.providers.find((p) => p.provider === providerId);
|
|
297
|
+
if (!entry || !Array.isArray(entry.keys)) return;
|
|
298
|
+
const refs = entry.keys.filter((k) => k && typeof k === 'string' && k.length > 0);
|
|
299
|
+
setTestAllProvider(providerId);
|
|
300
|
+
Promise.all(refs.map((ref) =>
|
|
301
|
+
fetch('/dsh-key-rotation/test', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ref }) })
|
|
302
|
+
.then((r) => r.json())
|
|
303
|
+
.then((data) => ({ ref, data }))
|
|
304
|
+
.catch((e) => ({ ref, data: { ok: false, message: String(e?.message ?? e) } }))
|
|
305
|
+
)).then((results) => {
|
|
306
|
+
setTestResult((m) => { const nm = { ...m }; for (const { ref, data } of results) nm[ref] = data; return nm; });
|
|
307
|
+
setTestAllProvider('');
|
|
308
|
+
});
|
|
309
|
+
};
|
|
278
310
|
|
|
279
311
|
const load = React.useCallback(() => {
|
|
280
312
|
setState((s) => ({ ...s, status: 'loading', error: '' }));
|
|
@@ -569,6 +601,7 @@ window.__ModuleLoader__.load({
|
|
|
569
601
|
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '24px', marginTop: '4px' } }, (() => { const now = Date.now(); const buckets = Array(24).fill(0); for (const ev of providerStatus.events) { const h = Math.floor((now - ev.at) / 3600000); if (h >= 0 && h < 24) buckets[23 - h]++; } const max = Math.max(1, ...buckets); return buckets.map((c, i) => h('div', { key: i, title: c + ' switches', style: { flex: 1, background: c ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-border-l2)', height: (c / max * 24) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
|
|
570
602
|
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', { style: { fontSize: '11px', marginTop: '6px' } }, h('summary', null, 'Recent failures ('+providerStatus.events.length+')'), h('ul', { style: { margin: '4px 0 0', paddingLeft: '16px' } }, providerStatus.events.slice().reverse().map((ev, i) => h('li', { key: i, style: ev.type === 'probe' ? { opacity: .5 } : null }, new Date(ev.at).toLocaleTimeString() + ' ' + (ev.type === 'probe' ? '[probe] ' : '') + ev.ref + ' ' + ev.reason + ' cd=' + ev.cooldownMs)) )) : null),
|
|
571
603
|
btn(t('resetCooldown'), () => doReset(entry.provider), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
|
|
604
|
+
btn(testAllProvider === entry.provider ? t('testing') : t('testAll'), () => doTestAll(entry.provider), { disabled: testAllProvider === entry.provider, title: t('testAll') }),
|
|
572
605
|
),
|
|
573
606
|
);
|
|
574
607
|
});
|
|
@@ -634,12 +667,38 @@ window.__ModuleLoader__.load({
|
|
|
634
667
|
function useLocale() {
|
|
635
668
|
return useActiveLocale(ctx);
|
|
636
669
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
670
|
+
// Collapsible card in Settings -> Plugins -> Plugin settings
|
|
671
|
+
// (settings.plugin.item), matching Model Sync / Spendmeter / Vision Bridge.
|
|
672
|
+
// key MUST equal the settings namespace (NS), else the tab silently skips it.
|
|
673
|
+
function KeyRotationCard(props) {
|
|
674
|
+
const locale = useLocale();
|
|
675
|
+
const t = makeT(locale === 'ru' ? ru : en, en);
|
|
676
|
+
const [open, setOpen] = React.useState(false);
|
|
677
|
+
return h('div', { className: 'krot-card' + (open ? ' krot-card-open' : '') },
|
|
678
|
+
h('button', { type: 'button', className: 'krot-card-header', 'aria-expanded': open, onClick: () => setOpen((v) => !v) },
|
|
679
|
+
h('span', { className: 'krot-card-head-text' },
|
|
680
|
+
h('span', { className: 'krot-card-name' }, t('title')),
|
|
681
|
+
h('span', { className: 'krot-card-description' }, t('subtitle'))),
|
|
682
|
+
h('span', { className: 'krot-card-chevron', 'aria-hidden': 'true' }, open ? '▴' : '▾')),
|
|
683
|
+
open ? h('div', { className: 'krot-card-body' }, h(KeyRotationSection, { ...props, locale })) : null);
|
|
684
|
+
}
|
|
685
|
+
const tryPluginItem = () => {
|
|
686
|
+
try {
|
|
687
|
+
ctx.slots.inject('settings.plugin.item', () =>
|
|
688
|
+
ctx.slots.register(
|
|
689
|
+
{ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
|
|
690
|
+
KeyRotationCard,
|
|
691
|
+
));
|
|
692
|
+
return true;
|
|
693
|
+
} catch { return false; }
|
|
694
|
+
};
|
|
695
|
+
if (!tryPluginItem()) {
|
|
696
|
+
// Fallback for builds without the Plugins tab slot: keep the sidebar section.
|
|
697
|
+
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
698
|
+
{ name: 'settings.section', id: 'dsh-key-rotation', order: 20, label: () => 'Key Rotation' },
|
|
699
|
+
(props) => h(KeyRotationSection, { ...props, locale: useLocale() }),
|
|
700
|
+
));
|
|
701
|
+
}
|
|
643
702
|
}
|
|
644
703
|
|
|
645
704
|
module.exports = { apply, inject: ['slots', 'locale'] };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Per-provider API key rotation for DeepSeek Harness: a key pool per provider, auto-created clone routes, and switching to the next key on quota/rate-limit errors. Includes a Settings section (Key Rotation) to edit the key pools, cooldown and switch codes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -54,5 +54,8 @@
|
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"test": "node --test test/*.test.js"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
57
60
|
}
|
|
58
61
|
}
|