@moguiyu/dsh-tavily 0.3.2 → 0.3.4
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 +2 -1
- package/lib/client.js +324 -27
- package/package.json +4 -3
- package/src/backend.js +110 -11
- package/src/client.js +324 -27
- package/src/index.js +2 -1
- package/src/lib.js +123 -3
- package/src/tools.js +178 -6
package/README.md
CHANGED
|
@@ -29,4 +29,5 @@ which is exactly how the old tool-level switch broke. Both old packages are depr
|
|
|
29
29
|
There is no tool-level on/off switch. Composing the plugin registers the tools, and the Plugins
|
|
30
30
|
page's own bundle toggle is the only on/off.
|
|
31
31
|
|
|
32
|
-
See the [workspace README](
|
|
32
|
+
See the [workspace README](https://github.com/moguiyu/dsh-tavily#readme) for install and
|
|
33
|
+
configuration.
|
package/lib/client.js
CHANGED
|
@@ -6,11 +6,18 @@ window.__ModuleLoader__.load({
|
|
|
6
6
|
const inject = ['slots']
|
|
7
7
|
|
|
8
8
|
const STRATEGIES = [
|
|
9
|
-
{ id: 'rotate', label: 'Round-robin', hint: 'Use keys in turn; on HTTP 401/429 the next key is tried automatically.' },
|
|
9
|
+
{ id: 'rotate', label: 'Round-robin', hint: 'Use keys in turn; on HTTP 401/429/432/433 the next key is tried automatically.' },
|
|
10
10
|
{ id: 'low-usage-first', label: 'Lowest usage first', hint: 'Re-orders keys by current Tavily usage, least-used first. The first key becomes primary.' },
|
|
11
|
-
{ id: 'high-usage-first', label: 'Highest usage first', hint: 'Re-orders keys by current Tavily usage, most-used first. The first key becomes primary.' }
|
|
11
|
+
{ id: 'high-usage-first', label: 'Highest usage first', hint: 'Re-orders keys by current Tavily usage, most-used first. The first key becomes primary.' },
|
|
12
|
+
{
|
|
13
|
+
id: 'load-balance',
|
|
14
|
+
label: 'Load balance',
|
|
15
|
+
hint: 'Keeps at most the set number of requests in flight per Tavily account and spreads concurrent calls across accounts, so one free account is never hit in parallel. Accounts are inferred from live Tavily usage data — pin a key below if that is wrong. Keys on one account share its 100 requests/minute.'
|
|
16
|
+
}
|
|
12
17
|
]
|
|
13
18
|
|
|
19
|
+
const ACCOUNT_CAPS = [1, 2, 3, 4, 5, 6, 7, 8]
|
|
20
|
+
|
|
14
21
|
const ICONS = {
|
|
15
22
|
eye: ['M1.5 8C1.5 8 4 3.5 8 3.5S14.5 8 14.5 8 12 12.5 8 12.5 1.5 8 1.5 8Z', 'M8 5.8a2.2 2.2 0 1 0 0 4.4 2.2 2.2 0 0 0 0-4.4Z'],
|
|
16
23
|
eyeOff: ['M1.5 8C1.5 8 4 3.5 8 3.5c1.6 0 3 .5 4.1 1.2', 'M14.5 8C14.5 8 12 12.5 8 12.5c-1.6 0-3-.5-4.1-1.2', 'M2 2l12 12', 'M8 5.8a2.2 2.2 0 0 1 2.2 2.2'],
|
|
@@ -32,6 +39,113 @@ window.__ModuleLoader__.load({
|
|
|
32
39
|
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate())
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
function formatClock(iso) {
|
|
43
|
+
if (!iso) return null
|
|
44
|
+
const d = new Date(iso)
|
|
45
|
+
if (isNaN(d.getTime())) return null
|
|
46
|
+
const pad = (n) => String(n).padStart(2, '0')
|
|
47
|
+
return pad(d.getHours()) + ':' + pad(d.getMinutes())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// `/api/tavily-usage` asks Tavily for every key's counters, so it can take a
|
|
51
|
+
// while. Two rules follow: the cell must never look *empty* while that is in
|
|
52
|
+
// flight, and the last values already shown must survive a page reload. Only
|
|
53
|
+
// masked names and counters are stored — never key material.
|
|
54
|
+
const USAGE_SNAPSHOT_KEY = 'dsh-tavily:usage-snapshot:v1'
|
|
55
|
+
const RING_GROWTH_MS = 650
|
|
56
|
+
|
|
57
|
+
function usageStorage() {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof window === 'undefined' || window === null || !window.localStorage) return null
|
|
60
|
+
return window.localStorage
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readUsageSnapshot() {
|
|
67
|
+
const storage = usageStorage()
|
|
68
|
+
if (storage === null) return null
|
|
69
|
+
try {
|
|
70
|
+
const parsed = JSON.parse(storage.getItem(USAGE_SNAPSHOT_KEY))
|
|
71
|
+
if (parsed === null || typeof parsed !== 'object') return null
|
|
72
|
+
if (parsed.version !== 1) return null
|
|
73
|
+
if (parsed.rows === null || typeof parsed.rows !== 'object') return null
|
|
74
|
+
return parsed
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeUsageSnapshot(data) {
|
|
81
|
+
const storage = usageStorage()
|
|
82
|
+
if (storage === null) return
|
|
83
|
+
try {
|
|
84
|
+
const perKey = data !== null && data !== undefined && Array.isArray(data.perKey) ? data.perKey : []
|
|
85
|
+
const rows = {}
|
|
86
|
+
for (const row of perKey) {
|
|
87
|
+
if (row === null || typeof row !== 'object' || typeof row.masked !== 'string') continue
|
|
88
|
+
rows[row.masked] = {
|
|
89
|
+
ok: row.ok === true,
|
|
90
|
+
usage: typeof row.usage === 'number' ? row.usage : null,
|
|
91
|
+
planUsage: typeof row.planUsage === 'number' ? row.planUsage : null,
|
|
92
|
+
planLimit: typeof row.planLimit === 'number' ? row.planLimit : null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (Object.keys(rows).length === 0) return
|
|
96
|
+
storage.setItem(USAGE_SNAPSHOT_KEY, JSON.stringify({ version: 1, at: new Date().toISOString(), rows: rows }))
|
|
97
|
+
} catch (error) {
|
|
98
|
+
/* storage is best-effort; the card still works without it */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function usagePercent(row) {
|
|
103
|
+
if (row === null || row === undefined || row.ok !== true) return null
|
|
104
|
+
if (row.planLimit === null || row.planLimit === undefined || row.planLimit <= 0) return null
|
|
105
|
+
if (row.planUsage === null || row.planUsage === undefined) return null
|
|
106
|
+
return Math.min(100, Math.round((row.planUsage / row.planLimit) * 100))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Count a number up to `target` instead of swapping it. Returns the value to
|
|
111
|
+
* paint now. A first-ever value (or a non-number) is set instantly: there is
|
|
112
|
+
* nothing to grow from.
|
|
113
|
+
*/
|
|
114
|
+
function useAnimatedNumber(target, durationMs) {
|
|
115
|
+
const [display, setDisplay] = react.useState(target)
|
|
116
|
+
const displayRef = react.useRef(target)
|
|
117
|
+
const frameRef = react.useRef(0)
|
|
118
|
+
react.useEffect(() => {
|
|
119
|
+
const from = typeof displayRef.current === 'number' ? displayRef.current : null
|
|
120
|
+
const settle = (value) => { displayRef.current = value; setDisplay(value) }
|
|
121
|
+
if (typeof target !== 'number' || from === null || from === target) {
|
|
122
|
+
settle(typeof target === 'number' ? target : null)
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
if (typeof requestAnimationFrame !== 'function') {
|
|
126
|
+
settle(target)
|
|
127
|
+
return undefined
|
|
128
|
+
}
|
|
129
|
+
let started = null
|
|
130
|
+
const tick = (now) => {
|
|
131
|
+
const stamp = typeof now === 'number' ? now : Date.now()
|
|
132
|
+
if (started === null) started = stamp
|
|
133
|
+
const elapsed = durationMs > 0 ? (stamp - started) / durationMs : 1
|
|
134
|
+
const t = Math.min(1, Math.max(0, elapsed))
|
|
135
|
+
const eased = 1 - Math.pow(1 - t, 3)
|
|
136
|
+
const next = t >= 1 ? target : Math.round(from + (target - from) * eased)
|
|
137
|
+
displayRef.current = next
|
|
138
|
+
setDisplay(next)
|
|
139
|
+
if (t < 1) frameRef.current = requestAnimationFrame(tick)
|
|
140
|
+
}
|
|
141
|
+
frameRef.current = requestAnimationFrame(tick)
|
|
142
|
+
return () => {
|
|
143
|
+
if (typeof cancelAnimationFrame === 'function' && frameRef.current) cancelAnimationFrame(frameRef.current)
|
|
144
|
+
}
|
|
145
|
+
}, [target, durationMs])
|
|
146
|
+
return display
|
|
147
|
+
}
|
|
148
|
+
|
|
35
149
|
function SvgIcon({ name, size, style }) {
|
|
36
150
|
const paths = ICONS[name] || []
|
|
37
151
|
return react.createElement('svg', { width: size || 14, height: size || 14, viewBox: '0 0 16 16', fill: 'none', stroke: 'currentColor', strokeWidth: 1.4, strokeLinecap: 'round', strokeLinejoin: 'round', 'aria-hidden': 'true', style: style },
|
|
@@ -51,17 +165,54 @@ window.__ModuleLoader__.load({
|
|
|
51
165
|
}, react.createElement(SvgIcon, { name: icon, size: 13 }))
|
|
52
166
|
}
|
|
53
167
|
|
|
54
|
-
function UsageCircle({ percent,
|
|
168
|
+
function UsageCircle({ percent, number, loading, stale, staleAt, onClick }) {
|
|
55
169
|
const radius = 15
|
|
56
170
|
const circumference = 2 * Math.PI * radius
|
|
57
171
|
const pct = percent != null ? Math.min(100, Math.max(0, percent)) : 0
|
|
58
|
-
const dim =
|
|
59
|
-
|
|
60
|
-
|
|
172
|
+
const dim = 44
|
|
173
|
+
const animated = useAnimatedNumber(percent != null ? percent : (number != null ? number : null), RING_GROWTH_MS)
|
|
174
|
+
const shown = animated != null ? animated : (number != null ? number : null)
|
|
175
|
+
const offset = circumference - (pct / 100) * circumference
|
|
176
|
+
const label = percent != null
|
|
177
|
+
? (shown != null ? shown + '%' : '—')
|
|
178
|
+
: (shown != null ? String(shown) : '—')
|
|
179
|
+
const clock = formatClock(staleAt)
|
|
180
|
+
const title = loading
|
|
181
|
+
? (stale && clock !== null ? 'Updating usage… showing the values loaded at ' + clock : 'Updating usage…')
|
|
182
|
+
: (stale && clock !== null ? 'Showing the values loaded at ' + clock + ' — click to refresh' : 'Reload usage')
|
|
183
|
+
return react.createElement('button', {
|
|
184
|
+
type: 'button',
|
|
185
|
+
title: title,
|
|
186
|
+
'aria-label': 'Reload usage',
|
|
187
|
+
'aria-busy': loading ? 'true' : 'false',
|
|
188
|
+
'data-loading': loading ? 'true' : 'false',
|
|
189
|
+
'data-stale': stale ? 'true' : 'false',
|
|
190
|
+
onClick: onClick,
|
|
191
|
+
style: { position: 'relative', width: dim, height: dim, border: 'none', background: 'transparent', padding: 0, cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }
|
|
192
|
+
},
|
|
193
|
+
react.createElement('svg', { width: dim, height: dim, viewBox: '0 0 40 40', className: loading ? 'dts-ring dts-ring-loading' : 'dts-ring' },
|
|
61
194
|
react.createElement('circle', { cx: 20, cy: 20, r: radius, fill: 'none', stroke: 'var(--dsw-alias-border-l2)', strokeWidth: 4 }),
|
|
62
|
-
react.createElement('circle', {
|
|
195
|
+
react.createElement('circle', {
|
|
196
|
+
className: 'dts-ring-progress',
|
|
197
|
+
cx: 20, cy: 20, r: radius, fill: 'none',
|
|
198
|
+
stroke: 'var(--dsw-alias-state-success-primary)', strokeWidth: 4, strokeLinecap: 'round',
|
|
199
|
+
strokeDasharray: circumference,
|
|
200
|
+
strokeDashoffset: offset,
|
|
201
|
+
transform: 'rotate(-90 20 20)',
|
|
202
|
+
// The offset is carried as a style value as well as an attribute:
|
|
203
|
+
// presentation attributes alone do not reliably transition.
|
|
204
|
+
style: { strokeDashoffset: offset, transition: 'stroke-dashoffset ' + RING_GROWTH_MS + 'ms cubic-bezier(.22,.61,.36,1)' }
|
|
205
|
+
}),
|
|
206
|
+
loading && react.createElement('g', { className: 'dts-ring-sweep', style: { transformOrigin: '20px 20px' } },
|
|
207
|
+
react.createElement('circle', {
|
|
208
|
+
cx: 20, cy: 20, r: radius, fill: 'none',
|
|
209
|
+
stroke: 'var(--dsw-alias-state-success-primary)', strokeWidth: 4, strokeLinecap: 'round',
|
|
210
|
+
strokeDasharray: (circumference * 0.22) + ' ' + (circumference * 0.78),
|
|
211
|
+
opacity: 0.45
|
|
212
|
+
})
|
|
213
|
+
)
|
|
63
214
|
),
|
|
64
|
-
react.createElement('span', { style: { position: 'absolute', fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, label
|
|
215
|
+
react.createElement('span', { className: 'dts-ring-label', style: { position: 'absolute', fontSize: 11, fontWeight: 600, color: 'var(--dsw-alias-label-primary)' } }, label)
|
|
65
216
|
)
|
|
66
217
|
}
|
|
67
218
|
|
|
@@ -79,39 +230,98 @@ window.__ModuleLoader__.load({
|
|
|
79
230
|
const [notice, setNotice] = react.useState(null)
|
|
80
231
|
const [usage, setUsage] = react.useState(null)
|
|
81
232
|
const [usageError, setUsageError] = react.useState(null)
|
|
233
|
+
const [usageLoading, setUsageLoading] = react.useState(true)
|
|
234
|
+
const [snapshot, setSnapshot] = react.useState(() => readUsageSnapshot())
|
|
82
235
|
|
|
83
236
|
const refresh = react.useCallback(async () => {
|
|
237
|
+
setUsageLoading(true)
|
|
238
|
+
// The two endpoints are independent and the usage one is the slow half,
|
|
239
|
+
// so they run together: the keys must not wait for Tavily's counters.
|
|
240
|
+
const keys = (async () => {
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch('/api/tavily-manager', { cache: 'no-store' })
|
|
243
|
+
const data = await response.json()
|
|
244
|
+
if (data.ok) { setServer(data); setStrategy(data.strategy) } else { setLoadError(data.error || 'Failed to load keys') }
|
|
245
|
+
} catch (error) {
|
|
246
|
+
setLoadError(String(error && error.message ? error.message : error))
|
|
247
|
+
}
|
|
248
|
+
})()
|
|
249
|
+
const counters = (async () => {
|
|
250
|
+
try {
|
|
251
|
+
const response = await fetch('/api/tavily-usage', { cache: 'no-store' })
|
|
252
|
+
const data = await response.json()
|
|
253
|
+
setUsage(data)
|
|
254
|
+
setUsageError(null)
|
|
255
|
+
if (data.ok) {
|
|
256
|
+
writeUsageSnapshot(data)
|
|
257
|
+
setSnapshot(readUsageSnapshot())
|
|
258
|
+
}
|
|
259
|
+
} catch (error) {
|
|
260
|
+
setUsageError(String(error && error.message ? error.message : error))
|
|
261
|
+
} finally {
|
|
262
|
+
setUsageLoading(false)
|
|
263
|
+
}
|
|
264
|
+
})()
|
|
265
|
+
await Promise.all([keys, counters])
|
|
266
|
+
}, [])
|
|
267
|
+
|
|
268
|
+
react.useEffect(() => { refresh() }, [refresh])
|
|
269
|
+
|
|
270
|
+
const saveStrategy = async (next) => {
|
|
271
|
+
setBusy(true)
|
|
272
|
+
setNotice(null)
|
|
84
273
|
try {
|
|
85
|
-
const response = await fetch('/api/tavily-manager', {
|
|
274
|
+
const response = await fetch('/api/tavily-manager', {
|
|
275
|
+
method: 'POST',
|
|
276
|
+
headers: { 'content-type': 'application/json' },
|
|
277
|
+
body: JSON.stringify({ add: [], remove: [], strategy: next })
|
|
278
|
+
})
|
|
86
279
|
const data = await response.json()
|
|
87
|
-
if (data.ok) {
|
|
280
|
+
if (!data.ok) { setNotice({ error: data.error || 'Strategy save failed' }); return }
|
|
281
|
+
setNotice({ ok: 'Strategy saved — effective on the next search.' })
|
|
282
|
+
await refresh()
|
|
88
283
|
} catch (error) {
|
|
89
|
-
|
|
284
|
+
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
285
|
+
} finally {
|
|
286
|
+
setBusy(false)
|
|
90
287
|
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// The account grouping is inferred from Tavily's usage counters, which is
|
|
291
|
+
// the only account-level signal the API offers; these two handlers are the
|
|
292
|
+
// human override for when the inference is wrong or the cap needs raising.
|
|
293
|
+
const saveAccount = async (masked, choice) => {
|
|
294
|
+
setBusy(true)
|
|
295
|
+
setNotice(null)
|
|
91
296
|
try {
|
|
92
|
-
const response = await fetch('/api/tavily-
|
|
297
|
+
const response = await fetch('/api/tavily-manager', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
headers: { 'content-type': 'application/json' },
|
|
300
|
+
body: JSON.stringify({ add: [], remove: [], strategy, accounts: { [masked]: choice } })
|
|
301
|
+
})
|
|
93
302
|
const data = await response.json()
|
|
94
|
-
|
|
95
|
-
|
|
303
|
+
if (!data.ok) { setNotice({ error: data.error || 'Account change failed' }); return }
|
|
304
|
+
setNotice({ ok: choice === 'auto' ? 'Account detected automatically again.' : 'Account pinned for this key.' })
|
|
305
|
+
await refresh()
|
|
96
306
|
} catch (error) {
|
|
97
|
-
|
|
307
|
+
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
308
|
+
} finally {
|
|
309
|
+
setBusy(false)
|
|
98
310
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
react.useEffect(() => { refresh() }, [refresh])
|
|
311
|
+
}
|
|
102
312
|
|
|
103
|
-
const
|
|
313
|
+
const saveCap = async (value) => {
|
|
104
314
|
setBusy(true)
|
|
105
315
|
setNotice(null)
|
|
106
316
|
try {
|
|
107
317
|
const response = await fetch('/api/tavily-manager', {
|
|
108
318
|
method: 'POST',
|
|
109
319
|
headers: { 'content-type': 'application/json' },
|
|
110
|
-
body: JSON.stringify({ add: [], remove: [], strategy:
|
|
320
|
+
body: JSON.stringify({ add: [], remove: [], strategy, accountCap: Number(value) })
|
|
111
321
|
})
|
|
112
322
|
const data = await response.json()
|
|
113
|
-
if (!data.ok) { setNotice({ error: data.error || '
|
|
114
|
-
setNotice({ ok: '
|
|
323
|
+
if (!data.ok) { setNotice({ error: data.error || 'Cap save failed' }); return }
|
|
324
|
+
setNotice({ ok: 'Concurrency cap saved — effective on the next search.' })
|
|
115
325
|
await refresh()
|
|
116
326
|
} catch (error) {
|
|
117
327
|
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
@@ -239,12 +449,45 @@ window.__ModuleLoader__.load({
|
|
|
239
449
|
|
|
240
450
|
const headStyle = { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l2)', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' }
|
|
241
451
|
const cellStyle = { padding: '8px', verticalAlign: 'top' }
|
|
452
|
+
const snapshotRows = snapshot !== null && typeof snapshot === 'object' && snapshot.rows !== null && typeof snapshot.rows === 'object'
|
|
453
|
+
? snapshot.rows
|
|
454
|
+
: null
|
|
455
|
+
const snapshotAt = snapshot !== null && typeof snapshot === 'object' ? snapshot.at : null
|
|
456
|
+
|
|
457
|
+
// The usage response carries the freshest inference (it is the route that
|
|
458
|
+
// runs it); the manager payload is the fallback, and the only source of the
|
|
459
|
+
// policy before the first usage answer arrives.
|
|
460
|
+
const accountsMap = usage !== null && usage.ok === true && usage.accounts !== null && typeof usage.accounts === 'object'
|
|
461
|
+
? usage.accounts
|
|
462
|
+
: (server !== null && server.accounts !== null && server.accounts !== undefined && typeof server.accounts === 'object' ? server.accounts : {})
|
|
463
|
+
const accountCap = usage !== null && usage.ok === true && typeof usage.accountCap === 'number'
|
|
464
|
+
? usage.accountCap
|
|
465
|
+
: (server !== null && typeof server.accountCap === 'number' ? server.accountCap : 1)
|
|
466
|
+
const accountOf = (masked) => {
|
|
467
|
+
const entry = accountsMap[masked]
|
|
468
|
+
const group = entry !== null && entry !== undefined && typeof entry === 'object' && typeof entry.group === 'string' ? entry.group : null
|
|
469
|
+
return {
|
|
470
|
+
group,
|
|
471
|
+
own: group !== null && group.indexOf('own:') === 0,
|
|
472
|
+
manual: entry !== null && entry !== undefined && entry.manual === true,
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const accountLabel = (group) => {
|
|
476
|
+
const name = /^acct-(\d+)$/.exec(group)
|
|
477
|
+
return name === null ? group : 'Account ' + name[1]
|
|
478
|
+
}
|
|
479
|
+
const groupChoices = []
|
|
480
|
+
for (const key of (server !== null ? server.keys : [])) {
|
|
481
|
+
const { group, own } = accountOf(key.masked)
|
|
482
|
+
if (group === null || own) continue
|
|
483
|
+
if (!groupChoices.includes(group)) groupChoices.push(group)
|
|
484
|
+
}
|
|
242
485
|
|
|
243
486
|
// The Plugins page draws the title, the icon, and the crumb — the
|
|
244
487
|
// `plugins.bundle.config` contract puts the form alone in the entry — so
|
|
245
488
|
// this renders no card chrome, no second heading, and no collapse.
|
|
246
489
|
return react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16, minWidth: 0 } },
|
|
247
|
-
react.createElement('style', null, '.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}'),
|
|
490
|
+
react.createElement('style', null, '.dts-icon-btn:hover{background:var(--dsw-alias-interactive-bg-hover)}.dts-icon-btn:disabled{opacity:.4;cursor:default}.dts-icon-btn-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger)}@keyframes dts-ring-spin{to{transform:rotate(360deg)}}@keyframes dts-ring-pulse{0%,100%{opacity:1}50%{opacity:.45}}.dts-ring-loading{animation:dts-ring-pulse 1.6s ease-in-out infinite}.dts-ring-sweep{animation:dts-ring-spin 1.1s linear infinite}@media (prefers-reduced-motion: reduce){.dts-ring-loading,.dts-ring-sweep{animation:none}.dts-ring-progress{transition:none!important}}'),
|
|
248
491
|
react.createElement(react.Fragment, null,
|
|
249
492
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
250
493
|
'All keys are listed below; the green dot marks the first (primary) key. The tavily_search tool uses all keys according to the strategy. Built-in web_search is unaffected.'
|
|
@@ -258,6 +501,7 @@ window.__ModuleLoader__.load({
|
|
|
258
501
|
react.createElement('thead', null,
|
|
259
502
|
react.createElement('tr', null,
|
|
260
503
|
react.createElement('th', { style: headStyle }, 'Key'),
|
|
504
|
+
react.createElement('th', { style: headStyle }, 'Account'),
|
|
261
505
|
react.createElement('th', { style: headStyle }, 'Usage'),
|
|
262
506
|
react.createElement('th', { style: headStyle }, 'Saved'),
|
|
263
507
|
react.createElement('th', { style: headStyle }, 'Actions')
|
|
@@ -269,12 +513,27 @@ window.__ModuleLoader__.load({
|
|
|
269
513
|
const isRemoved = removing[masked] === true
|
|
270
514
|
const isReplacing = replacing[masked] === true
|
|
271
515
|
const isRevealed = typeof revealed[masked] === 'string'
|
|
272
|
-
|
|
516
|
+
// Fresh counters win; until they arrive the last stored row is
|
|
517
|
+
// shown so the cell reads as "updating", never as "empty".
|
|
518
|
+
const freshRow = usage !== null && usage.ok === true
|
|
273
519
|
? (usage.perKey.find((row) => row.masked === masked) || usage.perKey[index])
|
|
274
520
|
: null
|
|
275
|
-
const
|
|
276
|
-
?
|
|
521
|
+
const storedRow = snapshotRows !== null && Object.prototype.hasOwnProperty.call(snapshotRows, masked)
|
|
522
|
+
? snapshotRows[masked]
|
|
523
|
+
: null
|
|
524
|
+
const usageRow = freshRow !== null && freshRow !== undefined ? freshRow : storedRow
|
|
525
|
+
const pct = usagePercent(usageRow)
|
|
526
|
+
const rawNumber = pct === null && usageRow !== null && usageRow !== undefined && usageRow.ok === true && usageRow.planUsage != null
|
|
527
|
+
? usageRow.planUsage
|
|
277
528
|
: null
|
|
529
|
+
const isStale = (freshRow === null || freshRow === undefined) && storedRow !== null
|
|
530
|
+
// Which Tavily account this key belongs to, and the override.
|
|
531
|
+
const account = accountOf(masked)
|
|
532
|
+
const currentChoice = account.manual ? (account.own ? 'own' : account.group) : 'auto'
|
|
533
|
+
const accountChoices = ['auto'].concat(groupChoices).concat(['own'])
|
|
534
|
+
if (currentChoice !== 'auto' && !accountChoices.includes(currentChoice)) {
|
|
535
|
+
accountChoices.splice(1, 0, currentChoice)
|
|
536
|
+
}
|
|
278
537
|
return react.createElement('tr', { key: masked, style: { borderBottom: '1px solid var(--dsw-alias-border-l2)', opacity: isRemoved ? 0.45 : 1 } },
|
|
279
538
|
react.createElement('td', { style: Object.assign({}, cellStyle, { fontFamily: 'var(--ds-font-family-code, monospace)', fontSize: 12, wordBreak: 'break-all' }) },
|
|
280
539
|
isReplacing
|
|
@@ -294,7 +553,34 @@ window.__ModuleLoader__.load({
|
|
|
294
553
|
)
|
|
295
554
|
),
|
|
296
555
|
react.createElement('td', { style: cellStyle },
|
|
297
|
-
react.createElement(
|
|
556
|
+
react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
557
|
+
react.createElement('span', {
|
|
558
|
+
className: 'dts-account-chip',
|
|
559
|
+
style: { fontSize: 11, color: account.group === null ? 'var(--dsw-alias-label-tertiary)' : 'var(--dsw-alias-label-secondary)' }
|
|
560
|
+
}, account.group === null ? 'Ungrouped' : (account.own ? 'Separate' : accountLabel(account.group))),
|
|
561
|
+
react.createElement('select', {
|
|
562
|
+
className: 'dts-account',
|
|
563
|
+
'data-masked': masked,
|
|
564
|
+
title: 'Which Tavily account this key counts against. Auto follows the grouping inferred from usage; Separate gives the key its own account, so it is never held back by another key.',
|
|
565
|
+
value: currentChoice,
|
|
566
|
+
disabled: busy,
|
|
567
|
+
onChange: (event) => saveAccount(masked, event.target.value),
|
|
568
|
+
style: { height: 26, borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', fontSize: 11, maxWidth: 130 }
|
|
569
|
+
},
|
|
570
|
+
accountChoices.map((choice) => react.createElement('option', { key: choice, value: choice },
|
|
571
|
+
choice === 'auto' ? 'Auto' : (choice === 'own' ? 'Separate' : accountLabel(choice))))
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
),
|
|
575
|
+
react.createElement('td', { style: cellStyle },
|
|
576
|
+
react.createElement(UsageCircle, {
|
|
577
|
+
percent: pct,
|
|
578
|
+
number: rawNumber,
|
|
579
|
+
loading: usageLoading,
|
|
580
|
+
stale: isStale,
|
|
581
|
+
staleAt: snapshotAt,
|
|
582
|
+
onClick: refresh
|
|
583
|
+
})
|
|
298
584
|
),
|
|
299
585
|
react.createElement('td', { style: Object.assign({}, cellStyle, { color: 'var(--dsw-alias-label-tertiary)', whiteSpace: 'nowrap' }) }, formatDate(key.savedAt)),
|
|
300
586
|
react.createElement('td', { style: cellStyle },
|
|
@@ -335,6 +621,7 @@ window.__ModuleLoader__.load({
|
|
|
335
621
|
),
|
|
336
622
|
react.createElement('td', { style: cellStyle }, '—'),
|
|
337
623
|
react.createElement('td', { style: cellStyle }, '—'),
|
|
624
|
+
react.createElement('td', { style: cellStyle }, '—'),
|
|
338
625
|
react.createElement('td', { style: cellStyle },
|
|
339
626
|
react.createElement('span', { style: { display: 'inline-flex', gap: 2 } },
|
|
340
627
|
react.createElement(IconButton, { icon: 'check', title: 'Save key', onClick: () => saveAdd(item), disabled: busy || item.value.trim().length === 0 }),
|
|
@@ -351,11 +638,21 @@ window.__ModuleLoader__.load({
|
|
|
351
638
|
react.createElement('div', { style: { display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' } },
|
|
352
639
|
react.createElement('label', { style: { fontWeight: 500, fontSize: 13 } }, 'Key usage strategy'),
|
|
353
640
|
react.createElement('select', {
|
|
641
|
+
className: 'dts-strategy',
|
|
354
642
|
style: { maxWidth: 280, height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
|
|
355
643
|
value: strategy,
|
|
356
644
|
onChange: (event) => saveStrategy(event.target.value),
|
|
357
645
|
disabled: busy
|
|
358
646
|
}, STRATEGIES.map((option) => react.createElement('option', { key: option.id, value: option.id }, option.label))),
|
|
647
|
+
strategy === 'load-balance' && react.createElement('label', { style: { fontWeight: 500, fontSize: 13, marginLeft: 8 } }, 'Per account'),
|
|
648
|
+
strategy === 'load-balance' && react.createElement('select', {
|
|
649
|
+
className: 'dts-account-cap',
|
|
650
|
+
title: 'How many requests may be in flight for one Tavily account at a time. One suits a free key.',
|
|
651
|
+
style: { height: 32, border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8, background: 'var(--dsw-alias-bg-layer-1)', color: 'var(--dsw-alias-label-primary)', padding: '0 10px', fontSize: 13 },
|
|
652
|
+
value: String(accountCap),
|
|
653
|
+
onChange: (event) => saveCap(event.target.value),
|
|
654
|
+
disabled: busy
|
|
655
|
+
}, ACCOUNT_CAPS.map((n) => react.createElement('option', { key: n, value: String(n) }, String(n) + (n === 1 ? ' request' : ' requests')))),
|
|
359
656
|
notice !== null && react.createElement('span', { style: { fontSize: 13, color: notice.ok ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, notice.ok ? notice.ok : String(notice.error))
|
|
360
657
|
),
|
|
361
658
|
react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moguiyu/dsh-tavily",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Advanced Tavily model tools for DeepSeek Harness (tavily_search, tavily_extract, tavily_map, tavily_crawl) with a key and usage management card on the Plugins page. The built-in web_search is never replaced.",
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "Advanced Tavily model tools for DeepSeek Harness (tavily_search, tavily_extract, tavily_map, tavily_crawl) with multi-key rotation, failover and a per-account in-flight cap, plus a key and usage management card on the Plugins page. The built-in web_search is never replaced. | DSH Tavily 可选高级搜索工具:多 Key 轮换、故障转移与按账号并发上限,插件配置页含密钥与用量管理卡片;内置 web_search 永不替换。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -64,7 +64,8 @@
|
|
|
64
64
|
],
|
|
65
65
|
"repository": {
|
|
66
66
|
"type": "git",
|
|
67
|
-
"url": "https://github.com/moguiyu/dsh-tavily.git"
|
|
67
|
+
"url": "git+https://github.com/moguiyu/dsh-tavily.git",
|
|
68
|
+
"directory": "packages/dsh-tavily"
|
|
68
69
|
},
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"@deepseek-ai/cordis": "^4.0.1",
|