@goodandready/dsh-key-rotation 0.8.8 → 0.8.11
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 +40 -3
- package/README.ru.md +40 -3
- package/README.zh.md +29 -4
- package/lib/best-effort.js +26 -0
- package/lib/bucket.js +1 -87
- package/lib/cascade.js +27 -39
- package/lib/client.js +360 -55
- package/lib/clock.js +0 -2
- package/lib/concurrency.js +9 -0
- package/lib/error-taxonomy.js +0 -3
- package/lib/heal.js +47 -0
- package/lib/http-bridge.js +0 -7
- package/lib/index.js +65 -10
- package/lib/plugin-updater.js +277 -0
- package/lib/pool.js +109 -29
- package/lib/rotate.js +127 -107
- package/lib/routes-ops.js +5 -2
- package/lib/sandbox.js +9 -1
- package/lib/webhook.js +2 -1
- package/package.json +5 -2
package/lib/client.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
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: source strings are English
|
|
12
|
+
// Localization: source strings are English; Chinese is a first-class locale (en + zh).
|
|
13
13
|
// Other languages come from the DSH core locale service / translation plugins
|
|
14
14
|
// via props.t (slot locale: NS). Active locale prefers ctx.locale.getSnapshot().active,
|
|
15
15
|
// else first navigator.languages entry, else 'en' — same fallback chain as DSH core.
|
|
@@ -143,16 +143,176 @@ window.__ModuleLoader__.load({
|
|
|
143
143
|
recentFailures: 'Recent failures ({n})',
|
|
144
144
|
noActivePools: 'No active pools',
|
|
145
145
|
headerPoolsTitle: 'Key rotation pools',
|
|
146
|
-
|
|
146
|
+
pausedBudget: 'paused',
|
|
147
147
|
sortUsage: 'Sort by usage',
|
|
148
|
+
testingAllProgress: 'Testing {n}/{total}…',
|
|
149
|
+
liveEventStream: 'Live Event Stream',
|
|
150
|
+
liveEventStreamEmpty: 'No rotation events recorded',
|
|
151
|
+
eventAt: 'Time',
|
|
152
|
+
eventKey: 'Key',
|
|
153
|
+
eventReason: 'Reason',
|
|
154
|
+
eventCooldown: 'Cooldown',
|
|
155
|
+
quotaResetIn: 'Reset in {time}',
|
|
156
|
+
routingStrategyLabel: 'Routing Strategy',
|
|
157
|
+
routingStrategyRoundRobin: 'Round-Robin (default)',
|
|
158
|
+
routingStrategyLeastLoaded: 'Least-Loaded (concurrency)',
|
|
159
|
+
routingStrategyLowestLatency: 'Lowest-Latency (p95)',
|
|
160
|
+
proactiveGuardLabel: 'Proactive Rate-Limit Guard',
|
|
161
|
+
proactiveGuardHint: 'Pre-emptively pause keys when remaining quota or Retry-After header indicates limit before hitting 429',
|
|
162
|
+
selfHealingLabel: 'Auto-Unbreak Interval (min)',
|
|
163
|
+
selfHealingHint: 'Periodic free model check to revive broken keys (0 to disable)',
|
|
148
164
|
};
|
|
149
165
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
166
|
+
|
|
167
|
+
const zh = {
|
|
168
|
+
title: '密钥轮换',
|
|
169
|
+
'header.title': '密钥轮换与故障转移',
|
|
170
|
+
'header.sub': '自动化多密钥轮换、429/5xx指数退避冷却、断路器保护及凭证安全 (~/.dsh/.credentials.yaml)。',
|
|
171
|
+
'header.pools_badge': '个活跃密钥池',
|
|
172
|
+
'header.keys_badge': '个已配置密钥',
|
|
173
|
+
sectionStats: '密钥池健康与遥测',
|
|
174
|
+
sectionStatsDesc: '活跃密钥池、健康凭证和故障转移状态的实时遥测。',
|
|
175
|
+
sectionFailover: '故障转移触发器与错误代码',
|
|
176
|
+
sectionFailoverDesc: '选择触发自动切换至下一个可用密钥的错误条件。',
|
|
177
|
+
sectionTiming: '时间与轮换计划',
|
|
178
|
+
sectionTimingDesc: '配置冷却持续时间和定期轮换计划窗口。',
|
|
179
|
+
sectionBackup: '备份与快照',
|
|
180
|
+
sectionBackupDesc: '导出或导入提供商密钥池,并创建完整插件配置快照。',
|
|
181
|
+
statPools: '已配置密钥池',
|
|
182
|
+
statKeys: '总密钥数',
|
|
183
|
+
statHealthy: '健康 / 就绪',
|
|
184
|
+
loadDistribution: '流量分布',
|
|
185
|
+
statRequests: '请求',
|
|
186
|
+
noTrafficYet: '暂无流量记录',
|
|
187
|
+
confirm: '确认',
|
|
188
|
+
cancel: '取消',
|
|
189
|
+
confirmResetTitle: '重置 {p} 的冷却状态?',
|
|
190
|
+
confirmResetDesc: '这将立即清除所有故障计数、冷却计时器并重置断路器。',
|
|
191
|
+
confirmRemoveProvTitle: '移除提供商 {p}?',
|
|
192
|
+
confirmRemoveProvDesc: '确定要从轮换中移除该提供商及其所有已配置密钥吗?',
|
|
193
|
+
filterAll: '全部',
|
|
194
|
+
filterReady: '就绪',
|
|
195
|
+
filterCooldown: '冷却中',
|
|
196
|
+
filterErrors: '存在错误',
|
|
197
|
+
subtitle: '按提供商 API 密钥轮换:密钥池、配额/限流错误自动故障转移、冷却与恢复。',
|
|
198
|
+
cardDesc: '按提供商 API 密钥轮换:密钥池、配额/限流错误自动故障转移、冷却与恢复。',
|
|
199
|
+
loading: '加载中…',
|
|
200
|
+
notRegistered: '未注册',
|
|
201
|
+
removeKey: '移除密钥',
|
|
202
|
+
removeProvider: '移除提供商',
|
|
203
|
+
addKey: '+ 添加密钥',
|
|
204
|
+
addKeyTitle: '添加 API 密钥',
|
|
205
|
+
noProviders: 'DSH 尚未注册任何提供商 — 暂无可选项目。',
|
|
206
|
+
addProvider: '+ 添加提供商',
|
|
207
|
+
desc: '按提供商的 API 密钥轮换。为每个提供商列出其 API 密钥(环境变量名,存储在 DSH 凭据中)。插件按顺序通过提供商的密钥路由模型,并在配额/速率限制失败时切换到下一个。',
|
|
208
|
+
cooldown: '故障后冷却时间 (毫秒)',
|
|
209
|
+
scheduleDays: '轮换计划 (天数,0=关闭)',
|
|
210
|
+
switchCodes: '切换代码 (逗号分隔)',
|
|
211
|
+
providersTitle: '提供商与其密钥',
|
|
212
|
+
save: '保存',
|
|
213
|
+
discard: '放弃',
|
|
214
|
+
saving: '保存中…',
|
|
215
|
+
moveUp: '上移',
|
|
216
|
+
moveDown: '下移',
|
|
217
|
+
keyActive: '使用中',
|
|
218
|
+
keyReady: '就绪',
|
|
219
|
+
keyCooling: '冷却中,{s}秒',
|
|
220
|
+
keyMissing: '凭证不存在',
|
|
221
|
+
switchesNone: '暂无切换',
|
|
222
|
+
switchesSome: '切换次数: {n} · 上次: {reason}, {ago}',
|
|
223
|
+
justNow: '刚刚',
|
|
224
|
+
minutesAgo: '{n} 分钟前',
|
|
225
|
+
hoursAgo: '{n} 小时前',
|
|
226
|
+
codesTitle: '在这些故障时切换',
|
|
227
|
+
keyValuePlaceholder: '粘贴密钥,然后保存',
|
|
228
|
+
keySave: '保存密钥',
|
|
229
|
+
keySaved: '已保存',
|
|
230
|
+
keyFromEnv: '来自环境变量,此处只读',
|
|
231
|
+
keyWriteFailed: '无法存储密钥: {msg}',
|
|
232
|
+
notSecretShape: '已保存,但该值看起来不像 API 密钥 - 请检查是否有拼写错误',
|
|
233
|
+
rpmTitle: '请求/分钟: 已用 {u}, 剩余 {r}',
|
|
234
|
+
budgetLabel: '预算:',
|
|
235
|
+
exportCsv: '导出 CSV',
|
|
236
|
+
weightHint: '轮换权重:该密钥参与循环的次数 (1 = 等分)',
|
|
237
|
+
retestBroken: '重新测试',
|
|
238
|
+
retestFail: '重新测试失败 - 密钥仍然不可用',
|
|
239
|
+
snapshotExport: '快照 ⬇',
|
|
240
|
+
snapshotImport: '恢复',
|
|
241
|
+
keyHint: '该值存储在 DSH 凭据中,绝不会发送回浏览器 — 仅显示其最后 5 个字符。名称已自动生成;悬停在密钥上可查看其使用的名称。',
|
|
242
|
+
brokenKey: '已损坏 (3× 认证失败)',
|
|
243
|
+
keyExpired: '已过期',
|
|
244
|
+
keyExpiringSoon: '{n} 天后过期',
|
|
245
|
+
exportPools: '导出',
|
|
246
|
+
exportOne: '⬇',
|
|
247
|
+
usedAgo: '{ago}前使用',
|
|
248
|
+
importPools: '导入',
|
|
249
|
+
importEnv: '导入 .env',
|
|
250
|
+
resetCooldown: '重置冷却',
|
|
251
|
+
testAll: '测试所有密钥',
|
|
252
|
+
testing: '测试中…',
|
|
253
|
+
testKey: '测试',
|
|
254
|
+
testOk: '正常',
|
|
255
|
+
testFail: '失败',
|
|
256
|
+
poolExhausted: '密钥池耗尽 — 所有密钥冷却中',
|
|
257
|
+
resetting: '重置中…',
|
|
258
|
+
keyLabel: '密钥 {n}',
|
|
259
|
+
undoProvider: '提供商已移除',
|
|
260
|
+
undoKey: '密钥已移除',
|
|
261
|
+
undo: '撤销',
|
|
262
|
+
emptyTitle: '暂无密钥池',
|
|
263
|
+
emptyDesc: '添加提供商及至少一个 API 密钥名称以开始轮换。',
|
|
264
|
+
errorTitle: '无法加载设置',
|
|
265
|
+
retry: '重试',
|
|
266
|
+
unavailableTitle: '设置桥接不可用',
|
|
267
|
+
unavailableDesc: '宿主未暴露 dsh-key-rotation 设置命名空间。请重新打开设置或检查插件是否已启用。',
|
|
268
|
+
modalClose: '关闭对话框',
|
|
269
|
+
dialogLabel: '确认',
|
|
270
|
+
loadChartAria: '密钥流量分布',
|
|
271
|
+
bulkCooldownPlaceholder: '批量冷却时间 (毫秒)',
|
|
272
|
+
bulkApply: '应用到所选项',
|
|
273
|
+
bulkRemove: '删除所选项',
|
|
274
|
+
confirmBulkRemoveTitle: '删除 {n} 个提供商?',
|
|
275
|
+
confirmBulkRemoveDesc: '所选提供商及其所有密钥将从轮换中移除。操作后可立即撤销一次。',
|
|
276
|
+
selectProvider: '选择提供商 {p}',
|
|
277
|
+
activeBadge: '活跃',
|
|
278
|
+
healthScoreTitle: '健康评分',
|
|
279
|
+
totalRequestsTitle: '总请求数',
|
|
280
|
+
exportProvider: '导出此提供商',
|
|
281
|
+
recentFailures: '近期故障 ({n})',
|
|
282
|
+
noActivePools: '无活跃密钥池',
|
|
283
|
+
headerPoolsTitle: '密钥轮换池',
|
|
284
|
+
pausedBudget: '已暂停',
|
|
285
|
+
sortUsage: '按使用量排序',
|
|
286
|
+
testingAllProgress: '正在测试 {n}/{total}…',
|
|
287
|
+
liveEventStream: '实时事件流',
|
|
288
|
+
liveEventStreamEmpty: '暂无轮换事件记录',
|
|
289
|
+
eventAt: '时间',
|
|
290
|
+
eventKey: '密钥',
|
|
291
|
+
eventReason: '原因',
|
|
292
|
+
eventCooldown: '冷却',
|
|
293
|
+
quotaResetIn: '{time} 后重置',
|
|
294
|
+
routingStrategyLabel: '路由策略',
|
|
295
|
+
routingStrategyRoundRobin: '轮询 (默认)',
|
|
296
|
+
routingStrategyLeastLoaded: '最低负载 (并发)',
|
|
297
|
+
routingStrategyLowestLatency: '最低延迟 (p95)',
|
|
298
|
+
proactiveGuardLabel: '前瞻性限流防护',
|
|
299
|
+
proactiveGuardHint: '当响应中的剩余配额或 Retry-After 头提示即将超限时,预先冷却密钥以防触发 429',
|
|
300
|
+
selfHealingLabel: '自愈检查间隔 (分钟)',
|
|
301
|
+
selfHealingHint: '定期轻量探测以自动恢复损坏密钥 (0 为禁用)',
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
// Codes worth switching on. Host list is DEFAULT_SWITCH_CODES; config may
|
|
305
|
+
// add extras — those render as separate checked boxes so a custom rule is
|
|
306
|
+
// not dropped on first save.
|
|
153
307
|
const KNOWN_CODES = ['QUOTA', 'RATE_LIMIT', 'SERVER', 'TIMEOUT', 'TRANSPORT', 'EMPTY_RESPONSE', 'UNKNOWN_MODEL', 'AUTH'];
|
|
154
308
|
|
|
155
309
|
// #273: keep the settings list item mounted if the section throws.
|
|
310
|
+
const bestEffort = (label, fn) => {
|
|
311
|
+
try { return fn(); } catch (err) {
|
|
312
|
+
try { console.debug('[dsh-key-rotation] best-effort ' + label, err); } catch { /* console unavailable */ }
|
|
313
|
+
return undefined;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
156
316
|
class KeyRotationErrorBoundary extends React.Component {
|
|
157
317
|
constructor(props) {
|
|
158
318
|
super(props);
|
|
@@ -162,7 +322,7 @@ window.__ModuleLoader__.load({
|
|
|
162
322
|
return { error };
|
|
163
323
|
}
|
|
164
324
|
componentDidCatch(error, info) {
|
|
165
|
-
|
|
325
|
+
bestEffort('error-boundary.log', () => { console.error('[dsh-key-rotation] settings card crashed', error, info); });
|
|
166
326
|
}
|
|
167
327
|
render() {
|
|
168
328
|
if (this.state.error) {
|
|
@@ -179,7 +339,7 @@ window.__ModuleLoader__.load({
|
|
|
179
339
|
}
|
|
180
340
|
}
|
|
181
341
|
|
|
182
|
-
/**
|
|
342
|
+
/** Poll rotation status while the settings section is open (smart polling). */
|
|
183
343
|
function useRotationStatus() {
|
|
184
344
|
const [byProvider, setByProvider] = React.useState({});
|
|
185
345
|
React.useEffect(() => {
|
|
@@ -194,7 +354,7 @@ window.__ModuleLoader__.load({
|
|
|
194
354
|
for (const entry of data.providers) map[entry.provider] = entry;
|
|
195
355
|
setByProvider(map);
|
|
196
356
|
})
|
|
197
|
-
.catch(() => { /*
|
|
357
|
+
.catch(() => { /* status is optional: card stays a working editor */ });
|
|
198
358
|
};
|
|
199
359
|
pull();
|
|
200
360
|
const id = setInterval(pull, 4000);
|
|
@@ -209,7 +369,7 @@ window.__ModuleLoader__.load({
|
|
|
209
369
|
return byProvider;
|
|
210
370
|
}
|
|
211
371
|
|
|
212
|
-
/**
|
|
372
|
+
/** Latest probe result per key (#219): /sandbox-cache (smart polling). */
|
|
213
373
|
function useProbeCache() {
|
|
214
374
|
const [cache, setCache] = React.useState({});
|
|
215
375
|
React.useEffect(() => {
|
|
@@ -219,7 +379,7 @@ window.__ModuleLoader__.load({
|
|
|
219
379
|
fetch('/dsh-key-rotation/sandbox-cache', { headers: { accept: 'application/json' } })
|
|
220
380
|
.then((r) => (r.ok ? r.json() : null))
|
|
221
381
|
.then((data) => { if (alive && data) setCache(data); })
|
|
222
|
-
.catch(() => { /*
|
|
382
|
+
.catch(() => { /* cache is non-critical: card works without it */ });
|
|
223
383
|
};
|
|
224
384
|
pull();
|
|
225
385
|
const id = setInterval(pull, 4000);
|
|
@@ -243,10 +403,9 @@ window.__ModuleLoader__.load({
|
|
|
243
403
|
return t('hoursAgo').replace('{n}', String(Math.round(sec / 3600)));
|
|
244
404
|
}
|
|
245
405
|
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
// под ним ужимается сама.
|
|
406
|
+
// Card layout uses a grid, not ad-hoc inline widths. Fixed widths used to
|
|
407
|
+
// clip key names and collide actions with the value field, so the name owns
|
|
408
|
+
// its own row and the meta row shrinks on its own.
|
|
250
409
|
const CARD_CSS = [
|
|
251
410
|
'.krot{display:flex;flex-direction:column;gap:20px;max-width:960px;padding:6px 0 24px;box-sizing:border-box}',
|
|
252
411
|
'.krot-header{display:flex;flex-direction:column;gap:8px;padding-bottom:16px;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
@@ -254,7 +413,7 @@ window.__ModuleLoader__.load({
|
|
|
254
413
|
'.krot-page-sub{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.5}',
|
|
255
414
|
'.krot p{margin:0}',
|
|
256
415
|
'.krot-hint{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.4}',
|
|
257
|
-
'.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:
|
|
416
|
+
'.krot-err{font-size:13px;color:var(--dsw-alias-state-error-primary);padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);display:flex;align-items:center;gap:8px}',
|
|
258
417
|
'.krot-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary)}',
|
|
259
418
|
'.krot-field{display:flex;flex-direction:column;gap:6px}',
|
|
260
419
|
'.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}',
|
|
@@ -277,15 +436,15 @@ window.__ModuleLoader__.load({
|
|
|
277
436
|
'.krot-load-legend{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;font-size:11px}',
|
|
278
437
|
'.krot-load-item{display:inline-flex;align-items:center;gap:4px}',
|
|
279
438
|
'.krot-load-dot{width:7px;height:7px;border-radius:50%}',
|
|
280
|
-
'.krot-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:
|
|
281
|
-
'.krot-modal-card{width:90%;max-width:420px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 36px
|
|
439
|
+
'.krot-modal-backdrop{position:fixed;top:0;left:0;right:0;bottom:0;background:var(--dsw-alias-bg-overlay, color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 55%, transparent));z-index:9999;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(2px)}',
|
|
440
|
+
'.krot-modal-card{width:90%;max-width:420px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:12px;box-shadow:0 12px 36px color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 28%, transparent);padding:20px;display:flex;flex-direction:column;gap:12px}',
|
|
282
441
|
'.krot-modal-title{font-size:16px;font-weight:600;color:var(--dsw-alias-label-primary)}',
|
|
283
442
|
'.krot-modal-desc{font-size:13px;color:var(--dsw-alias-label-secondary);line-height:1.45}',
|
|
284
443
|
'.krot-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:8px}',
|
|
285
444
|
'.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}',
|
|
286
|
-
'.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:
|
|
287
|
-
'.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:
|
|
288
|
-
'.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:
|
|
445
|
+
'.krot-badge-ok{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary);background:color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 8%, transparent)}',
|
|
446
|
+
'.krot-badge-warn{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:color-mix(in srgb, var(--dsw-alias-state-warning-primary, #f59e0b) 8%, transparent)}',
|
|
447
|
+
'.krot-badge-bad{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary);background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 8%, transparent)}',
|
|
289
448
|
'.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}',
|
|
290
449
|
'.krot-prov-head{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
291
450
|
'.krot-prov-head select{flex:1;min-width:180px}',
|
|
@@ -303,18 +462,22 @@ window.__ModuleLoader__.load({
|
|
|
303
462
|
'.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))}',
|
|
304
463
|
'.krot-btn-primary,.krot-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);border-color:transparent;font-weight:600}',
|
|
305
464
|
'.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}',
|
|
306
|
-
'.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:
|
|
307
|
-
'.krot-btn-danger:hover:not(:disabled){background:
|
|
465
|
+
'.krot-btn-danger{color:var(--dsw-alias-state-error-primary);border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 30%, transparent)}',
|
|
466
|
+
'.krot-btn-danger:hover:not(:disabled){background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 12%, transparent)!important;border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 50%, transparent)}',
|
|
308
467
|
'.krot-btn:disabled{opacity:0.45;cursor:not-allowed}',
|
|
309
468
|
'.krot-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}',
|
|
310
469
|
'.krot-filter-bar{display:flex;gap:8px;margin:4px 0 10px;flex-wrap:wrap}',
|
|
311
470
|
'.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}',
|
|
312
471
|
'.krot-pill:hover{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}',
|
|
313
472
|
'.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}',
|
|
314
|
-
'.krot-pill-warn{border-color:
|
|
315
|
-
'.krot-pill-err{border-color:
|
|
316
|
-
'.krot-alert-ok{padding:10px 14px;border-radius:8px;background:
|
|
317
|
-
'.krot-alert-bad{padding:10px 14px;border-radius:8px;background:
|
|
473
|
+
'.krot-pill-warn{border-color:color-mix(in srgb, var(--dsw-alias-state-warning-primary, #f59e0b) 30%, transparent);color:var(--dsw-alias-state-warning-primary)}',
|
|
474
|
+
'.krot-pill-err{border-color:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 30%, transparent);color:var(--dsw-alias-state-error-primary)}',
|
|
475
|
+
'.krot-alert-ok{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-success-primary, #10b981) 10%, transparent);color:var(--dsw-alias-state-success-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
476
|
+
'.krot-alert-bad{padding:10px 14px;border-radius:8px;background:color-mix(in srgb, var(--dsw-alias-state-error-primary, #ef4444) 10%, transparent);color:var(--dsw-alias-state-error-primary);font-size:13px;display:flex;align-items:center;gap:10px}',
|
|
477
|
+
'.krot-event-stream{display:flex;flex-direction:column;gap:6px;margin-top:8px;padding:10px 12px;background:var(--dsw-alias-bg-layer-3);border:1px solid var(--dsw-alias-border-l2);border-radius:8px}',
|
|
478
|
+
'.krot-event-row{display:flex;align-items:center;gap:8px;font-size:12px;padding:4px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
479
|
+
'.krot-event-time{font-family:ui-monospace,Menlo,Consolas,monospace;color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
480
|
+
':root{--krot-chart-4:var(--dsw-alias-state-brand-primary);--krot-chart-5:var(--dsw-alias-state-error-primary);--krot-chart-6:var(--dsw-alias-state-success-primary)}',
|
|
318
481
|
'.krot-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}',
|
|
319
482
|
'.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}',
|
|
320
483
|
'.krot-card-head-text{display:flex;flex-direction:column;flex:1;gap:4px;min-width:0}',
|
|
@@ -324,7 +487,7 @@ window.__ModuleLoader__.load({
|
|
|
324
487
|
'.krot-card-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding:16px 0 8px}',
|
|
325
488
|
'.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}',
|
|
326
489
|
'.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)}',
|
|
327
|
-
'.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
|
|
490
|
+
'.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 color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 45%, transparent),0 2px 8px color-mix(in srgb, var(--dsw-alias-bg-layer-1, #000) 15%, transparent);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);display:flex;flex-direction:column;gap:8px;text-align:left}',
|
|
328
491
|
'.krot-pop-title{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--dsw-alias-label-secondary)}',
|
|
329
492
|
'.krot-pop-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:4px 0}',
|
|
330
493
|
'.krot-pop-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary);display:flex;align-items:center;gap:8px}',
|
|
@@ -342,13 +505,13 @@ window.__ModuleLoader__.load({
|
|
|
342
505
|
}
|
|
343
506
|
|
|
344
507
|
/**
|
|
345
|
-
*
|
|
508
|
+
* Env-var name for a newly added key.
|
|
346
509
|
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
510
|
+
* Users no longer type it: the first key of a provider becomes
|
|
511
|
+
* <PROVIDER>_API_KEY, later keys reuse that root with _2, _3… suffixes.
|
|
512
|
+
* The root is taken from existing keys so hand-made names keep working,
|
|
513
|
+
* and uniqueness is checked across ALL providers — otherwise two providers
|
|
514
|
+
* would silently share one credential.
|
|
352
515
|
*/
|
|
353
516
|
// nextKeyRef also in lib/client-helpers.js
|
|
354
517
|
function nextKeyRef(providerId, existingKeys, allRefs) {
|
|
@@ -493,13 +656,14 @@ window.__ModuleLoader__.load({
|
|
|
493
656
|
cancelAnimationFrame(id);
|
|
494
657
|
const prev = modalReturnFocusRef.current;
|
|
495
658
|
if (prev && typeof prev.focus === 'function') {
|
|
496
|
-
|
|
659
|
+
bestEffort('modal.returnFocus', () => { prev.focus(); });
|
|
497
660
|
}
|
|
498
661
|
};
|
|
499
662
|
}, [confirmModal]);
|
|
500
663
|
const [testing, setTesting] = React.useState('');
|
|
501
664
|
const [testResult, setTestResult] = React.useState({});
|
|
502
665
|
const [testAllProvider, setTestAllProvider] = React.useState('');
|
|
666
|
+
const [testingProgress, setTestingProgress] = React.useState({});
|
|
503
667
|
const [secretDraft, setSecretDraft] = React.useState({});
|
|
504
668
|
const [secretError, setSecretError] = React.useState('');
|
|
505
669
|
const stashUndo = (u) => { setUndo(u); if (undoTimer.current) clearTimeout(undoTimer.current); undoTimer.current = setTimeout(() => setUndo(null), 5000); };
|
|
@@ -727,8 +891,7 @@ window.__ModuleLoader__.load({
|
|
|
727
891
|
stashUndo({ type: 'provider', index: pIndex, entry: arr[pIndex] });
|
|
728
892
|
return { ...cur, providers: arr.filter((_, i) => i !== pIndex) };
|
|
729
893
|
});
|
|
730
|
-
//
|
|
731
|
-
// а не перепечатыванием имён.
|
|
894
|
+
// Key order is attempt order — move it with buttons, do not retype names.
|
|
732
895
|
const moveKey = (pIndex, kIndex, delta) => setField((cur) => {
|
|
733
896
|
const providers = [...(cur.providers ?? [])];
|
|
734
897
|
const entry = { ...(providers[pIndex] ?? {}) };
|
|
@@ -763,8 +926,8 @@ window.__ModuleLoader__.load({
|
|
|
763
926
|
return { ...cur, providers };
|
|
764
927
|
});
|
|
765
928
|
|
|
766
|
-
//
|
|
767
|
-
//
|
|
929
|
+
// Config codes missing from the known list still render as checked items;
|
|
930
|
+
// otherwise the checkboxes would silently drop a custom rule on first save.
|
|
768
931
|
const selectedCodes = new Set(Array.isArray(val.switchCodes) ? val.switchCodes : []);
|
|
769
932
|
const codeList = [...KNOWN_CODES, ...[...selectedCodes].filter((c) => !KNOWN_CODES.includes(c))];
|
|
770
933
|
const toggleCode = (code, on) => setField((cur) => {
|
|
@@ -834,8 +997,8 @@ window.__ModuleLoader__.load({
|
|
|
834
997
|
title: (opts && opts.title) || undefined,
|
|
835
998
|
}, labelText);
|
|
836
999
|
|
|
837
|
-
//
|
|
838
|
-
//
|
|
1000
|
+
// Key state dot: color and label read at a glance; "key not found"
|
|
1001
|
+
// catches an env-name typo that would otherwise stay silent.
|
|
839
1002
|
const keyStatus = (providerId, ref) => {
|
|
840
1003
|
const hit = keyInfo(providerId, ref);
|
|
841
1004
|
if (!hit) return null;
|
|
@@ -904,8 +1067,7 @@ window.__ModuleLoader__.load({
|
|
|
904
1067
|
const typed = secretDraft[rowKey];
|
|
905
1068
|
const fromEnv = Boolean(info && info.source === 'env');
|
|
906
1069
|
|
|
907
|
-
//
|
|
908
|
-
// соседние ключи выглядели одинаково.
|
|
1070
|
+
// Key name owns a full row so neighbouring keys stay distinguishable.
|
|
909
1071
|
const nameRow = [
|
|
910
1072
|
h('span', { className: 'krot-num', key: 'n' }, String(kIndex + 1)),
|
|
911
1073
|
h('span', { key: 'i', className: 'krot-name', title: key + ' (click to copy)', style: { cursor: 'copy' }, onClick: () => {
|
|
@@ -1046,13 +1208,13 @@ window.__ModuleLoader__.load({
|
|
|
1046
1208
|
});
|
|
1047
1209
|
const total = keyUsages.reduce((acc, x) => acc + x.usage, 0);
|
|
1048
1210
|
const palette = [
|
|
1049
|
-
'var(--dsw-alias-state-brand-primary
|
|
1050
|
-
'var(--dsw-alias-state-success-primary
|
|
1051
|
-
'var(--dsw-alias-state-warning-primary
|
|
1052
|
-
'var(--dsw-alias-state-info-primary
|
|
1053
|
-
'
|
|
1054
|
-
'
|
|
1055
|
-
'
|
|
1211
|
+
'var(--dsw-alias-state-brand-primary)',
|
|
1212
|
+
'var(--dsw-alias-state-success-primary)',
|
|
1213
|
+
'var(--dsw-alias-state-warning-primary)',
|
|
1214
|
+
'var(--dsw-alias-state-info-primary)',
|
|
1215
|
+
'var(--krot-chart-4)',
|
|
1216
|
+
'var(--krot-chart-5)',
|
|
1217
|
+
'var(--krot-chart-6)',
|
|
1056
1218
|
];
|
|
1057
1219
|
return h('div', { className: 'krot-load-chart' },
|
|
1058
1220
|
h('div', { className: 'krot-load-header' },
|
|
@@ -1124,6 +1286,24 @@ window.__ModuleLoader__.load({
|
|
|
1124
1286
|
return h('span', { className: 'krot-tail', title: t('healthScoreTitle'), style: { flex: 'none', color, fontWeight: 700 } }, String(score));
|
|
1125
1287
|
})(),
|
|
1126
1288
|
(() => { const ps = status[entry.provider]; const tot = ps && typeof ps.totalUsage === 'number' ? ps.totalUsage : null; return tot !== null ? h('span', { className: 'krot-tail', title: t('totalRequestsTitle'), style: { flex: 'none' } }, String(tot)) : null; })(),
|
|
1289
|
+
(() => {
|
|
1290
|
+
const q = status.quota || (providerStatus && providerStatus.quota);
|
|
1291
|
+
let nearestReset = null;
|
|
1292
|
+
if (q && typeof q === 'object') {
|
|
1293
|
+
for (const k of (entry.keys || [])) {
|
|
1294
|
+
const entryQ = q[k];
|
|
1295
|
+
if (entryQ && typeof entryQ.reset === 'number' && entryQ.reset > Date.now()) {
|
|
1296
|
+
if (!nearestReset || entryQ.reset < nearestReset) nearestReset = entryQ.reset;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (!nearestReset) return null;
|
|
1301
|
+
const diffMs = nearestReset - Date.now();
|
|
1302
|
+
if (diffMs <= 0) return null;
|
|
1303
|
+
const mins = Math.ceil(diffMs / 60000);
|
|
1304
|
+
const timeStr = mins > 60 ? Math.floor(mins / 60) + 'h ' + (mins % 60) + 'm' : mins + 'm';
|
|
1305
|
+
return h('span', { className: 'krot-badge krot-badge-warn', title: 'Quota window reset' }, t('quotaResetIn').replace('{time}', timeStr));
|
|
1306
|
+
})(),
|
|
1127
1307
|
btn('✕', () => setConfirmModal({
|
|
1128
1308
|
title: t('confirmRemoveProvTitle').replace('{p}', entry.provider),
|
|
1129
1309
|
desc: t('confirmRemoveProvDesc'),
|
|
@@ -1135,7 +1315,8 @@ window.__ModuleLoader__.load({
|
|
|
1135
1315
|
filterBar,
|
|
1136
1316
|
loadChart,
|
|
1137
1317
|
h('div', { className: 'krot-keys' }, keyRows),
|
|
1138
|
-
h(
|
|
1318
|
+
h(UpdaterSection, { t }),
|
|
1319
|
+
h('div', { className: 'krot-foot' },
|
|
1139
1320
|
btn(t('addKey'), () => addKey(pIndex), { title: t('addKeyTitle') }),
|
|
1140
1321
|
switchesLine,
|
|
1141
1322
|
budgetLine,
|
|
@@ -1144,7 +1325,26 @@ window.__ModuleLoader__.load({
|
|
|
1144
1325
|
(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),
|
|
1145
1326
|
// #224: 7-day switches per day (client-side, from the same events)
|
|
1146
1327
|
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('div', { style: { display: 'flex', gap: '2px', alignItems: 'end', height: '16px', marginTop: '2px' } }, (() => { const now = Date.now(); const days = Array(7).fill(0); for (const ev of providerStatus.events) { const d = Math.floor((now - ev.at) / 86400000); if (d >= 0 && d < 7) days[6 - d]++; } const max = Math.max(1, ...days); return days.map((c, i) => h('div', { key: i, title: c + ' switches · day -' + (6 - i), style: { flex: 1, background: c ? 'var(--dsw-alias-state-info-primary, var(--dsw-alias-state-warning-primary))' : 'var(--dsw-alias-border-l2)', height: (c / max * 16) + 'px', minHeight: '2px', borderRadius: '2px' } })); })()) : null),
|
|
1147
|
-
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', {
|
|
1328
|
+
(providerStatus && Array.isArray(providerStatus.events) && providerStatus.events.length > 0 ? h('details', { className: 'krot-event-stream' },
|
|
1329
|
+
h('summary', { style: { display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer', fontWeight: 600, fontSize: '12px' } },
|
|
1330
|
+
h('span', null, '📜 ' + t('liveEventStream')),
|
|
1331
|
+
h('span', { className: 'krot-badge' }, String(providerStatus.events.length))
|
|
1332
|
+
),
|
|
1333
|
+
h('div', { style: { display: 'flex', flexDirection: 'column', gap: '4px', marginTop: '8px' } },
|
|
1334
|
+
providerStatus.events.slice().reverse().map((ev, i) => {
|
|
1335
|
+
const isHeal = ev.type === 'heal' || ev.reason === 'auto-unbreak' || ev.reason === 'self-heal';
|
|
1336
|
+
const isBad = ev.code === 'AUTH' || ev.reason === 'AUTH' || ev.type === 'broken';
|
|
1337
|
+
const badgeClass = isHeal ? 'krot-badge-ok' : isBad ? 'krot-badge-bad' : 'krot-badge-warn';
|
|
1338
|
+
const timeStr = ev.at ? new Date(ev.at).toLocaleTimeString() : '';
|
|
1339
|
+
return h('div', { key: i, className: 'krot-event-row' },
|
|
1340
|
+
h('span', { className: 'krot-event-time' }, timeStr),
|
|
1341
|
+
h('span', { className: 'krot-tail' }, ev.ref || 'pool'),
|
|
1342
|
+
h('span', { className: 'krot-badge ' + badgeClass }, String(ev.reason || ev.code || ev.type)),
|
|
1343
|
+
ev.cooldownMs > 0 ? h('span', { style: { fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' } }, 'cd: ' + Math.round(ev.cooldownMs / 1000) + 's') : null
|
|
1344
|
+
);
|
|
1345
|
+
})
|
|
1346
|
+
)
|
|
1347
|
+
) : null),
|
|
1148
1348
|
btn(t('resetCooldown'), () => setConfirmModal({
|
|
1149
1349
|
title: t('confirmResetTitle').replace('{p}', entry.provider),
|
|
1150
1350
|
desc: t('confirmResetDesc'),
|
|
@@ -1152,7 +1352,7 @@ window.__ModuleLoader__.load({
|
|
|
1152
1352
|
danger: false,
|
|
1153
1353
|
onConfirm: () => doReset(entry.provider),
|
|
1154
1354
|
}), { disabled: !(providerStatus && providerStatus.switches > 0) || resetting === entry.provider, title: t('resetCooldown') }),
|
|
1155
|
-
btn(testAllProvider === entry.provider ? t('testing') : t('testAll'), () => doTestAll(entry.provider), { disabled: testAllProvider === entry.provider, title: t('testAll') }),
|
|
1355
|
+
btn(testAllProvider === entry.provider ? (testingProgress[entry.provider] ? t('testingAllProgress').replace('{n}', testingProgress[entry.provider].split('/')[0]).replace('{total}', testingProgress[entry.provider].split('/')[1]) : t('testing')) : t('testAll'), () => doTestAll(entry.provider), { disabled: testAllProvider === entry.provider, title: t('testAll') }),
|
|
1156
1356
|
exportCsv,
|
|
1157
1357
|
),
|
|
1158
1358
|
);
|
|
@@ -1276,6 +1476,26 @@ window.__ModuleLoader__.load({
|
|
|
1276
1476
|
h('div', { className: 'krot-grid-2' },
|
|
1277
1477
|
field(t('cooldown'), textInput(String(val.cooldownMs ?? 60000), (v) => setField((cur) => ({ ...cur, cooldownMs: Number(v) || 0 })))),
|
|
1278
1478
|
field(t('scheduleDays'), textInput(String(val.rotationScheduleDays ?? 0), (v) => setField((cur) => ({ ...cur, rotationScheduleDays: Number(v) || 0 }))))
|
|
1479
|
+
),
|
|
1480
|
+
h('div', { className: 'krot-grid-3', style: { marginTop: '10px' } },
|
|
1481
|
+
field(t('routingStrategyLabel'), h('select', {
|
|
1482
|
+
className: 'krot-in',
|
|
1483
|
+
value: val.routingStrategy ?? 'round-robin',
|
|
1484
|
+
onChange: (e) => setField((cur) => ({ ...cur, routingStrategy: e.target.value })),
|
|
1485
|
+
}, [
|
|
1486
|
+
h('option', { key: 'round-robin', value: 'round-robin' }, t('routingStrategyRoundRobin')),
|
|
1487
|
+
h('option', { key: 'least-loaded', value: 'least-loaded' }, t('routingStrategyLeastLoaded')),
|
|
1488
|
+
h('option', { key: 'lowest-latency', value: 'lowest-latency' }, t('routingStrategyLowestLatency')),
|
|
1489
|
+
])),
|
|
1490
|
+
field(t('selfHealingLabel'), textInput(String(val.selfHealingIntervalMinutes ?? 30), (v) => setField((cur) => ({ ...cur, selfHealingIntervalMinutes: Number(v) || 0 })))),
|
|
1491
|
+
field(t('proactiveGuardLabel'), h('label', { style: { display: 'flex', alignItems: 'center', gap: '8px', height: '36px', cursor: 'pointer' } }, [
|
|
1492
|
+
h('input', {
|
|
1493
|
+
type: 'checkbox',
|
|
1494
|
+
checked: val.proactiveRateLimitGuard ?? true,
|
|
1495
|
+
onChange: (e) => setField((cur) => ({ ...cur, proactiveRateLimitGuard: e.target.checked })),
|
|
1496
|
+
}),
|
|
1497
|
+
h('span', { style: { fontSize: '13px', color: 'var(--dsw-alias-label-secondary)' } }, t('proactiveGuardLabel')),
|
|
1498
|
+
]))
|
|
1279
1499
|
)
|
|
1280
1500
|
);
|
|
1281
1501
|
|
|
@@ -1475,7 +1695,7 @@ window.__ModuleLoader__.load({
|
|
|
1475
1695
|
}
|
|
1476
1696
|
|
|
1477
1697
|
function apply(ctx) {
|
|
1478
|
-
ctx.effect(() => ctx.locale.register(NS, { en }), 'dsh-key-rotation: dictionaries');
|
|
1698
|
+
ctx.effect(() => ctx.locale.register(NS, { en, zh }), 'dsh-key-rotation: dictionaries');
|
|
1479
1699
|
// Header chip (#201): status dot in the session header utilities slot,
|
|
1480
1700
|
// same slot dsh-gitea / dsh-subscriptions use for their header widgets.
|
|
1481
1701
|
ctx.effect(() => {
|
|
@@ -1495,7 +1715,72 @@ window.__ModuleLoader__.load({
|
|
|
1495
1715
|
// Collapsible card in Settings -> Plugins -> Plugin settings
|
|
1496
1716
|
// (settings.plugin.item), matching Model Sync / Spendmeter / Vision Bridge.
|
|
1497
1717
|
// key MUST equal the settings namespace (NS), else the tab silently skips it.
|
|
1498
|
-
|
|
1718
|
+
|
|
1719
|
+
function UpdaterSection({ t }) {
|
|
1720
|
+
const [state, setState] = React.useState({ phase: 'idle', current: '', latest: '', message: '' });
|
|
1721
|
+
const load = React.useCallback(async () => {
|
|
1722
|
+
setState((s) => ({ ...s, phase: 'loading', message: '' }));
|
|
1723
|
+
try {
|
|
1724
|
+
const res = await fetch('/api/dsh-key-rotation/update', { headers: { 'x-dsh-plugin-update': '1' } });
|
|
1725
|
+
const data = await res.json();
|
|
1726
|
+
if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
|
|
1727
|
+
setState({
|
|
1728
|
+
phase: data.updateAvailable ? 'available' : 'current',
|
|
1729
|
+
current: data.currentVersion || '',
|
|
1730
|
+
latest: data.latestVersion || '',
|
|
1731
|
+
message: data.updateAvailable ? t('updateAvailable') : t('updateNone'),
|
|
1732
|
+
});
|
|
1733
|
+
} catch (e) {
|
|
1734
|
+
setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
|
|
1735
|
+
}
|
|
1736
|
+
}, [t]);
|
|
1737
|
+
React.useEffect(() => { load(); }, [load]);
|
|
1738
|
+
const run = async () => {
|
|
1739
|
+
setState((s) => ({ ...s, phase: 'running', message: t('updateRunning') }));
|
|
1740
|
+
try {
|
|
1741
|
+
const res = await fetch('/api/dsh-key-rotation/update', {
|
|
1742
|
+
method: 'POST',
|
|
1743
|
+
headers: { 'x-dsh-plugin-update': '1', 'content-type': 'application/json' },
|
|
1744
|
+
});
|
|
1745
|
+
const data = await res.json();
|
|
1746
|
+
if (!res.ok) throw new Error(data?.error?.message || t('updateFailed'));
|
|
1747
|
+
setState({
|
|
1748
|
+
phase: 'done',
|
|
1749
|
+
current: data.currentVersion || '',
|
|
1750
|
+
latest: data.installedVersion || data.latestVersion || '',
|
|
1751
|
+
message: t('updateRestart'),
|
|
1752
|
+
});
|
|
1753
|
+
} catch (e) {
|
|
1754
|
+
setState({ phase: 'error', current: '', latest: '', message: e?.message || t('updateFailed') });
|
|
1755
|
+
}
|
|
1756
|
+
};
|
|
1757
|
+
return h('div', { className: 'krot-update', style: { marginTop: 12, paddingTop: 12, borderTop: '1px solid var(--dsw-alias-border-l2)' } },
|
|
1758
|
+
h('div', { style: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' } },
|
|
1759
|
+
h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
|
|
1760
|
+
t('currentVersion') + ': ' + (state.current || '—')),
|
|
1761
|
+
state.latest ? h('span', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } },
|
|
1762
|
+
t('latestVersion') + ': ' + state.latest) : null,
|
|
1763
|
+
h('button', {
|
|
1764
|
+
className: 'krot-btn',
|
|
1765
|
+
type: 'button',
|
|
1766
|
+
disabled: state.phase === 'running' || state.phase === 'loading',
|
|
1767
|
+
onClick: load,
|
|
1768
|
+
}, t('updateCheck')),
|
|
1769
|
+
state.phase === 'available' ? h('button', {
|
|
1770
|
+
className: 'krot-btn krot-btn-primary',
|
|
1771
|
+
type: 'button',
|
|
1772
|
+
disabled: state.phase === 'running',
|
|
1773
|
+
onClick: run,
|
|
1774
|
+
}, t('updateRun')) : null,
|
|
1775
|
+
),
|
|
1776
|
+
state.message ? h('div', {
|
|
1777
|
+
className: state.phase === 'error' ? 'krot-alert-bad' : state.phase === 'done' ? 'krot-alert-ok' : '',
|
|
1778
|
+
style: { marginTop: 8, fontSize: 12 },
|
|
1779
|
+
}, state.message) : null,
|
|
1780
|
+
);
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
function KeyRotationCard(props) {
|
|
1499
1784
|
const locale = useLocale();
|
|
1500
1785
|
const t = resolveT(props);
|
|
1501
1786
|
const [open, setOpen] = React.useState(false);
|
|
@@ -1518,11 +1803,31 @@ window.__ModuleLoader__.load({
|
|
|
1518
1803
|
KeyRotationCard,
|
|
1519
1804
|
));
|
|
1520
1805
|
} catch (e) {
|
|
1521
|
-
|
|
1806
|
+
bestEffort('slots.register.log', () => { console.error('[dsh-key-rotation] settings.plugin.item register failed', e); });
|
|
1522
1807
|
}
|
|
1523
1808
|
}
|
|
1524
1809
|
|
|
1525
1810
|
module.exports = { apply, inject: ['slots', 'locale', 'settingsScope'] };
|
|
1526
1811
|
return module.exports;
|
|
1527
1812
|
},
|
|
1813
|
+
updateCheck: 'Check for updates',
|
|
1814
|
+
updateAvailable: 'Update available',
|
|
1815
|
+
updateNone: 'Plugin is up to date',
|
|
1816
|
+
updateRun: 'Update now',
|
|
1817
|
+
updateRestart: 'Update installed. Restart DSH to load the new version.',
|
|
1818
|
+
updateRunning: 'Updating…',
|
|
1819
|
+
updateFailed: 'Update failed',
|
|
1820
|
+
currentVersion: 'Current version',
|
|
1821
|
+
latestVersion: 'Latest version',
|
|
1822
|
+
|
|
1823
|
+
updateCheck: '检查更新',
|
|
1824
|
+
updateAvailable: '有可用更新',
|
|
1825
|
+
updateNone: '插件已是最新版本',
|
|
1826
|
+
updateRun: '立即更新',
|
|
1827
|
+
updateRestart: '更新已安装。请重启 DSH 以加载新版本。',
|
|
1828
|
+
updateRunning: '更新中…',
|
|
1829
|
+
updateFailed: '更新失败',
|
|
1830
|
+
currentVersion: '当前版本',
|
|
1831
|
+
latestVersion: '最新版本',
|
|
1832
|
+
|
|
1528
1833
|
});
|