@goodandready/dsh-key-rotation 0.8.88 → 0.8.89
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/CHANGELOG.md +9 -0
- package/lib/client-src/10-locales.js +10 -0
- package/lib/client-src/60-header-chip.js +69 -14
- package/lib/client.js +45 -6
- package/lib/routes/quick-action.js +26 -2
- package/lib/routes/status.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.89 (2026-11-03)
|
|
4
|
+
|
|
5
|
+
### Interactive Header Chip UI & In-Chat Key Controls (Block 16 Release)
|
|
6
|
+
- **Interactive Header Chip Component (Issue #577)**: Enhanced `KeyRotationHeaderChip` (`lib/client-src/60-header-chip.js`) into a rich, interactive control center directly in the DeepSeek Harness chat header.
|
|
7
|
+
- **In-Chat Key Pinning & Selection**: Added instant dropdown selection to pin (`pinnedRef`) any key in the pool or restore `Auto (Round-robin)` rotation without opening the full settings page.
|
|
8
|
+
- **Prompt Cache & Health Badges**: Displays live prompt cache hit counters (`⚡ {promptCacheHits}`) and active/pinned key tails (`...tail`) with real-time health indicator status.
|
|
9
|
+
- **Quick-Action API Pinning Support**: Extended `/dsh-key-rotation/quick-action` route with `action: 'pin'` and audit event emission (`KEY_PINNED` / `KEY_UNPINNED`).
|
|
10
|
+
- **Detailed Health Endpoint**: Enriched `/dsh-key-rotation/health` to expose pool key manifests, individual key statuses (`active`, `pinned`, `paused`, `cooling`), and prompt cache hit statistics.
|
|
11
|
+
|
|
3
12
|
## 0.8.88 (2026-11-03)
|
|
4
13
|
|
|
5
14
|
### Subagent Concurrency Partitions & Task Isolation (Block 15 Release)
|
|
@@ -245,6 +245,11 @@
|
|
|
245
245
|
quickResetTitle: 'Reset cooldowns and circuit breaker for active provider',
|
|
246
246
|
quickPause: 'Pause',
|
|
247
247
|
quickPauseTitle: 'Pause or resume active key',
|
|
248
|
+
quickPin: 'Pin',
|
|
249
|
+
quickPinTitle: 'Pin specific key or select Auto round-robin',
|
|
250
|
+
quickAuto: 'Auto (Round-robin)',
|
|
251
|
+
cacheHits: 'Cache: {n}',
|
|
252
|
+
pinnedBadge: 'pinned',
|
|
248
253
|
exportConfig: 'Export Pools',
|
|
249
254
|
importConfig: 'Import Pools',
|
|
250
255
|
exportSafe: 'Masked (Safe for sharing)',
|
|
@@ -518,6 +523,11 @@
|
|
|
518
523
|
quickResetTitle: '重置当前提供商的冷却时间与断路器',
|
|
519
524
|
quickPause: '暂停',
|
|
520
525
|
quickPauseTitle: '暂停或恢复当前正在使用的密钥',
|
|
526
|
+
quickPin: '固定',
|
|
527
|
+
quickPinTitle: '固定指定密钥或设为自动轮换',
|
|
528
|
+
quickAuto: '自动轮换',
|
|
529
|
+
cacheHits: '缓存: {n}',
|
|
530
|
+
pinnedBadge: '已固定',
|
|
521
531
|
exportConfig: '导出密钥池',
|
|
522
532
|
importConfig: '导入密钥池',
|
|
523
533
|
exportSafe: '脱敏导出 (安全分享)',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// (issue 201, #499) header chip: one dot + counts for all pools. Green = all healthy,
|
|
2
|
-
// amber = some keys cooling, red = a pool fully exhausted. Click opens quick actions popover.
|
|
1
|
+
// (issue 201, #499, #577) header chip: one dot + counts for all pools. Green = all healthy,
|
|
2
|
+
// amber = some keys cooling, red = a pool fully exhausted. Click opens interactive quick actions popover.
|
|
3
3
|
function KeyRotationHeaderChip(props) {
|
|
4
4
|
const t = resolveT(props);
|
|
5
5
|
const [snap, setSnap] = React.useState(null);
|
|
@@ -93,46 +93,101 @@
|
|
|
93
93
|
<span style={{ fontSize: '9px', opacity: 0.6 }}>{open ? '▲' : '▼'}</span>
|
|
94
94
|
</button>
|
|
95
95
|
{open ? (
|
|
96
|
-
<div className="krot-popover" style={{ minWidth: '
|
|
97
|
-
<div className="krot-pop-title"
|
|
96
|
+
<div className="krot-popover" style={{ minWidth: '260px' }}>
|
|
97
|
+
<div className="krot-pop-title" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
98
|
+
<span>{t('headerPoolsTitle')}</span>
|
|
99
|
+
{busy ? <span style={{ fontSize: '11px', opacity: 0.7 }}>...</span> : null}
|
|
100
|
+
</div>
|
|
98
101
|
{!poolEntries.length ? (
|
|
99
102
|
<div style={{ fontSize: '12px', color: 'var(--dsw-alias-label-tertiary)' }}>{t('noActivePools')}</div>
|
|
100
103
|
) : null}
|
|
101
104
|
{poolEntries.map(([name, p]) => {
|
|
102
105
|
const c = p.exhausted ? 'var(--dsw-alias-state-error-primary)' : (p.healthy < p.total ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-state-success-primary)');
|
|
103
106
|
const provName = p.providerName || name;
|
|
104
|
-
const
|
|
107
|
+
const activeTail = p.lastUsedTail;
|
|
108
|
+
const pinnedTail = p.pinnedTail;
|
|
109
|
+
const isPinned = Boolean(p.pinnedRef);
|
|
110
|
+
const keysList = Array.isArray(p.keys) ? p.keys : [];
|
|
111
|
+
const cacheHits = p.promptCacheHits || 0;
|
|
112
|
+
|
|
105
113
|
return (
|
|
106
|
-
<div key={name} className="krot-pop-row" style={{ display: 'flex', flexDirection: 'column', gap: '
|
|
114
|
+
<div key={name} className="krot-pop-row" style={{ display: 'flex', flexDirection: 'column', gap: '5px', padding: '8px 0', borderBottom: '1px solid var(--dsw-alias-border-subtle)' }}>
|
|
107
115
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
108
|
-
<div className="krot-pop-name">
|
|
116
|
+
<div className="krot-pop-name" style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
|
109
117
|
<span style={{ width: '7px', height: '7px', borderRadius: '50%', background: c, flex: 'none' }} />
|
|
110
|
-
{name}
|
|
111
|
-
{tail ? <span style={{ marginLeft: '6px', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)', fontFeatureSettings: '"tnum"', fontFamily: 'monospace' }}>{`...${tail}`}</span> : null}
|
|
118
|
+
<strong style={{ fontSize: '12px' }}>{name}</strong>
|
|
112
119
|
</div>
|
|
113
|
-
<div className="krot-pop-count" style={{ color: c, fontWeight: 600 }}>{`${p.healthy}/${p.total}`}</div>
|
|
120
|
+
<div className="krot-pop-count" style={{ color: c, fontWeight: 600, fontSize: '11px' }}>{`${p.healthy}/${p.total}`}</div>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', fontSize: '11px', color: 'var(--dsw-alias-label-tertiary)' }}>
|
|
124
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: '4px' }}>
|
|
125
|
+
<span>{isPinned ? '📌' : '⚡'}</span>
|
|
126
|
+
<span style={{ fontFamily: 'monospace' }}>
|
|
127
|
+
{isPinned ? (pinnedTail ? `...${pinnedTail} (pinned)` : p.pinnedRef) : (activeTail ? `...${activeTail}` : 'auto')}
|
|
128
|
+
</span>
|
|
129
|
+
</span>
|
|
130
|
+
{cacheHits > 0 ? (
|
|
131
|
+
<span style={{ fontSize: '10px', padding: '1px 4px', borderRadius: '3px', background: 'var(--dsw-alias-bg-subtle, transparent)', color: 'var(--dsw-alias-state-success-primary)' }} title="Prompt cache hits">
|
|
132
|
+
{`⚡ ${cacheHits}`}
|
|
133
|
+
</span>
|
|
134
|
+
) : null}
|
|
114
135
|
</div>
|
|
115
|
-
|
|
136
|
+
|
|
137
|
+
{keysList.length > 1 ? (
|
|
138
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '6px', marginTop: '2px' }}>
|
|
139
|
+
<select
|
|
140
|
+
disabled={busy}
|
|
141
|
+
value={p.pinnedRef || ''}
|
|
142
|
+
onChange={(e) => runAction('pin', provName, e.target.value)}
|
|
143
|
+
title={t('quickPinTitle')}
|
|
144
|
+
style={{
|
|
145
|
+
width: '100%',
|
|
146
|
+
fontSize: '11px',
|
|
147
|
+
padding: '3px 6px',
|
|
148
|
+
borderRadius: '4px',
|
|
149
|
+
border: '1px solid var(--dsw-alias-border-subtle)',
|
|
150
|
+
background: 'var(--dsw-alias-bg-subtle, transparent)',
|
|
151
|
+
color: 'inherit',
|
|
152
|
+
fontFamily: 'inherit',
|
|
153
|
+
cursor: 'pointer'
|
|
154
|
+
}}
|
|
155
|
+
>
|
|
156
|
+
<option value="">{`🔀 ${t('quickAuto')}`}</option>
|
|
157
|
+
{keysList.map((k) => {
|
|
158
|
+
const marker = k.pinned ? '📌 ' : (k.active ? '▶ ' : '');
|
|
159
|
+
const status = k.paused ? ' [paused]' : (k.cooling ? ' [cooling]' : '');
|
|
160
|
+
return (
|
|
161
|
+
<option key={k.ref} value={k.ref}>
|
|
162
|
+
{`${marker}...${k.tail || k.ref.slice(-4)}${status}`}
|
|
163
|
+
</option>
|
|
164
|
+
);
|
|
165
|
+
})}
|
|
166
|
+
</select>
|
|
167
|
+
</div>
|
|
168
|
+
) : null}
|
|
169
|
+
|
|
170
|
+
<div style={{ display: 'flex', gap: '4px', marginTop: '3px' }}>
|
|
116
171
|
<button
|
|
117
172
|
type="button"
|
|
118
173
|
disabled={busy}
|
|
119
174
|
onClick={(e) => { e.stopPropagation(); runAction('skip', provName, p.lastUsed); }}
|
|
120
175
|
title={t('quickSkipTitle')}
|
|
121
|
-
style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
176
|
+
style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
122
177
|
>{`⏭️ ${t('quickSkip')}`}</button>
|
|
123
178
|
<button
|
|
124
179
|
type="button"
|
|
125
180
|
disabled={busy}
|
|
126
181
|
onClick={(e) => { e.stopPropagation(); runAction('reset', provName, p.lastUsed); }}
|
|
127
182
|
title={t('quickResetTitle')}
|
|
128
|
-
style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
183
|
+
style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
129
184
|
>{`🔄 ${t('quickReset')}`}</button>
|
|
130
185
|
<button
|
|
131
186
|
type="button"
|
|
132
187
|
disabled={busy || !p.lastUsed}
|
|
133
188
|
onClick={(e) => { e.stopPropagation(); runAction('pause', provName, p.lastUsed); }}
|
|
134
189
|
title={t('quickPauseTitle')}
|
|
135
|
-
style={{ padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
190
|
+
style={{ flex: 1, padding: '2px 6px', fontSize: '11px', borderRadius: '4px', border: '1px solid var(--dsw-alias-border-subtle)', background: 'transparent', cursor: 'pointer', color: 'inherit' }}
|
|
136
191
|
>{`⏸️ ${t('quickPause')}`}</button>
|
|
137
192
|
</div>
|
|
138
193
|
</div>
|
package/lib/client.js
CHANGED
|
@@ -264,6 +264,11 @@ window.__ModuleLoader__.load({
|
|
|
264
264
|
quickResetTitle: "Reset cooldowns and circuit breaker for active provider",
|
|
265
265
|
quickPause: "Pause",
|
|
266
266
|
quickPauseTitle: "Pause or resume active key",
|
|
267
|
+
quickPin: "Pin",
|
|
268
|
+
quickPinTitle: "Pin specific key or select Auto round-robin",
|
|
269
|
+
quickAuto: "Auto (Round-robin)",
|
|
270
|
+
cacheHits: "Cache: {n}",
|
|
271
|
+
pinnedBadge: "pinned",
|
|
267
272
|
exportConfig: "Export Pools",
|
|
268
273
|
importConfig: "Import Pools",
|
|
269
274
|
exportSafe: "Masked (Safe for sharing)",
|
|
@@ -536,6 +541,11 @@ window.__ModuleLoader__.load({
|
|
|
536
541
|
quickResetTitle: "重置当前提供商的冷却时间与断路器",
|
|
537
542
|
quickPause: "暂停",
|
|
538
543
|
quickPauseTitle: "暂停或恢复当前正在使用的密钥",
|
|
544
|
+
quickPin: "固定",
|
|
545
|
+
quickPinTitle: "固定指定密钥或设为自动轮换",
|
|
546
|
+
quickAuto: "自动轮换",
|
|
547
|
+
cacheHits: "缓存: {n}",
|
|
548
|
+
pinnedBadge: "已固定",
|
|
539
549
|
exportConfig: "导出密钥池",
|
|
540
550
|
importConfig: "导入密钥池",
|
|
541
551
|
exportSafe: "脱敏导出 (安全分享)",
|
|
@@ -3833,11 +3843,40 @@ window.__ModuleLoader__.load({
|
|
|
3833
3843
|
/* @__PURE__ */ React.createElement("span", { style: { width: "8px", height: "8px", borderRadius: "50%", background: color, flex: "none", boxShadow: `0 0 6px ${color}` } }),
|
|
3834
3844
|
label,
|
|
3835
3845
|
/* @__PURE__ */ React.createElement("span", { style: { fontSize: "9px", opacity: 0.6 } }, open ? "▲" : "▼")
|
|
3836
|
-
), open ? /* @__PURE__ */ React.createElement("div", { className: "krot-popover", style: { minWidth: "
|
|
3846
|
+
), open ? /* @__PURE__ */ React.createElement("div", { className: "krot-popover", style: { minWidth: "260px" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-title", style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, /* @__PURE__ */ React.createElement("span", null, t("headerPoolsTitle")), busy ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: "11px", opacity: 0.7 } }, "...") : null), !poolEntries.length ? /* @__PURE__ */ React.createElement("div", { style: { fontSize: "12px", color: "var(--dsw-alias-label-tertiary)" } }, t("noActivePools")) : null, poolEntries.map(([name, p]) => {
|
|
3837
3847
|
const c = p.exhausted ? "var(--dsw-alias-state-error-primary)" : p.healthy < p.total ? "var(--dsw-alias-state-warn-primary)" : "var(--dsw-alias-state-success-primary)";
|
|
3838
3848
|
const provName = p.providerName || name;
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3849
|
+
const activeTail = p.lastUsedTail;
|
|
3850
|
+
const pinnedTail = p.pinnedTail;
|
|
3851
|
+
const isPinned = Boolean(p.pinnedRef);
|
|
3852
|
+
const keysList = Array.isArray(p.keys) ? p.keys : [];
|
|
3853
|
+
const cacheHits = p.promptCacheHits || 0;
|
|
3854
|
+
return /* @__PURE__ */ React.createElement("div", { key: name, className: "krot-pop-row", style: { display: "flex", flexDirection: "column", gap: "5px", padding: "8px 0", borderBottom: "1px solid var(--dsw-alias-border-subtle)" } }, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", justifyContent: "space-between", alignItems: "center" } }, /* @__PURE__ */ React.createElement("div", { className: "krot-pop-name", style: { display: "flex", alignItems: "center", gap: "6px" } }, /* @__PURE__ */ React.createElement("span", { style: { width: "7px", height: "7px", borderRadius: "50%", background: c, flex: "none" } }), /* @__PURE__ */ React.createElement("strong", { style: { fontSize: "12px" } }, name)), /* @__PURE__ */ React.createElement("div", { className: "krot-pop-count", style: { color: c, fontWeight: 600, fontSize: "11px" } }, `${p.healthy}/${p.total}`)), /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", fontSize: "11px", color: "var(--dsw-alias-label-tertiary)" } }, /* @__PURE__ */ React.createElement("span", { style: { display: "inline-flex", alignItems: "center", gap: "4px" } }, /* @__PURE__ */ React.createElement("span", null, isPinned ? "📌" : "⚡"), /* @__PURE__ */ React.createElement("span", { style: { fontFamily: "monospace" } }, isPinned ? pinnedTail ? `...${pinnedTail} (pinned)` : p.pinnedRef : activeTail ? `...${activeTail}` : "auto")), cacheHits > 0 ? /* @__PURE__ */ React.createElement("span", { style: { fontSize: "10px", padding: "1px 4px", borderRadius: "3px", background: "var(--dsw-alias-bg-subtle, transparent)", color: "var(--dsw-alias-state-success-primary)" }, title: "Prompt cache hits" }, `⚡ ${cacheHits}`) : null), keysList.length > 1 ? /* @__PURE__ */ React.createElement("div", { style: { display: "flex", alignItems: "center", gap: "6px", marginTop: "2px" } }, /* @__PURE__ */ React.createElement(
|
|
3855
|
+
"select",
|
|
3856
|
+
{
|
|
3857
|
+
disabled: busy,
|
|
3858
|
+
value: p.pinnedRef || "",
|
|
3859
|
+
onChange: (e) => runAction("pin", provName, e.target.value),
|
|
3860
|
+
title: t("quickPinTitle"),
|
|
3861
|
+
style: {
|
|
3862
|
+
width: "100%",
|
|
3863
|
+
fontSize: "11px",
|
|
3864
|
+
padding: "3px 6px",
|
|
3865
|
+
borderRadius: "4px",
|
|
3866
|
+
border: "1px solid var(--dsw-alias-border-subtle)",
|
|
3867
|
+
background: "var(--dsw-alias-bg-subtle, transparent)",
|
|
3868
|
+
color: "inherit",
|
|
3869
|
+
fontFamily: "inherit",
|
|
3870
|
+
cursor: "pointer"
|
|
3871
|
+
}
|
|
3872
|
+
},
|
|
3873
|
+
/* @__PURE__ */ React.createElement("option", { value: "" }, `🔀 ${t("quickAuto")}`),
|
|
3874
|
+
keysList.map((k) => {
|
|
3875
|
+
const marker = k.pinned ? "📌 " : k.active ? "▶ " : "";
|
|
3876
|
+
const status = k.paused ? " [paused]" : k.cooling ? " [cooling]" : "";
|
|
3877
|
+
return /* @__PURE__ */ React.createElement("option", { key: k.ref, value: k.ref }, `${marker}...${k.tail || k.ref.slice(-4)}${status}`);
|
|
3878
|
+
})
|
|
3879
|
+
)) : null, /* @__PURE__ */ React.createElement("div", { style: { display: "flex", gap: "4px", marginTop: "3px" } }, /* @__PURE__ */ React.createElement(
|
|
3841
3880
|
"button",
|
|
3842
3881
|
{
|
|
3843
3882
|
type: "button",
|
|
@@ -3847,7 +3886,7 @@ window.__ModuleLoader__.load({
|
|
|
3847
3886
|
runAction("skip", provName, p.lastUsed);
|
|
3848
3887
|
},
|
|
3849
3888
|
title: t("quickSkipTitle"),
|
|
3850
|
-
style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3889
|
+
style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3851
3890
|
},
|
|
3852
3891
|
`⏭️ ${t("quickSkip")}`
|
|
3853
3892
|
), /* @__PURE__ */ React.createElement(
|
|
@@ -3860,7 +3899,7 @@ window.__ModuleLoader__.load({
|
|
|
3860
3899
|
runAction("reset", provName, p.lastUsed);
|
|
3861
3900
|
},
|
|
3862
3901
|
title: t("quickResetTitle"),
|
|
3863
|
-
style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3902
|
+
style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3864
3903
|
},
|
|
3865
3904
|
`🔄 ${t("quickReset")}`
|
|
3866
3905
|
), /* @__PURE__ */ React.createElement(
|
|
@@ -3873,7 +3912,7 @@ window.__ModuleLoader__.load({
|
|
|
3873
3912
|
runAction("pause", provName, p.lastUsed);
|
|
3874
3913
|
},
|
|
3875
3914
|
title: t("quickPauseTitle"),
|
|
3876
|
-
style: { padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3915
|
+
style: { flex: 1, padding: "2px 6px", fontSize: "11px", borderRadius: "4px", border: "1px solid var(--dsw-alias-border-subtle)", background: "transparent", cursor: "pointer", color: "inherit" }
|
|
3877
3916
|
},
|
|
3878
3917
|
`⏸️ ${t("quickPause")}`
|
|
3879
3918
|
)));
|
|
@@ -35,8 +35,8 @@ export function registerQuickActionRoutes(ctx, deps) {
|
|
|
35
35
|
const provider = typeof body?.provider === 'string' ? body.provider.trim() : '';
|
|
36
36
|
const ref = typeof body?.ref === 'string' ? body.ref.trim() : null;
|
|
37
37
|
|
|
38
|
-
if (!action || !['skip', 'reset', 'pause'].includes(action)) {
|
|
39
|
-
json(res, 400, { error: { code: 'invalid-action', message: 'action must be skip, reset, or
|
|
38
|
+
if (!action || !['skip', 'reset', 'pause', 'pin'].includes(action)) {
|
|
39
|
+
json(res, 400, { error: { code: 'invalid-action', message: 'action must be skip, reset, pause, or pin' } });
|
|
40
40
|
return;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -164,6 +164,30 @@ export function registerQuickActionRoutes(ctx, deps) {
|
|
|
164
164
|
});
|
|
165
165
|
return;
|
|
166
166
|
}
|
|
167
|
+
if (action === 'pin') {
|
|
168
|
+
const targetRef = typeof ref === 'string' && ref.trim().length > 0 ? ref.trim() : null;
|
|
169
|
+
if (targetRef && targetPool.refs && !targetPool.refs.includes(targetRef)) {
|
|
170
|
+
json(res, 400, { error: { code: 'invalid-key', message: `Key ${targetRef} not in pool` } });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
targetPool.pinnedRef = targetRef;
|
|
174
|
+
if (targetRef) {
|
|
175
|
+
targetPool.state.lastUsed = targetRef;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof pushEvent === 'function') {
|
|
179
|
+
pushEvent(targetPool, targetRef || '*', targetRef ? 'KEY_PINNED' : 'KEY_UNPINNED', 0);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
json(res, 200, {
|
|
183
|
+
ok: true,
|
|
184
|
+
action: 'pin',
|
|
185
|
+
provider,
|
|
186
|
+
pinnedRef: targetPool.pinnedRef,
|
|
187
|
+
pinnedTail: targetPool.pinnedRef ? keyTail(targetPool.pinnedRef) : null,
|
|
188
|
+
});
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
167
191
|
},
|
|
168
192
|
}));
|
|
169
193
|
}
|
package/lib/routes/status.js
CHANGED
|
@@ -249,6 +249,17 @@ ctx.effect(() => getWebServer(ctx).register({
|
|
|
249
249
|
modelQuota: quotaAware ? { configured: true, exhaustedKeys: quotaExhausted } : null,
|
|
250
250
|
lastUsed: pool.state.lastUsed ?? null,
|
|
251
251
|
lastUsedTail: pool.state.lastUsed ? keyTail(pool.state.lastUsed) : null,
|
|
252
|
+
pinnedRef: pool.pinnedRef ?? null,
|
|
253
|
+
pinnedTail: pool.pinnedRef ? keyTail(pool.pinnedRef) : null,
|
|
254
|
+
promptCacheHits: pool.state.promptCacheHits ?? 0,
|
|
255
|
+
keys: pool.refs.map((r) => ({
|
|
256
|
+
ref: r,
|
|
257
|
+
tail: keyTail(r),
|
|
258
|
+
active: pool.state.lastUsed === r,
|
|
259
|
+
pinned: pool.pinnedRef === r,
|
|
260
|
+
paused: isKeyPaused(pool, r),
|
|
261
|
+
cooling: (pool.state.failedUntil?.get(r) ?? 0) > now,
|
|
262
|
+
})),
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
let circuitOpenAny = false;
|
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.89",
|
|
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": [
|