@goodandready/dsh-key-rotation 0.8.1 → 0.8.3
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 +3 -2
- package/README.ru.md +2 -1
- package/README.zh.md +2 -1
- package/lib/client.js +290 -280
- package/lib/http-bridge.js +2 -1
- package/lib/index.js +6 -1
- package/lib/rotate.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
- **🕒 Accurate Midnight PST Resets**: Corrected UTC-8 timezone calculation offset sign for calendar quota reset windows.
|
|
46
46
|
- **🧹 Lifecycle Timer Cleanup**: Wrapped `canaryTimer` and `selfHealTimer` in Cordis effect scopes, eliminating background orphaned intervals on hot reload.
|
|
47
47
|
- **⚡ Stale Lock Recovery in Load Balancer**: Added expired lock detection to `pickLeastLoaded` for uninterrupted least-connections routing.
|
|
48
|
-
-
|
|
48
|
+
- **🎨 Native Design System (Changed in v0.8.3)**: Unified with `dsh-clinebot` baseline: modular section cards, live pool telemetry stat boxes, pill badges, and complete semantic theme token styling (#281).
|
|
49
|
+
- **🌐 Localization (Changed in v0.8.2)**: Source strings are English-only. Russian/Chinese UI comes from the DSH core locale service and translation plugins (`props.t`). Active locale: host snapshot → first `navigator.languages` entry → `en` (#277).
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
### 🚀 What's New in v0.7.31
|
|
@@ -301,5 +302,5 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
301
302
|
- **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).
|
|
302
303
|
- **Settings & Secret Roles**: Added `.role('secret')` to `incidentGitHubToken` and `webhookActionToken` in `Config` schema for automatic UI masking (#237).
|
|
303
304
|
- **Settings Architecture & UI**: Added native `settingsScope` snapshot reading/saving in settings card with graceful bridge fallback (#235).
|
|
304
|
-
- **Localization**:
|
|
305
|
+
- **Localization (Changed in v0.8.2)**: Card uses `props.t` with `locale: NS`; plugin registers only `en`; no `settings.section` fallback and no bundled `ru`/`zh` tables (#236, #275, #277).
|
|
305
306
|
- **Dead Code Purge**: Removed obsolete `mountDashboard` routine after header-chip migration (#240).
|
package/README.ru.md
CHANGED
|
@@ -295,7 +295,8 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
295
295
|
- **Очистка жизненного цикла**: Патч `credentials.resolve` и слушатели событий `ctx.on` (`llm/stream`, `agent/request-error`) переведены в скоупы `ctx.effect` с автоматическим восстановлением функций и отпиской при выгрузке плагина (#238, #239).
|
|
296
296
|
- **Роли секретов в схеме**: Полям `incidentGitHubToken` и `webhookActionToken` в схеме `Config` присвоена роль `.role('secret')` для маскирования в UI (#237).
|
|
297
297
|
- **Архитектура настроек**: Добавлена нативная интеграция со снимками `settingsScope` в карточке настроек с безопасным фоллбеком на HTTP-мост (#235).
|
|
298
|
-
-
|
|
298
|
+
- **Нативная дизайн-система (Changed in v0.8.3)**: Унификация со стилем `dsh-clinebot`: модульные карточки секций, плитки оперативной телеметрии пулов, капсульные бейджи и системные CSS-токены DSH (#281).
|
|
299
|
+
- **Локализация (Changed in v0.8.2)**: Исходные строки только на английском (`en`). Русский и китайский приходят через core `props.t` / translation-плагины. Фолбек активной локали: snapshot → первый `navigator.languages` → `en`. Свой `settings.section` и встроенные `ru`/`zh`-таблицы удалены (#236, #275, #277).
|
|
299
300
|
- **Удаление мёртвого кода**: Удалена неиспользуемая функция `mountDashboard` после перехода на header chip (#240).
|
|
300
301
|
|
|
301
302
|
### Параметры circuit breaker (Changed in v0.8.0)
|
package/README.zh.md
CHANGED
|
@@ -259,7 +259,8 @@ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
|
|
|
259
259
|
- **生命周期清理**: 将 `credentials.resolve` 猴子补丁和 `ctx.on` 事件监听器 (`llm/stream`, `agent/request-error`) 封装在 `ctx.effect` 作用域内,确保卸载时自动注销并恢复原始方法 (#238, #239)。
|
|
260
260
|
- **配置密钥角色**: 在 `Config` Schema 中为 `incidentGitHubToken` 和 `webhookActionToken` 增加 `.role('secret')`,避免明文泄露并在 UI 中掩码显示 (#237)。
|
|
261
261
|
- **设置架构与状态**: 在设置卡片中增加原生 `settingsScope` 绑定支持,保留 HTTP 桥接安全回退机制 (#235)。
|
|
262
|
-
-
|
|
262
|
+
- **原生设计系统 (Changed in v0.8.3)**: 与 `dsh-clinebot` 基准对齐:模块化分区卡片、实时密钥池指标块、胶囊状态徽章与全局主题语义 token (#281)。
|
|
263
|
+
- **本地化与文案 (Changed in v0.8.2)**: 插件仅注册英文源字符串 `en`;俄文/中文由 DSH 核心 locale 与 translation 插件通过 `props.t` 提供。活动语言回退:snapshot → 首个 `navigator.languages` → `en`。已移除 `settings.section` 回退与内置 `ru`/`zh` 表 (#236, #275, #277)。
|
|
263
264
|
- **死代码清理**: 移除 header-chip 迁移后残留的废弃 `mountDashboard` 函数 (#240)。
|
|
264
265
|
|
|
265
266
|
|
package/lib/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// dsh-key-rotation — Settings
|
|
1
|
+
// dsh-key-rotation — Settings card (Key Rotation).
|
|
2
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.
|
|
@@ -9,11 +9,10 @@
|
|
|
9
9
|
// manual route typing is ever needed. The plugin derives the fallback chain and
|
|
10
10
|
// auto-creates clone routes from the key count.
|
|
11
11
|
//
|
|
12
|
-
// Localization:
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// DSH UI locale changes (Settings → Language).
|
|
12
|
+
// Localization: source strings are English only (ctx.locale.register(NS, { en })).
|
|
13
|
+
// Other languages come from the DSH core locale service / translation plugins
|
|
14
|
+
// via props.t (slot locale: NS). Active locale prefers ctx.locale.getSnapshot().active,
|
|
15
|
+
// else first navigator.languages entry, else 'en' — same fallback chain as DSH core.
|
|
17
16
|
window.__ModuleLoader__.load({
|
|
18
17
|
id: '@goodandready/dsh-key-rotation',
|
|
19
18
|
factory: (require) => {
|
|
@@ -28,6 +27,17 @@ window.__ModuleLoader__.load({
|
|
|
28
27
|
// -------------------------------------------------------------- i18n
|
|
29
28
|
const en = {
|
|
30
29
|
title: 'Key Rotation',
|
|
30
|
+
sectionStats: 'Pool Health & Telemetry',
|
|
31
|
+
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
32
|
+
sectionFailover: 'Failover Triggers & Error Codes',
|
|
33
|
+
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
34
|
+
sectionTiming: 'Timing & Rotation Schedule',
|
|
35
|
+
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
36
|
+
sectionBackup: 'Backup & Snapshots',
|
|
37
|
+
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
38
|
+
statPools: 'Configured Pools',
|
|
39
|
+
statKeys: 'Total Keys',
|
|
40
|
+
statHealthy: 'Healthy / Ready',
|
|
31
41
|
filterAll: 'All',
|
|
32
42
|
filterReady: 'Ready',
|
|
33
43
|
filterCooldown: 'In Cooldown',
|
|
@@ -94,146 +104,10 @@ window.__ModuleLoader__.load({
|
|
|
94
104
|
poolExhausted: 'pool exhausted — all keys cooling',
|
|
95
105
|
resetting: 'Resetting…',
|
|
96
106
|
keyLabel: 'Key {n}',
|
|
107
|
+
undoProvider: 'Provider removed',
|
|
108
|
+
undoKey: 'Key removed',
|
|
109
|
+
undo: 'Undo',
|
|
97
110
|
};
|
|
98
|
-
const ru = {
|
|
99
|
-
title: 'Ротация ключей',
|
|
100
|
-
filterAll: 'Все',
|
|
101
|
-
filterReady: 'Готовы',
|
|
102
|
-
filterCooldown: 'В кулдауне',
|
|
103
|
-
filterErrors: 'С ошибками',
|
|
104
|
-
subtitle: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
|
|
105
|
-
cardDesc: 'Ротация API-ключей по провайдерам: пулы ключей, автоматическое переключение при исчерпании квоты или лимита, кулдаун и восстановление.',
|
|
106
|
-
loading: 'Загрузка…',
|
|
107
|
-
notRegistered: 'не зарегистрирован',
|
|
108
|
-
removeKey: 'Удалить ключ',
|
|
109
|
-
removeProvider: 'Удалить провайдера',
|
|
110
|
-
addKey: '+ Добавить ключ',
|
|
111
|
-
addKeyTitle: 'Добавить API-ключ',
|
|
112
|
-
noProviders: 'Провайдеры ещё не зарегистрированы в DSH — выбирать не из чего.',
|
|
113
|
-
addProvider: '+ Добавить провайдера',
|
|
114
|
-
desc: 'Ротация API-ключей по провайдерам. Для каждого провайдера укажите его API-ключи (имена env, хранятся в учётных данных DSH). Плагин ведёт модель по ключам провайдера по порядку и переключается на следующий при исчерпании квоты/превышении лимита.',
|
|
115
|
-
cooldown: 'Задержка после сбоя (мс)',
|
|
116
|
-
scheduleDays: 'Расписание ротации (дней, 0=выкл)',
|
|
117
|
-
switchCodes: 'Коды переключения (через запятую)',
|
|
118
|
-
providersTitle: 'Провайдеры и их ключи',
|
|
119
|
-
save: 'Сохранить',
|
|
120
|
-
discard: 'Отменить',
|
|
121
|
-
saving: 'Сохранение…',
|
|
122
|
-
moveUp: 'Выше',
|
|
123
|
-
moveDown: 'Ниже',
|
|
124
|
-
keyActive: 'используется',
|
|
125
|
-
keyReady: 'готов',
|
|
126
|
-
keyCooling: 'остывает, {s}с',
|
|
127
|
-
keyMissing: 'ключ не найден',
|
|
128
|
-
switchesNone: 'переключений не было',
|
|
129
|
-
switchesSome: 'переключений: {n} · последнее: {reason}, {ago}',
|
|
130
|
-
justNow: 'только что',
|
|
131
|
-
minutesAgo: '{n} мин назад',
|
|
132
|
-
hoursAgo: '{n} ч назад',
|
|
133
|
-
codesTitle: 'Переключаться при этих сбоях',
|
|
134
|
-
keyValuePlaceholder: 'вставьте ключ и нажмите «Сохранить»',
|
|
135
|
-
keySave: 'Сохранить ключ',
|
|
136
|
-
keySaved: 'сохранён',
|
|
137
|
-
keyFromEnv: 'задан в окружении, отсюда не меняется',
|
|
138
|
-
keyWriteFailed: 'не удалось сохранить ключ: {msg}',
|
|
139
|
-
notSecretShape: 'сохранено, но значение не похоже на API-ключ — проверьте опечатки',
|
|
140
|
-
rpmTitle: 'запросов/мин: {u} использовано, {r} осталось',
|
|
141
|
-
budgetLabel: 'бюджет:',
|
|
142
|
-
exportCsv: 'CSV',
|
|
143
|
-
weightHint: 'вес в круге: сколько раз ключ участвует в ротации (1 = поровну)',
|
|
144
|
-
retestBroken: 'Re-test',
|
|
145
|
-
retestFail: 'перепроверка не прошла — ключ всё ещё недоступен',
|
|
146
|
-
snapshotExport: 'Снапшот ⬇',
|
|
147
|
-
snapshotImport: 'Восстановить',
|
|
148
|
-
keyHint: 'Значение хранится в учётных данных DSH и обратно в браузер не отдаётся — показываются только последние 5 символов. Имена переменных создаются автоматически; наведите на ключ, чтобы увидеть используемое имя.',
|
|
149
|
-
brokenKey: 'сломан (3× AUTH)',
|
|
150
|
-
keyExpired: 'истёк',
|
|
151
|
-
keyExpiringSoon: 'истекает через {n} д',
|
|
152
|
-
exportPools: 'Экспорт',
|
|
153
|
-
exportOne: '⬇',
|
|
154
|
-
usedAgo: '{ago} назад',
|
|
155
|
-
importPools: 'Импорт',
|
|
156
|
-
importEnv: 'Импорт .env',
|
|
157
|
-
resetCooldown: 'Сбросить кулдаун',
|
|
158
|
-
testAll: 'Тест всех ключей',
|
|
159
|
-
testing: 'Тестирование…',
|
|
160
|
-
testKey: 'Тест',
|
|
161
|
-
testOk: 'OK',
|
|
162
|
-
testFail: 'FAIL',
|
|
163
|
-
poolExhausted: 'пул исчерпан — все ключи остывают',
|
|
164
|
-
resetting: 'Сброс…',
|
|
165
|
-
keyLabel: 'Ключ {n}',
|
|
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
|
-
|
|
237
111
|
|
|
238
112
|
// Коды, на которых имеет смысл переключать ключ. Список из хоста
|
|
239
113
|
// (DEFAULT_SWITCH_CODES); конфиг может содержать и свои — они показываются
|
|
@@ -336,54 +210,81 @@ window.__ModuleLoader__.load({
|
|
|
336
210
|
// на поле значения, поэтому имя занимает свою строку, а служебная строка
|
|
337
211
|
// под ним ужимается сама.
|
|
338
212
|
const CARD_CSS = [
|
|
339
|
-
'.krot{display:flex;flex-direction:column;gap:
|
|
213
|
+
'.krot{display:flex;flex-direction:column;gap:18px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
|
|
340
214
|
'.krot p{margin:0}',
|
|
341
|
-
'.krot-hint{font-size:
|
|
342
|
-
'.krot-err{font-size:
|
|
343
|
-
'.krot-label{font-size:
|
|
344
|
-
'.krot-field{display:flex;flex-direction:column;gap:
|
|
345
|
-
'.krot-in{background:var(--dsw-
|
|
346
|
-
'.krot-in:focus{outline:none;border-color:var(--dsw-alias-
|
|
347
|
-
'.krot-codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(
|
|
348
|
-
'.krot-code{display:flex;align-items:center;gap:
|
|
349
|
-
'.krot-
|
|
350
|
-
'.krot-
|
|
351
|
-
'.krot-
|
|
215
|
+
'.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
|
|
216
|
+
'.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);display:flex;align-items:center;gap:8px}',
|
|
217
|
+
'.krot-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
218
|
+
'.krot-field{display:flex;flex-direction:column;gap:6px}',
|
|
219
|
+
'.krot-in{background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;height:36px;font-family:inherit;min-width:0;width:100%;box-sizing:border-box;transition:border-color .15s ease}',
|
|
220
|
+
'.krot-in:focus{outline:none;border-color:var(--dsw-alias-state-brand-primary)}',
|
|
221
|
+
'.krot-codes{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:8px 14px}',
|
|
222
|
+
'.krot-code{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--dsw-alias-label-primary);cursor:pointer;user-select:none}',
|
|
223
|
+
'.krot-section-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;padding:18px 20px;display:flex;flex-direction:column;gap:14px}',
|
|
224
|
+
'.krot-section-title{font-size:15px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;justify-content:space-between;gap:8px}',
|
|
225
|
+
'.krot-section-desc{font-size:13px;color:var(--dsw-alias-label-secondary);margin-top:-6px;line-height:1.4}',
|
|
226
|
+
'.krot-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}',
|
|
227
|
+
'.krot-grid-2{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px}',
|
|
228
|
+
'.krot-grid-3{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}',
|
|
229
|
+
'.krot-stat-box{padding:12px 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);display:flex;flex-direction:column;gap:4px}',
|
|
230
|
+
'.krot-stat-val{font-size:20px;font-weight:700;color:var(--dsw-alias-label-primary)}',
|
|
231
|
+
'.krot-stat-lbl{font-size:12px;color:var(--dsw-alias-label-secondary)}',
|
|
232
|
+
'.krot-badge{font-size:12px;padding:3px 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);display:inline-flex;align-items:center;gap:5px;font-weight:500;white-space:nowrap}',
|
|
233
|
+
'.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:rgba(16,185,129,0.08)}',
|
|
234
|
+
'.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:rgba(245,158,11,0.08)}',
|
|
235
|
+
'.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:rgba(239,68,68,0.08)}',
|
|
236
|
+
'.krot-prov{display:flex;flex-direction:column;gap:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);padding:14px 16px}',
|
|
237
|
+
'.krot-prov-head{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
238
|
+
'.krot-prov-head select{flex:1;min-width:180px}',
|
|
352
239
|
'.krot-keys{display:flex;flex-direction:column;gap:8px}',
|
|
353
|
-
'.krot-key{display:grid;grid-template-columns:
|
|
354
|
-
'.krot-num{font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:right}',
|
|
355
|
-
'.krot-name{font-size:13px;color:var(--dsw-alias-label-primary);cursor:default}',
|
|
240
|
+
'.krot-key{display:grid;grid-template-columns:22px minmax(0,1fr);gap:6px 10px;align-items:center;padding:8px 12px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
|
|
241
|
+
'.krot-num{font-size:12px;font-weight:600;color:var(--dsw-alias-label-tertiary);text-align:right}',
|
|
242
|
+
'.krot-name{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary);cursor:default}',
|
|
356
243
|
'.krot-meta{grid-column:2;display:flex;align-items:center;gap:8px;flex-wrap:wrap}',
|
|
357
244
|
'.krot-dot{width:8px;height:8px;border-radius:50%;flex:none}',
|
|
358
|
-
'.krot-state{font-size:
|
|
359
|
-
'.krot-tail{font-size:11px;color:var(--dsw-alias-label-
|
|
245
|
+
'.krot-state{font-size:12px;color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
246
|
+
'.krot-tail{font-size:11px;color:var(--dsw-alias-label-secondary);font-family:ui-monospace,Menlo,Consolas,monospace;background:var(--dsw-alias-bg-layer-1);padding:2px 6px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2)}',
|
|
360
247
|
'.krot-secret{flex:1;min-width:120px;max-width:220px}',
|
|
361
248
|
'.krot-acts{display:flex;gap:4px;margin-left:auto;flex:none}',
|
|
362
|
-
'.krot-btn{cursor:pointer;border-radius:
|
|
363
|
-
'.krot-btn:disabled{
|
|
364
|
-
'.krot-btn
|
|
365
|
-
'.krot-
|
|
366
|
-
'.krot-
|
|
367
|
-
'.krot-
|
|
368
|
-
'.krot-
|
|
249
|
+
'.krot-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:6px 14px;font-size:13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-weight:500;display:inline-flex;align-items:center;justify-content:center;gap:6px;transition:all .15s ease}',
|
|
250
|
+
'.krot-btn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-4,var(--dsw-alias-bg-layer-2));border-color:var(--dsw-alias-label-dimmed,var(--dsw-alias-border-l2))}',
|
|
251
|
+
'.krot-btn-primary,.krot-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent;font-weight:600}',
|
|
252
|
+
'.krot-btn-primary:hover:not(:disabled),.krot-save:hover:not(:disabled){background:var(--dsw-alias-label-primary)!important;color:var(--dsw-alias-bg-layer-3)!important;opacity:0.88}',
|
|
253
|
+
'.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:rgba(239,68,68,0.3)}',
|
|
254
|
+
'.krot-btn-danger:hover:not(:disabled){background:rgba(239,68,68,0.12)!important;border-color:rgba(239,68,68,0.5)}',
|
|
255
|
+
'.krot-btn:disabled{opacity:0.45;cursor:not-allowed}',
|
|
256
|
+
'.krot-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
257
|
+
'.krot-filter-bar{display:flex;gap:8px;margin:4px 0 10px;flex-wrap:wrap}',
|
|
258
|
+
'.krot-pill{appearance:none;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:4px 12px;font-size:12px;font-weight:500;color:var(--dsw-alias-label-secondary);cursor:pointer;transition:all .15s ease}',
|
|
369
259
|
'.krot-pill:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
|
|
370
|
-
'.krot-pill-active{background:var(--dsw-alias-
|
|
371
|
-
'.krot-pill-warn{border-color:rgba(245,158,11,0.3);color
|
|
372
|
-
'.krot-pill-err{border-color:rgba(239,68,68,0.3);color
|
|
260
|
+
'.krot-pill-active{background:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)!important;font-weight:600}',
|
|
261
|
+
'.krot-pill-warn{border-color:rgba(245,158,11,0.3);color:var(--dsw-alias-state-warning-primary)}',
|
|
262
|
+
'.krot-pill-err{border-color:rgba(239,68,68,0.3);color:var(--dsw-alias-state-error-primary)}',
|
|
263
|
+
'.krot-alert-ok{padding:10px 14px;border-radius:8px;background:rgba(16,185,129,0.1);color:var(--dsw-alias-state-success-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
264
|
+
'.krot-alert-bad{padding:10px 14px;border-radius:8px;background:rgba(239,68,68,0.1);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
373
265
|
'.krot-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}',
|
|
374
266
|
'.krot-card-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}',
|
|
375
267
|
'.krot-card-head-text{display:flex;flex-direction:column;flex:1;gap:4px;min-width:0}',
|
|
376
268
|
'.krot-card-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}',
|
|
377
269
|
'.krot-card-description{color:var(--dsw-alias-label-secondary);font-size:13px}',
|
|
378
270
|
'.krot-card-chevron{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s ease;display:flex;align-items:center}.krot-card-chevron-open{transform:rotate(180deg)}',
|
|
379
|
-
'.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding
|
|
271
|
+
'.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:16px 0 8px}',
|
|
272
|
+
'.krot-header-chip{display:inline-flex;align-items:center;gap:6px;height:26px;padding:0 10px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font-size:12px;font-weight:600;cursor:pointer;position:relative;user-select:none;transition:all .15s ease}',
|
|
273
|
+
'.krot-header-chip:hover{background:var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-3));border-color:var(--dsw-alias-border-l1);transform:translateY(-0.5px)}',
|
|
274
|
+
'.krot-popover{position:absolute;top:calc(100% + 6px);right:0;z-index:10000;min-width:220px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;padding:12px 14px;box-shadow:0 16px 40px rgba(0,0,0,.45),0 2px 8px rgba(0,0,0,.15);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:8px;text-align:left}',
|
|
275
|
+
'.krot-pop-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}',
|
|
276
|
+
'.krot-pop-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}',
|
|
277
|
+
'.krot-pop-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px}',
|
|
278
|
+
'.krot-pop-count{font-size:12px;font-weight:700;font-variant-numeric:tabular-nums;opacity:.9}',
|
|
380
279
|
].join('');
|
|
381
280
|
const CARD_CSS_ID = 'dsh-key-rotation/section.module.css';
|
|
382
|
-
if (typeof document !== 'undefined' && !document.
|
|
281
|
+
if (typeof document !== 'undefined' && !document.getElementById('dsh-key-rotation-full-css')) {
|
|
383
282
|
const tag = document.createElement('style');
|
|
384
|
-
tag.
|
|
385
|
-
tag.
|
|
283
|
+
tag.id = 'dsh-key-rotation-full-css';
|
|
284
|
+
tag.dataset.dshPlugin = NS;
|
|
386
285
|
tag.dataset.pluginCss = CARD_CSS_ID;
|
|
286
|
+
tag.setAttribute('data-plugin', NS);
|
|
287
|
+
tag.textContent = CARD_CSS;
|
|
387
288
|
document.head.appendChild(tag);
|
|
388
289
|
}
|
|
389
290
|
|
|
@@ -419,7 +320,17 @@ window.__ModuleLoader__.load({
|
|
|
419
320
|
const active = ctx.locale.getSnapshot().active;
|
|
420
321
|
if (typeof active === 'string' && active) return active;
|
|
421
322
|
}
|
|
422
|
-
|
|
323
|
+
// Match DSH core: first entry of navigator.languages, else en.
|
|
324
|
+
if (typeof navigator !== 'undefined') {
|
|
325
|
+
const langs = (navigator.languages && navigator.languages.length)
|
|
326
|
+
? Array.from(navigator.languages)
|
|
327
|
+
: (navigator.language ? [navigator.language] : []);
|
|
328
|
+
for (const lang of langs) {
|
|
329
|
+
const code = String(lang || '').slice(0, 2).toLowerCase();
|
|
330
|
+
if (code) return code;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return 'en';
|
|
423
334
|
}, [ctx])
|
|
424
335
|
);
|
|
425
336
|
}
|
|
@@ -428,9 +339,18 @@ window.__ModuleLoader__.load({
|
|
|
428
339
|
return (key) => (DICT && DICT[key]) || (fallbackKeys && fallbackKeys[key]) || key;
|
|
429
340
|
}
|
|
430
341
|
|
|
342
|
+
// Prefer the host slot translator (props.t + translation plugins); fall back to en source.
|
|
343
|
+
function resolveT(props) {
|
|
344
|
+
const coreT = props && typeof props.t === 'function' ? props.t : null;
|
|
345
|
+
if (!coreT) return makeT(en, en);
|
|
346
|
+
return (key) => {
|
|
347
|
+
const v = coreT(key);
|
|
348
|
+
return (v && v !== key) ? v : (en[key] || key);
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
431
352
|
function KeyRotationSection(props) {
|
|
432
|
-
const
|
|
433
|
-
const t = makeT(DICT, en);
|
|
353
|
+
const t = resolveT(props);
|
|
434
354
|
const settingsScope = React.useMemo(() => {
|
|
435
355
|
if (props.ctx && props.ctx.settingsScope) {
|
|
436
356
|
try {
|
|
@@ -848,10 +768,10 @@ window.__ModuleLoader__.load({
|
|
|
848
768
|
});
|
|
849
769
|
|
|
850
770
|
const filterBar = keys.length > 1 ? h('div', { className: 'krot-filter-bar' },
|
|
851
|
-
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'all' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('all') },
|
|
852
|
-
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'ready' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('ready') },
|
|
853
|
-
cooldownCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-warn' + (statusFilter === 'cooldown' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('cooldown') },
|
|
854
|
-
errorCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-err' + (statusFilter === 'error' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('error') },
|
|
771
|
+
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'all' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('all') }, t('filterAll') + ' (' + keys.length + ')'),
|
|
772
|
+
h('button', { type: 'button', className: 'krot-pill' + (statusFilter === 'ready' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('ready') }, t('filterReady') + ' (' + readyCount + ')'),
|
|
773
|
+
cooldownCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-warn' + (statusFilter === 'cooldown' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('cooldown') }, t('filterCooldown') + ' (' + cooldownCount + ')') : null,
|
|
774
|
+
errorCount > 0 ? h('button', { type: 'button', className: 'krot-pill krot-pill-err' + (statusFilter === 'error' ? ' krot-pill-active' : ''), onClick: () => setStatusFilter('error') }, t('filterErrors') + ' (' + errorCount + ')') : null,
|
|
855
775
|
) : null;
|
|
856
776
|
|
|
857
777
|
const keyRows = filteredIndices.map(({ key, kIndex }) => {
|
|
@@ -1047,82 +967,175 @@ window.__ModuleLoader__.load({
|
|
|
1047
967
|
? h('p', { className: 'krot-err' }, t('noProviders'))
|
|
1048
968
|
: null;
|
|
1049
969
|
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
970
|
+
const allConfiguredProviders = Array.isArray(val?.providers) ? val.providers : [];
|
|
971
|
+
const totalPoolsCount = allConfiguredProviders.length;
|
|
972
|
+
let totalKeysCount = 0;
|
|
973
|
+
let healthyKeysCount = 0;
|
|
974
|
+
for (const prov of allConfiguredProviders) {
|
|
975
|
+
const pKeys = Array.isArray(prov.keys) ? prov.keys : [];
|
|
976
|
+
totalKeysCount += pKeys.length;
|
|
977
|
+
for (const k of pKeys) {
|
|
978
|
+
const st = keyStatus(prov.provider, k);
|
|
979
|
+
const isDown = st && (st.text === t('brokenKey') || st.text === t('keyMissing') || (st.text && st.text.includes(t('keyCooling').slice(0, 4)))) && !optimisticReset[k];
|
|
980
|
+
if (!isDown) healthyKeysCount++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const statsSection = h('div', { className: 'krot-section-card' },
|
|
985
|
+
h('div', { className: 'krot-section-title' },
|
|
986
|
+
h('span', null, '📊 ' + t('sectionStats')),
|
|
987
|
+
h('span', { className: 'krot-badge ' + (totalPoolsCount > 0 && healthyKeysCount === totalKeysCount ? 'krot-badge-ok' : totalPoolsCount > 0 ? 'krot-badge-warn' : 'krot-badge-bad') },
|
|
988
|
+
totalPoolsCount > 0 ? (healthyKeysCount + '/' + totalKeysCount + ' ' + t('keyReady')) : t('notRegistered')
|
|
989
|
+
)
|
|
990
|
+
),
|
|
991
|
+
h('div', { className: 'krot-section-desc' }, t('sectionStatsDesc')),
|
|
992
|
+
h('div', { className: 'krot-grid-3' },
|
|
993
|
+
h('div', { className: 'krot-stat-box' },
|
|
994
|
+
h('span', { className: 'krot-stat-val' }, String(totalPoolsCount)),
|
|
995
|
+
h('span', { className: 'krot-stat-lbl' }, t('statPools'))
|
|
996
|
+
),
|
|
997
|
+
h('div', { className: 'krot-stat-box' },
|
|
998
|
+
h('span', { className: 'krot-stat-val' }, String(totalKeysCount)),
|
|
999
|
+
h('span', { className: 'krot-stat-lbl' }, t('statKeys'))
|
|
1000
|
+
),
|
|
1001
|
+
h('div', { className: 'krot-stat-box' },
|
|
1002
|
+
h('span', { className: 'krot-stat-val', style: { color: healthyKeysCount > 0 ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-label-primary)' } }, String(healthyKeysCount)),
|
|
1003
|
+
h('span', { className: 'krot-stat-lbl' }, t('statHealthy'))
|
|
1004
|
+
)
|
|
1005
|
+
)
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
const poolsSection = h('div', { className: 'krot-section-card' },
|
|
1009
|
+
h('div', { className: 'krot-section-title' },
|
|
1010
|
+
h('span', null, '🔑 ' + t('providersTitle')),
|
|
1011
|
+
h('span', { className: 'krot-badge krot-badge-ok' }, totalPoolsCount + ' ' + t('statPools'))
|
|
1012
|
+
),
|
|
1013
|
+
h('div', { className: 'krot-section-desc' }, t('desc')),
|
|
1014
|
+
searchInput,
|
|
1015
|
+
h('div', { className: 'krot-foot', style: { marginBottom: '8px' } },
|
|
1055
1016
|
h('input', {
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1017
|
+
className: 'krot-in',
|
|
1018
|
+
placeholder: 'Bulk cooldown ms',
|
|
1019
|
+
value: bulkCooldown,
|
|
1020
|
+
onChange: (e) => setBulkCooldown(e.target.value),
|
|
1021
|
+
style: { maxWidth: '160px' },
|
|
1059
1022
|
}),
|
|
1060
|
-
|
|
1061
|
-
)))),
|
|
1062
|
-
field(t('providersTitle'), h('div', { className: 'krot-keys' },
|
|
1063
|
-
searchInput,
|
|
1064
|
-
h('div', { className: 'krot-foot' }, h('input', { className: 'krot-in', placeholder: 'Bulk cooldown ms', value: bulkCooldown, onChange: (e) => setBulkCooldown(e.target.value), style: { maxWidth: '140px' } }), btn('Apply to selected', () => {
|
|
1023
|
+
btn('Apply to selected', () => {
|
|
1065
1024
|
const v = Number(bulkCooldown); if (!v) return;
|
|
1066
1025
|
setField((cur) => {
|
|
1067
1026
|
const next = [...(cur.providers ?? [])];
|
|
1068
|
-
for (let i=0;i<next.length;i++) if (selected.has(next[i].provider)) next[i] = { ...next[i], cooldownMs: v };
|
|
1027
|
+
for (let i = 0; i < next.length; i++) if (selected.has(next[i].provider)) next[i] = { ...next[i], cooldownMs: v };
|
|
1069
1028
|
return { ...cur, providers: next };
|
|
1070
1029
|
});
|
|
1071
|
-
}, { disabled: selected.size === 0 || !bulkCooldown })
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1030
|
+
}, { disabled: selected.size === 0 || !bulkCooldown })
|
|
1031
|
+
),
|
|
1032
|
+
h('div', { className: 'krot-keys' }, providerRows),
|
|
1033
|
+
h('div', { className: 'krot-foot', style: { marginTop: '10px' } },
|
|
1034
|
+
btn(t('addProvider'), addProvider, { primary: false }),
|
|
1035
|
+
noProviders
|
|
1036
|
+
)
|
|
1037
|
+
);
|
|
1038
|
+
|
|
1039
|
+
const failoverSection = h('div', { className: 'krot-section-card' },
|
|
1040
|
+
h('div', { className: 'krot-section-title' },
|
|
1041
|
+
h('span', null, '⚡ ' + t('codesTitle')),
|
|
1042
|
+
h('span', { className: 'krot-badge krot-badge-warn' }, selectedCodes.size + ' active')
|
|
1043
|
+
),
|
|
1044
|
+
h('div', { className: 'krot-section-desc' }, t('sectionFailoverDesc')),
|
|
1045
|
+
h('div', { className: 'krot-codes' }, codeList.map((code) => h('label', { key: code, className: 'krot-code' },
|
|
1046
|
+
h('input', {
|
|
1047
|
+
type: 'checkbox',
|
|
1048
|
+
checked: selectedCodes.has(code),
|
|
1049
|
+
onChange: (e) => toggleCode(code, e.target.checked),
|
|
1050
|
+
}),
|
|
1051
|
+
code,
|
|
1052
|
+
)))
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
const timingSection = h('div', { className: 'krot-section-card' },
|
|
1056
|
+
h('div', { className: 'krot-section-title' },
|
|
1057
|
+
h('span', null, '⏱ ' + t('sectionTiming')),
|
|
1058
|
+
),
|
|
1059
|
+
h('div', { className: 'krot-section-desc' }, t('sectionTimingDesc')),
|
|
1060
|
+
h('div', { className: 'krot-grid-2' },
|
|
1061
|
+
field(t('cooldown'), textInput(String(val.cooldownMs ?? 60000), (v) => setField((cur) => ({ ...cur, cooldownMs: Number(v) || 0 })))),
|
|
1062
|
+
field(t('scheduleDays'), textInput(String(val.rotationScheduleDays ?? 0), (v) => setField((cur) => ({ ...cur, rotationScheduleDays: Number(v) || 0 }))))
|
|
1063
|
+
)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
const backupSection = h('div', { className: 'krot-section-card' },
|
|
1067
|
+
h('div', { className: 'krot-section-title' },
|
|
1068
|
+
h('span', null, '💾 ' + t('sectionBackup')),
|
|
1069
|
+
),
|
|
1070
|
+
h('div', { className: 'krot-section-desc' }, t('sectionBackupDesc')),
|
|
1071
|
+
h('div', { className: 'krot-row' },
|
|
1072
|
+
btn(t('exportPools'), () => {
|
|
1073
|
+
const data = JSON.stringify(val.providers ?? [], null, 2);
|
|
1074
|
+
const blob = new Blob([data], { type: 'application/json' });
|
|
1075
|
+
const url = URL.createObjectURL(blob);
|
|
1076
|
+
const a = document.createElement('a'); a.href = url; a.download = 'pools.json'; a.click(); URL.revokeObjectURL(url);
|
|
1077
|
+
}, {}),
|
|
1078
|
+
h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('importPools'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
|
|
1079
|
+
const f = e.target.files[0]; if (!f) return;
|
|
1080
|
+
const reader = new FileReader();
|
|
1081
|
+
reader.onload = () => { try { const imp = JSON.parse(String(reader.result)); if (!Array.isArray(imp)) throw new Error('expected array'); setField((cur) => {
|
|
1082
|
+
const curProviders = Array.isArray(cur.providers) ? [...cur.providers] : [];
|
|
1083
|
+
const map = new Map(curProviders.map((p) => [p.provider, p]));
|
|
1084
|
+
for (const p of imp) { if (p && typeof p.provider === 'string') map.set(p.provider, p); }
|
|
1085
|
+
return { ...cur, providers: [...map.values()] };
|
|
1086
|
+
}); } catch (err) { setSecretError(String(err.message || err)); } };
|
|
1087
|
+
reader.readAsText(f);
|
|
1088
|
+
} })),
|
|
1089
|
+
btn(t('snapshotExport'), () => {
|
|
1090
|
+
fetch('/dsh-key-rotation/snapshot', { headers: { accept: 'application/json' } })
|
|
1091
|
+
.then((r) => r.json())
|
|
1092
|
+
.then((data) => {
|
|
1093
|
+
const blob = new Blob([JSON.stringify(data.snapshot ?? {}, null, 2)], { type: 'application/json' });
|
|
1094
|
+
const url = URL.createObjectURL(blob);
|
|
1095
|
+
const a = document.createElement('a'); a.href = url; a.download = 'dsh-key-rotation-snapshot.json'; a.click(); URL.revokeObjectURL(url);
|
|
1096
|
+
})
|
|
1097
|
+
.catch((e) => setSecretError(String(e?.message ?? e)));
|
|
1098
|
+
}, {}),
|
|
1099
|
+
h('label', { className: 'krot-btn', style: { cursor: 'pointer' } }, t('snapshotImport'), h('input', { type: 'file', accept: '.json', style: { display: 'none' }, onChange: (e) => {
|
|
1100
|
+
const f = e.target.files[0]; if (!f) return;
|
|
1101
|
+
const reader2 = new FileReader();
|
|
1102
|
+
reader2.onload = () => {
|
|
1103
|
+
try {
|
|
1104
|
+
const snap = JSON.parse(String(reader2.result));
|
|
1105
|
+
if (!snap || typeof snap !== 'object' || Array.isArray(snap)) throw new Error('expected snapshot object');
|
|
1106
|
+
fetch('/dsh-key-rotation/snapshot', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ snapshot: snap }) })
|
|
1107
|
+
.then((r) => r.json().then((data) => ({ ok: r.ok, data })))
|
|
1108
|
+
.then(({ ok, data }) => { if (!ok) throw new Error(data?.error?.message ?? 'import failed'); load(); })
|
|
1109
|
+
.catch((err) => setSecretError(t('keyWriteFailed').replace('{msg}', String(err?.message ?? err))));
|
|
1110
|
+
} catch (err) { setSecretError(String(err.message || err)); }
|
|
1111
|
+
};
|
|
1112
|
+
reader2.readAsText(f);
|
|
1113
|
+
} }))
|
|
1114
|
+
),
|
|
1115
|
+
h('p', { className: 'krot-hint', style: { marginTop: '4px' } }, t('keyHint'))
|
|
1116
|
+
);
|
|
1117
|
+
|
|
1118
|
+
const alerts = [
|
|
1119
|
+
secretError ? h('div', { key: 'se', className: 'krot-alert-bad' }, secretError) : null,
|
|
1120
|
+
state.error ? h('div', { key: 'st', className: 'krot-alert-bad' }, state.error) : null,
|
|
1121
|
+
undo ? h('div', { key: 'un', className: 'krot-alert-ok' },
|
|
1122
|
+
h('span', null, undo.type === 'provider' ? t('undoProvider') : t('undoKey')),
|
|
1123
|
+
btn(t('undo'), doUndo, { className: 'krot-btn-sm' })
|
|
1124
|
+
) : null,
|
|
1125
|
+
].filter(Boolean);
|
|
1126
|
+
|
|
1127
|
+
return h('div', { className: 'krot' },
|
|
1128
|
+
statsSection,
|
|
1129
|
+
poolsSection,
|
|
1130
|
+
failoverSection,
|
|
1131
|
+
timingSection,
|
|
1132
|
+
backupSection,
|
|
1133
|
+
alerts.length > 0 ? h('div', { style: { display: 'flex', flexDirection: 'column', gap: '8px' } }, alerts) : null,
|
|
1134
|
+
h('div', { className: 'krot-foot', style: { marginTop: '6px', paddingTop: '14px', borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1122
1135
|
btn(t('save'), save, { primary: true }),
|
|
1123
1136
|
btn(t('discard'), load, {}),
|
|
1124
1137
|
state.status === 'saving' ? h('span', { className: 'krot-hint' }, t('saving')) : null,
|
|
1125
|
-
)
|
|
1138
|
+
)
|
|
1126
1139
|
);
|
|
1127
1140
|
}
|
|
1128
1141
|
|
|
@@ -1170,14 +1183,25 @@ window.__ModuleLoader__.load({
|
|
|
1170
1183
|
const total = poolEntries.reduce((a, [, p]) => a + (p.total || 0), 0);
|
|
1171
1184
|
const healthy = poolEntries.reduce((a, [, p]) => a + (p.healthy || 0), 0);
|
|
1172
1185
|
const anyExhausted = poolEntries.some(([, p]) => p.exhausted);
|
|
1173
|
-
const color = !poolEntries.length ? 'var(--dsw-alias-label-tertiary)' : anyExhausted ? '
|
|
1186
|
+
const color = !poolEntries.length ? 'var(--dsw-alias-label-tertiary)' : anyExhausted ? 'var(--dsw-alias-state-error-primary)' : healthy < total ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)';
|
|
1174
1187
|
const label = poolEntries.length ? `${healthy}/${total} rot` : 'rot';
|
|
1175
1188
|
|
|
1176
1189
|
return h('div', { ref, style: { position: 'relative', display: 'inline-flex' } },
|
|
1177
1190
|
h('button', {
|
|
1178
1191
|
type: 'button',
|
|
1179
1192
|
className: 'krot-header-chip',
|
|
1180
|
-
title: '
|
|
1193
|
+
title: 'Key Rotation',
|
|
1194
|
+
sectionStats: 'Pool Health & Telemetry',
|
|
1195
|
+
sectionStatsDesc: 'Real-time telemetry of active key pools, healthy credentials, and failover status.',
|
|
1196
|
+
sectionFailover: 'Failover Triggers & Error Codes',
|
|
1197
|
+
sectionFailoverDesc: 'Select error conditions that trigger automatic switching to the next available key.',
|
|
1198
|
+
sectionTiming: 'Timing & Rotation Schedule',
|
|
1199
|
+
sectionTimingDesc: 'Configure cooldown durations and scheduled periodic rotation windows.',
|
|
1200
|
+
sectionBackup: 'Backup & Snapshots',
|
|
1201
|
+
sectionBackupDesc: 'Export or import provider pools and create full plugin configuration snapshots.',
|
|
1202
|
+
statPools: 'Configured Pools',
|
|
1203
|
+
statKeys: 'Total Keys',
|
|
1204
|
+
statHealthy: 'Healthy / Ready',
|
|
1181
1205
|
onClick: () => setOpen((v) => !v),
|
|
1182
1206
|
},
|
|
1183
1207
|
h('span', { style: { width: '8px', height: '8px', borderRadius: '50%', background: color, flex: 'none', boxShadow: `0 0 6px ${color}` } }),
|
|
@@ -1185,10 +1209,10 @@ window.__ModuleLoader__.load({
|
|
|
1185
1209
|
h('span', { style: { fontSize: '9px', opacity: 0.6 } }, open ? '▲' : '▼')
|
|
1186
1210
|
),
|
|
1187
1211
|
open ? h('div', { className: 'krot-popover' },
|
|
1188
|
-
h('div', { className: 'krot-pop-title' }, '
|
|
1189
|
-
!poolEntries.length ? h('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, '
|
|
1212
|
+
h('div', { className: 'krot-pop-title' }, 'Key rotation pools'),
|
|
1213
|
+
!poolEntries.length ? h('div', { style: { fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' } }, 'No active pools') : null,
|
|
1190
1214
|
poolEntries.map(([name, p]) => {
|
|
1191
|
-
const c = p.exhausted ? '
|
|
1215
|
+
const c = p.exhausted ? 'var(--dsw-alias-state-error-primary)' : (p.healthy < p.total ? 'var(--dsw-alias-state-warning-primary)' : 'var(--dsw-alias-state-success-primary)');
|
|
1192
1216
|
return h('div', { key: name, className: 'krot-pop-row' },
|
|
1193
1217
|
h('div', { className: 'krot-pop-name' },
|
|
1194
1218
|
h('span', { style: { width: '7px', height: '7px', borderRadius: '50%', background: c, flex: 'none' } }),
|
|
@@ -1202,7 +1226,7 @@ window.__ModuleLoader__.load({
|
|
|
1202
1226
|
}
|
|
1203
1227
|
|
|
1204
1228
|
function apply(ctx) {
|
|
1205
|
-
ctx.effect(() => ctx.locale.register(NS, { en
|
|
1229
|
+
ctx.effect(() => ctx.locale.register(NS, { en }), 'dsh-key-rotation: dictionaries');
|
|
1206
1230
|
// Header chip (#201): status dot in the session header utilities slot,
|
|
1207
1231
|
// same slot dsh-gitea / dsh-subscriptions use for their header widgets.
|
|
1208
1232
|
ctx.effect(() => {
|
|
@@ -1224,7 +1248,7 @@ window.__ModuleLoader__.load({
|
|
|
1224
1248
|
// key MUST equal the settings namespace (NS), else the tab silently skips it.
|
|
1225
1249
|
function KeyRotationCard(props) {
|
|
1226
1250
|
const locale = useLocale();
|
|
1227
|
-
const t =
|
|
1251
|
+
const t = resolveT(props);
|
|
1228
1252
|
const [open, setOpen] = React.useState(false);
|
|
1229
1253
|
return h('div', { className: 'krot-card' + (open ? ' krot-card-open' : '') },
|
|
1230
1254
|
h('button', { type: 'button', className: 'krot-card-header', 'aria-expanded': open, onClick: () => setOpen((v) => !v) },
|
|
@@ -1236,30 +1260,16 @@ window.__ModuleLoader__.load({
|
|
|
1236
1260
|
h('path', { d: 'M3.5 5.25L7 8.75L10.5 5.25' })))),
|
|
1237
1261
|
open ? h('div', { className: 'krot-card-body' }, h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale }))) : null);
|
|
1238
1262
|
}
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
if (!tryPluginItem()) {
|
|
1250
|
-
// Fallback for builds without the Plugins tab slot: keep the sidebar section.
|
|
1251
|
-
const t = ctx.locale ? ctx.locale.bind(NS) : null;
|
|
1252
|
-
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1253
|
-
{
|
|
1254
|
-
name: 'settings.section',
|
|
1255
|
-
id: 'dsh-key-rotation',
|
|
1256
|
-
order: 20,
|
|
1257
|
-
locale: NS,
|
|
1258
|
-
label: () => (t ? t('title') : 'Key Rotation') || 'Key Rotation',
|
|
1259
|
-
inject: () => ({ ctx }),
|
|
1260
|
-
},
|
|
1261
|
-
(props) => h(KeyRotationErrorBoundary, null, h(KeyRotationSection, { ...props, locale: useLocale() })),
|
|
1262
|
-
));
|
|
1263
|
+
// #275: card only — no top-level sidebar row. The host sidebar is a
|
|
1264
|
+
// shared flat list; a duplicate row wastes a slot and confuses Settings.
|
|
1265
|
+
try {
|
|
1266
|
+
ctx.slots.inject('settings.plugin.item', () =>
|
|
1267
|
+
ctx.slots.register(
|
|
1268
|
+
{ name: 'settings.plugin.item', key: NS, locale: NS, inject: () => ({ ctx }) },
|
|
1269
|
+
KeyRotationCard,
|
|
1270
|
+
));
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
try { console.error('[dsh-key-rotation] settings.plugin.item register failed', e); } catch (_) {}
|
|
1263
1273
|
}
|
|
1264
1274
|
}
|
|
1265
1275
|
|
package/lib/http-bridge.js
CHANGED
|
@@ -69,7 +69,8 @@ export async function writeSection(ctx, ns, section, expectedRevision, res) {
|
|
|
69
69
|
export function providerCatalog(ctx, cloneIds) {
|
|
70
70
|
const seen = new Set();
|
|
71
71
|
const out = [];
|
|
72
|
-
|
|
72
|
+
const llm = ctx.get('llm');
|
|
73
|
+
for (const info of (llm?.listProviders?.() ?? [])) {
|
|
73
74
|
if (seen.has(info.id) || cloneIds.has(info.id)) continue;
|
|
74
75
|
seen.add(info.id);
|
|
75
76
|
out.push({ id: info.id, name: info.name ?? info.id });
|
package/lib/index.js
CHANGED
|
@@ -794,7 +794,12 @@ export function apply(ctx, config = {}) {
|
|
|
794
794
|
}), 'dsh-key-rotation: agent/request-error');
|
|
795
795
|
|
|
796
796
|
ctx.inject(['settings'], (sctx) => {
|
|
797
|
-
const
|
|
797
|
+
const settingsSvc = sctx.get('settings');
|
|
798
|
+
if (!settingsSvc || typeof settingsSvc.register !== 'function') {
|
|
799
|
+
console.warn('[dsh-key-rotation] settings service unavailable — config scope not registered');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const scope = settingsSvc.register(NS, Config, { base: config });
|
|
798
803
|
getConfig = () => scope.get() ?? config;
|
|
799
804
|
sctx.effect(() => () => {
|
|
800
805
|
getConfig = () => config;
|
package/lib/rotate.js
CHANGED
|
@@ -98,7 +98,13 @@ export function createRotate(deps) {
|
|
|
98
98
|
let inner;
|
|
99
99
|
try {
|
|
100
100
|
// mark the internal dispatch so the interceptor does not re-rotate
|
|
101
|
-
inner = dispatchStorage.run(reqStore, () =>
|
|
101
|
+
inner = dispatchStorage.run(reqStore, () => {
|
|
102
|
+
const llm = ctx.get('llm');
|
|
103
|
+
if (!llm || typeof llm.stream !== 'function') {
|
|
104
|
+
throw new Error('dsh-key-rotation: llm service unavailable');
|
|
105
|
+
}
|
|
106
|
+
return llm.stream({ ...options, [MARKER]: true });
|
|
107
|
+
});
|
|
102
108
|
} catch (e) {
|
|
103
109
|
const curRef = reqStore.pickedRef ?? pool.state.lastUsed;
|
|
104
110
|
penalizeRef(curRef, e?.code ?? 'TRANSPORT', String(e?.message ?? ''));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-key-rotation",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"packageManager": "pnpm@10.33.2",
|
|
5
5
|
"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.",
|
|
6
6
|
"keywords": [
|