@moguiyu/dsh-tavily 0.2.8 → 0.3.1
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 +21 -21
- package/lib/client.js +29 -118
- package/package.json +3 -12
- package/src/backend.js +376 -0
- package/src/client.js +29 -118
- package/src/index.js +30 -157
- package/src/lib.js +65 -0
- package/src/tools.js +632 -0
package/src/backend.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes half of `@moguiyu/dsh-tavily`: key and usage management on the
|
|
3
|
+
* harness webServer (`/api/tavily-usage`, `/api/tavily-manager`). The
|
|
4
|
+
* built-in `web_search` tool is never replaced — no web search provider is
|
|
5
|
+
* registered and `web.searchProvider` is never touched.
|
|
6
|
+
*
|
|
7
|
+
* There is no `/api/tavily-tool` switch route: the tool-level on/off is gone
|
|
8
|
+
* (see `tools.js`). Key management is not a capability gate — it only edits
|
|
9
|
+
* the credentials the tools read.
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
|
14
|
+
import { STRATEGIES, isValidStrategy, maskValue, parseKeyList, orderKeys, readJsonFile } from './lib.js'
|
|
15
|
+
|
|
16
|
+
const USAGE_TTL_MS = 60000
|
|
17
|
+
const usageCache = new Map()
|
|
18
|
+
|
|
19
|
+
const MANAGER_STATE = 'tavily-manager.json'
|
|
20
|
+
const LEGACY_STATE = 'tavily-settings.json'
|
|
21
|
+
const REFS = ['TAVILY_API_KEYS', 'TAVILY_API_KEY']
|
|
22
|
+
|
|
23
|
+
function statePath(file) {
|
|
24
|
+
return join(resolveDshHome(), file)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readState(file) {
|
|
28
|
+
return readJsonFile(statePath(file), {})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeState(file, state) {
|
|
32
|
+
writeFileSync(statePath(file), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readBody(req) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const chunks = []
|
|
38
|
+
req.on('data', (chunk) => chunks.push(chunk))
|
|
39
|
+
req.on('end', () => {
|
|
40
|
+
const text = Buffer.concat(chunks).toString('utf8')
|
|
41
|
+
try {
|
|
42
|
+
resolve(text.length > 0 ? JSON.parse(text) : {})
|
|
43
|
+
} catch {
|
|
44
|
+
reject(new Error('invalid JSON body'))
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
req.on('error', reject)
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function fetchUsageDetailsFor(key) {
|
|
52
|
+
const cached = usageCache.get(key)
|
|
53
|
+
if (cached !== undefined && Date.now() - cached.at < USAGE_TTL_MS) return cached.details
|
|
54
|
+
try {
|
|
55
|
+
const controller = new AbortController()
|
|
56
|
+
const timer = setTimeout(() => controller.abort(), 15000)
|
|
57
|
+
let response
|
|
58
|
+
try {
|
|
59
|
+
response = await fetch('https://api.tavily.com/usage', {
|
|
60
|
+
headers: { authorization: 'Bearer ' + key, accept: 'application/json' },
|
|
61
|
+
signal: controller.signal,
|
|
62
|
+
})
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer)
|
|
65
|
+
}
|
|
66
|
+
if (!response.ok) return null
|
|
67
|
+
const json = await response.json()
|
|
68
|
+
const account = json !== null && typeof json === 'object' ? json.account : undefined
|
|
69
|
+
const keyUsage = json !== null && typeof json === 'object' ? json.key : undefined
|
|
70
|
+
const usage = keyUsage !== undefined && typeof keyUsage.usage === 'number'
|
|
71
|
+
? keyUsage.usage
|
|
72
|
+
: (account !== undefined && typeof account.plan_usage === 'number' ? account.plan_usage : null)
|
|
73
|
+
const planUsage = account !== undefined && typeof account.plan_usage === 'number'
|
|
74
|
+
? account.plan_usage
|
|
75
|
+
: (keyUsage !== undefined && typeof keyUsage.usage === 'number' ? keyUsage.usage : null)
|
|
76
|
+
const planLimit = account !== undefined && typeof account.plan_limit === 'number'
|
|
77
|
+
? account.plan_limit
|
|
78
|
+
: (keyUsage !== undefined && typeof keyUsage.limit === 'number' ? keyUsage.limit
|
|
79
|
+
: (keyUsage !== undefined && typeof keyUsage.plan_limit === 'number' ? keyUsage.plan_limit : null))
|
|
80
|
+
const currentPlan = account !== undefined && typeof account.plan === 'string'
|
|
81
|
+
? account.plan
|
|
82
|
+
: (keyUsage !== undefined && typeof keyUsage.plan === 'string' ? keyUsage.plan : null)
|
|
83
|
+
if (usage === null && planUsage === null) return null
|
|
84
|
+
const details = { usage, planUsage, planLimit, currentPlan }
|
|
85
|
+
usageCache.set(key, { at: Date.now(), details })
|
|
86
|
+
return details
|
|
87
|
+
} catch {
|
|
88
|
+
return null
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function fetchUsageFor(key) {
|
|
93
|
+
const details = await fetchUsageDetailsFor(key)
|
|
94
|
+
if (details === null) return null
|
|
95
|
+
if (details.planLimit !== null && details.planLimit > 0 && details.planUsage !== null) {
|
|
96
|
+
return (details.planUsage / details.planLimit) * 100
|
|
97
|
+
}
|
|
98
|
+
return details.usage !== null ? details.usage : details.planUsage
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Resolve one credential ref to its string value, or `null` when absent. */
|
|
102
|
+
async function resolveRef(credentials, ref) {
|
|
103
|
+
try {
|
|
104
|
+
const hit = await credentials.resolve(ref)
|
|
105
|
+
return hit !== undefined && hit !== null && typeof hit.value === 'string' ? hit.value : null
|
|
106
|
+
} catch {
|
|
107
|
+
return null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether a ref can be written here. The launching environment's variables are
|
|
113
|
+
* inherited READ-ONLY: `set`/`unset` throw for them (`describe().writable === false`).
|
|
114
|
+
* Assume writable when the service cannot say.
|
|
115
|
+
*/
|
|
116
|
+
async function refWritable(credentials, ref) {
|
|
117
|
+
try {
|
|
118
|
+
const info = await credentials.describe(ref)
|
|
119
|
+
return info === undefined || info === null || info.writable !== false
|
|
120
|
+
} catch {
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The managed key list, and where it came from.
|
|
127
|
+
*
|
|
128
|
+
* `TAVILY_API_KEYS` is AUTHORITATIVE. `TAVILY_API_KEY` — the derived primary — is
|
|
129
|
+
* read only as a fallback, for a deployment that sets nothing else. The two used to
|
|
130
|
+
* be unioned, and that was a bug with two faces: an environment-supplied
|
|
131
|
+
* `TAVILY_API_KEY` re-added its key on every read, so that key could never be deleted
|
|
132
|
+
* from the card; and every write then tried to re-sync the primary, which the
|
|
133
|
+
* launching environment owns, so the write threw AFTER the list had already changed.
|
|
134
|
+
*/
|
|
135
|
+
async function collectStoredKeys(credentials) {
|
|
136
|
+
const fromList = parseKeyList(await resolveRef(credentials, 'TAVILY_API_KEYS') ?? '')
|
|
137
|
+
if (fromList.length > 0) return { values: dedupe([...fromList]), source: 'list' }
|
|
138
|
+
const fromPrimary = parseKeyList(await resolveRef(credentials, 'TAVILY_API_KEY') ?? '')
|
|
139
|
+
return { values: dedupe([...fromPrimary]), source: fromPrimary.length > 0 ? 'primary' : 'list' }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function dedupe(values) {
|
|
143
|
+
const out = []
|
|
144
|
+
const seen = new Set()
|
|
145
|
+
for (const key of values) {
|
|
146
|
+
if (!seen.has(key)) {
|
|
147
|
+
seen.add(key)
|
|
148
|
+
out.push(key)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Keep the derived primary in step with the list, best effort. The tools read
|
|
156
|
+
* `TAVILY_API_KEYS`, so a primary the environment owns must never fail the request.
|
|
157
|
+
*/
|
|
158
|
+
async function syncPrimary(credentials, value) {
|
|
159
|
+
if (!(await refWritable(credentials, 'TAVILY_API_KEY'))) return false
|
|
160
|
+
try {
|
|
161
|
+
if (value === null) await credentials.unset('TAVILY_API_KEY')
|
|
162
|
+
else await credentials.set('TAVILY_API_KEY', value)
|
|
163
|
+
return true
|
|
164
|
+
} catch {
|
|
165
|
+
return false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register the Tavily key/usage routes on `ctx.webServer`:
|
|
171
|
+
* `/api/tavily-usage` and `/api/tavily-manager`.
|
|
172
|
+
*
|
|
173
|
+
* Every route is registered through `ctx.effect`, so the calling fiber owns
|
|
174
|
+
* it and unloading the row leaves nothing registered.
|
|
175
|
+
*/
|
|
176
|
+
export function installBackend(ctx) {
|
|
177
|
+
const credentials = ctx.get('credentials')
|
|
178
|
+
|
|
179
|
+
const send = (res, status, payload) => {
|
|
180
|
+
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' })
|
|
181
|
+
res.end(JSON.stringify(payload))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── key manager payload ────────────────────────────────────────────────────
|
|
185
|
+
async function buildManagerPayload(keys, source = 'list') {
|
|
186
|
+
const state = readState(MANAGER_STATE)
|
|
187
|
+
const keySavedAt = state.keySavedAt !== undefined && typeof state.keySavedAt === 'object' ? state.keySavedAt : {}
|
|
188
|
+
if (Object.keys(keySavedAt).length === 0 && credentials !== undefined) {
|
|
189
|
+
// Migrate dates recorded by the pre-0.2 layout (ref-keyed legacy file).
|
|
190
|
+
const legacy = readJsonFile(statePath(LEGACY_STATE), null)
|
|
191
|
+
if (legacy !== null && typeof legacy === 'object') {
|
|
192
|
+
for (const ref of REFS) {
|
|
193
|
+
const entry = legacy[ref]
|
|
194
|
+
if (entry !== null && typeof entry === 'object' && typeof entry.savedAt === 'string') {
|
|
195
|
+
try {
|
|
196
|
+
const hit = await credentials.resolve(ref)
|
|
197
|
+
if (hit !== undefined) keySavedAt[maskValue(hit.value)] = entry.savedAt
|
|
198
|
+
} catch {
|
|
199
|
+
/* keep going */
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const primaryMasked = keys.length > 0 ? maskValue(keys[0]) : null
|
|
206
|
+
const display = keys.map((value) => {
|
|
207
|
+
const masked = maskValue(value)
|
|
208
|
+
return {
|
|
209
|
+
masked,
|
|
210
|
+
savedAt: keySavedAt[masked] !== undefined ? keySavedAt[masked] : null,
|
|
211
|
+
}
|
|
212
|
+
}).sort((a, b) => {
|
|
213
|
+
if (a.savedAt === b.savedAt) return 0
|
|
214
|
+
if (a.savedAt === null) return 1
|
|
215
|
+
if (b.savedAt === null) return -1
|
|
216
|
+
return a.savedAt < b.savedAt ? -1 : 1
|
|
217
|
+
})
|
|
218
|
+
// The card needs to know what it may change. A ref inherited from the
|
|
219
|
+
// launching environment is read-only, and saying so beats a raw 500 from
|
|
220
|
+
// `set` after the list has already been written.
|
|
221
|
+
const keysWritable = credentials === undefined ? true : await refWritable(credentials, 'TAVILY_API_KEYS')
|
|
222
|
+
const primaryWritable = credentials === undefined ? true : await refWritable(credentials, 'TAVILY_API_KEY')
|
|
223
|
+
const removable = source === 'list' ? keysWritable : primaryWritable
|
|
224
|
+
return {
|
|
225
|
+
keys: display.map((entry) => ({
|
|
226
|
+
masked: entry.masked,
|
|
227
|
+
savedAt: entry.savedAt,
|
|
228
|
+
primary: entry.masked === primaryMasked,
|
|
229
|
+
removable,
|
|
230
|
+
})),
|
|
231
|
+
strategy: isValidStrategy(state.strategy) ? state.strategy : 'rotate',
|
|
232
|
+
source,
|
|
233
|
+
writable: { keys: keysWritable, primary: primaryWritable },
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ctx.effect(() => ctx.webServer.register({
|
|
238
|
+
kind: 'exact',
|
|
239
|
+
path: '/api/tavily-usage',
|
|
240
|
+
handler: async (_req, res) => {
|
|
241
|
+
if (credentials === undefined) return send(res, 500, { ok: false, error: 'credentials service unavailable' })
|
|
242
|
+
try {
|
|
243
|
+
const { values: keys } = await collectStoredKeys(credentials)
|
|
244
|
+
if (keys.length === 0) return send(res, 200, { ok: false, error: 'no Tavily API key configured' })
|
|
245
|
+
const state = readState(MANAGER_STATE)
|
|
246
|
+
const keySavedAt = state.keySavedAt !== undefined && typeof state.keySavedAt === 'object' ? state.keySavedAt : {}
|
|
247
|
+
const orderedKeys = keys.slice().sort((a, b) => {
|
|
248
|
+
const am = keySavedAt[maskValue(a)] !== undefined ? keySavedAt[maskValue(a)] : null
|
|
249
|
+
const bm = keySavedAt[maskValue(b)] !== undefined ? keySavedAt[maskValue(b)] : null
|
|
250
|
+
if (am === bm) return 0
|
|
251
|
+
if (am === null) return 1
|
|
252
|
+
if (bm === null) return -1
|
|
253
|
+
return am < bm ? -1 : 1
|
|
254
|
+
})
|
|
255
|
+
const perKey = []
|
|
256
|
+
for (const key of orderedKeys) {
|
|
257
|
+
try {
|
|
258
|
+
const usage = await fetchUsageDetailsFor(key)
|
|
259
|
+
perKey.push(usage !== null
|
|
260
|
+
? { ok: true, masked: maskValue(key), ...usage }
|
|
261
|
+
: { ok: false, masked: maskValue(key), error: 'usage unavailable' })
|
|
262
|
+
} catch (error) {
|
|
263
|
+
perKey.push({ ok: false, masked: maskValue(key), error: String(error && error.message ? error.message : error) })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const okRows = perKey.filter((row) => row.ok)
|
|
267
|
+
return send(res, 200, {
|
|
268
|
+
ok: true,
|
|
269
|
+
perKey,
|
|
270
|
+
totals: {
|
|
271
|
+
keys: keys.length,
|
|
272
|
+
okKeys: okRows.length,
|
|
273
|
+
usage: okRows.reduce((sum, row) => sum + (row.usage || 0), 0),
|
|
274
|
+
planUsage: okRows.reduce((sum, row) => sum + (row.planUsage || 0), 0),
|
|
275
|
+
planLimit: okRows.every((row) => row.planLimit !== null) ? okRows.reduce((sum, row) => sum + row.planLimit, 0) : null,
|
|
276
|
+
},
|
|
277
|
+
})
|
|
278
|
+
} catch (error) {
|
|
279
|
+
send(res, 500, { ok: false, error: String(error && error.message ? error.message : error) })
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
}), 'tavily-backend: route /api/tavily-usage')
|
|
283
|
+
|
|
284
|
+
ctx.effect(() => ctx.webServer.register({
|
|
285
|
+
kind: 'exact',
|
|
286
|
+
path: '/api/tavily-manager',
|
|
287
|
+
handler: async (req, res) => {
|
|
288
|
+
if (credentials === undefined) return send(res, 500, { ok: false, error: 'credentials service unavailable' })
|
|
289
|
+
try {
|
|
290
|
+
const url = new URL(req.url || '/', 'http://x')
|
|
291
|
+
if (req.method === 'GET') {
|
|
292
|
+
const reveal = url.searchParams.get('reveal')
|
|
293
|
+
const { values: keys, source } = await collectStoredKeys(credentials)
|
|
294
|
+
if (reveal !== null) {
|
|
295
|
+
const match = keys.find((key) => maskValue(key) === reveal)
|
|
296
|
+
if (match === undefined) return send(res, 404, { ok: false, error: 'key not found' })
|
|
297
|
+
return send(res, 200, { ok: true, value: match })
|
|
298
|
+
}
|
|
299
|
+
return send(res, 200, { ok: true, ...(await buildManagerPayload(keys, source)) })
|
|
300
|
+
}
|
|
301
|
+
if (req.method === 'POST') {
|
|
302
|
+
const body = await readBody(req)
|
|
303
|
+
const strategy = isValidStrategy(body.strategy) ? body.strategy : 'rotate'
|
|
304
|
+
const addValues = Array.isArray(body.add)
|
|
305
|
+
? body.add.map((item) => typeof item === 'string' ? item.trim() : '').filter((value) => value.length > 0)
|
|
306
|
+
: []
|
|
307
|
+
const removeMasked = Array.isArray(body.remove)
|
|
308
|
+
? body.remove.filter((item) => typeof item === 'string' && item.length > 0)
|
|
309
|
+
: []
|
|
310
|
+
|
|
311
|
+
let { values: stored, source } = await collectStoredKeys(credentials)
|
|
312
|
+
if (removeMasked.length > 0) {
|
|
313
|
+
stored = stored.filter((key) => !removeMasked.includes(maskValue(key)))
|
|
314
|
+
}
|
|
315
|
+
let values = [...stored]
|
|
316
|
+
for (const value of addValues) {
|
|
317
|
+
if (!values.includes(value)) values.push(value)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// A ref the launching environment supplies is inherited READ-ONLY: set and
|
|
321
|
+
// unset throw for it. Decide that BEFORE writing anything. The old order
|
|
322
|
+
// wrote `TAVILY_API_KEYS` first and then threw on the derived-primary sync,
|
|
323
|
+
// so the card reported a failure for a write that had half happened — and
|
|
324
|
+
// the unioned read then put the deleted key straight back.
|
|
325
|
+
const canWriteList = await refWritable(credentials, 'TAVILY_API_KEYS')
|
|
326
|
+
const canWritePrimary = await refWritable(credentials, 'TAVILY_API_KEY')
|
|
327
|
+
const READONLY_LIST = 'TAVILY_API_KEYS is supplied read-only by the launching environment, so the key list cannot be changed here. Unset it in the shell you start dsh from, or manage keys in ~/.dsh/.credentials.yaml.'
|
|
328
|
+
const READONLY_PRIMARY = 'That key comes from TAVILY_API_KEY, which the launching environment supplies read-only. Unset it in the shell you start dsh from.'
|
|
329
|
+
if (!canWriteList && (addValues.length > 0 || removeMasked.length > 0)) {
|
|
330
|
+
return send(res, 409, { ok: false, error: READONLY_LIST })
|
|
331
|
+
}
|
|
332
|
+
if (!canWritePrimary && source === 'primary' && removeMasked.length > 0) {
|
|
333
|
+
return send(res, 409, { ok: false, error: READONLY_PRIMARY })
|
|
334
|
+
}
|
|
335
|
+
// Emptying the list while an inherited primary still carries a key would
|
|
336
|
+
// read straight back as that key, so refuse rather than appear to undo itself.
|
|
337
|
+
if (values.length === 0 && !canWritePrimary) {
|
|
338
|
+
const inherited = parseKeyList(await resolveRef(credentials, 'TAVILY_API_KEY') ?? '')
|
|
339
|
+
if (inherited.length > 0) return send(res, 409, { ok: false, error: READONLY_PRIMARY })
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (values.length === 0) {
|
|
343
|
+
await credentials.unset('TAVILY_API_KEYS')
|
|
344
|
+
await syncPrimary(credentials, null)
|
|
345
|
+
} else {
|
|
346
|
+
if (strategy !== 'rotate') {
|
|
347
|
+
const usageRows = await Promise.all(values.map(async (value) => ({ value, usage: await fetchUsageFor(value) })))
|
|
348
|
+
const usageOf = (value) => {
|
|
349
|
+
const row = usageRows.find((r) => r.value === value)
|
|
350
|
+
return row !== undefined && row.usage !== null ? row.usage : null
|
|
351
|
+
}
|
|
352
|
+
values = orderKeys(values, strategy, usageOf)
|
|
353
|
+
}
|
|
354
|
+
await credentials.set('TAVILY_API_KEYS', values.join(','))
|
|
355
|
+
await syncPrimary(credentials, values[0])
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const state = readState(MANAGER_STATE)
|
|
359
|
+
const keySavedAt = state.keySavedAt !== undefined && typeof state.keySavedAt === 'object' ? state.keySavedAt : {}
|
|
360
|
+
const now = new Date().toISOString()
|
|
361
|
+
for (const value of values) {
|
|
362
|
+
const masked = maskValue(value)
|
|
363
|
+
if (keySavedAt[masked] === undefined) keySavedAt[masked] = now
|
|
364
|
+
}
|
|
365
|
+
writeState(MANAGER_STATE, { keySavedAt, strategy })
|
|
366
|
+
|
|
367
|
+
return send(res, 200, { ok: true, ...(await buildManagerPayload(values, source)) })
|
|
368
|
+
}
|
|
369
|
+
return send(res, 405, { ok: false, error: 'method not allowed' })
|
|
370
|
+
} catch (error) {
|
|
371
|
+
send(res, 500, { ok: false, error: String(error && error.message ? error.message : error) })
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
}), 'tavily-backend: route /api/tavily-manager')
|
|
375
|
+
|
|
376
|
+
}
|
package/src/client.js
CHANGED
|
@@ -7,7 +7,6 @@ export const id = '@moguiyu/dsh-tavily'
|
|
|
7
7
|
|
|
8
8
|
export function factory(require) {
|
|
9
9
|
const react = require('react')
|
|
10
|
-
const _primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
11
10
|
|
|
12
11
|
const inject = ['slots']
|
|
13
12
|
|
|
@@ -57,18 +56,6 @@ export function factory(require) {
|
|
|
57
56
|
}, react.createElement(SvgIcon, { name: icon, size: 13 }))
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
function Switch({ checked, disabled, label, onChange }) {
|
|
61
|
-
return react.createElement('button', {
|
|
62
|
-
type: 'button',
|
|
63
|
-
role: 'switch',
|
|
64
|
-
'aria-checked': checked === true,
|
|
65
|
-
'aria-label': label,
|
|
66
|
-
disabled: disabled || checked === null,
|
|
67
|
-
onClick: onChange,
|
|
68
|
-
style: { width: 36, height: 20, borderRadius: 10, border: '1px solid var(--dsw-alias-border-l2)', background: checked === true ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-bg-layer-1)', cursor: 'pointer', padding: 0, position: 'relative', flex: 'none', opacity: disabled ? 0.5 : 1 }
|
|
69
|
-
}, react.createElement('span', { style: { position: 'absolute', top: 2, left: checked === true ? 18 : 2, width: 14, height: 14, borderRadius: 7, background: '#fff', transition: 'left .12s ease' } }))
|
|
70
|
-
}
|
|
71
|
-
|
|
72
59
|
function UsageCircle({ percent, size, label, onClick }) {
|
|
73
60
|
const radius = 15
|
|
74
61
|
const circumference = 2 * Math.PI * radius
|
|
@@ -84,8 +71,6 @@ export function factory(require) {
|
|
|
84
71
|
}
|
|
85
72
|
|
|
86
73
|
function TavilySettingsCard() {
|
|
87
|
-
const [enabled, setEnabled] = react.useState(null)
|
|
88
|
-
const [expanded, setExpanded] = react.useState(false)
|
|
89
74
|
const [server, setServer] = react.useState(null)
|
|
90
75
|
const [loadError, setLoadError] = react.useState(null)
|
|
91
76
|
const [strategy, setStrategy] = react.useState('rotate')
|
|
@@ -101,13 +86,6 @@ export function factory(require) {
|
|
|
101
86
|
const [usageError, setUsageError] = react.useState(null)
|
|
102
87
|
|
|
103
88
|
const refresh = react.useCallback(async () => {
|
|
104
|
-
try {
|
|
105
|
-
const response = await fetch('/api/tavily-tool', { cache: 'no-store' })
|
|
106
|
-
const data = await response.json()
|
|
107
|
-
if (data.ok) { setEnabled(data.enabled); setLoadError(null) } else { setLoadError(data.error || 'Failed to load settings') }
|
|
108
|
-
} catch (error) {
|
|
109
|
-
setLoadError(String(error && error.message ? error.message : error))
|
|
110
|
-
}
|
|
111
89
|
try {
|
|
112
90
|
const response = await fetch('/api/tavily-manager', { cache: 'no-store' })
|
|
113
91
|
const data = await response.json()
|
|
@@ -127,27 +105,6 @@ export function factory(require) {
|
|
|
127
105
|
|
|
128
106
|
react.useEffect(() => { refresh() }, [refresh])
|
|
129
107
|
|
|
130
|
-
const toggleEnabled = async () => {
|
|
131
|
-
const next = enabled !== true
|
|
132
|
-
setBusy(true)
|
|
133
|
-
setNotice(null)
|
|
134
|
-
try {
|
|
135
|
-
const response = await fetch('/api/tavily-tool', {
|
|
136
|
-
method: 'POST',
|
|
137
|
-
headers: { 'content-type': 'application/json' },
|
|
138
|
-
body: JSON.stringify({ enabled: next })
|
|
139
|
-
})
|
|
140
|
-
const data = await response.json()
|
|
141
|
-
if (!data.ok) { setNotice({ error: data.error || 'Switch failed' }); return }
|
|
142
|
-
setEnabled(data.enabled)
|
|
143
|
-
setNotice({ ok: data.enabled ? 'Advanced tavily_search tool enabled.' : 'Advanced tavily_search tool disabled — built-in web_search is unchanged.' })
|
|
144
|
-
} catch (error) {
|
|
145
|
-
setNotice({ error: String(error && error.message ? error.message : error) })
|
|
146
|
-
} finally {
|
|
147
|
-
setBusy(false)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
108
|
const saveStrategy = async (next) => {
|
|
152
109
|
setBusy(true)
|
|
153
110
|
setNotice(null)
|
|
@@ -288,49 +245,19 @@ export function factory(require) {
|
|
|
288
245
|
const headStyle = { textAlign: 'left', padding: '6px 8px', borderBottom: '1px solid var(--dsw-alias-border-l2)', fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' }
|
|
289
246
|
const cellStyle = { padding: '8px', verticalAlign: 'top' }
|
|
290
247
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return react.createElement('div', { className: expanded ? 'dts-card dts-card-open' : 'dts-card', style: { minWidth: 0 } },
|
|
298
|
-
react.createElement('style', null, '.dts-card{border:.5px solid var(--dsw-alias-border-l4);border-radius:16px;background:var(--dsw-alias-bg-layer-3);transition:border-color .16s,background .16s}.dts-card:hover{border-color:var(--dsw-alias-label-dimmed)}.dts-card-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}.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)}'),
|
|
299
|
-
react.createElement('div', { style: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: '12px 14px', minHeight: 52, boxSizing: 'border-box' } },
|
|
300
|
-
react.createElement('button', {
|
|
301
|
-
type: 'button',
|
|
302
|
-
'aria-expanded': expanded,
|
|
303
|
-
onClick: () => setExpanded((current) => !current),
|
|
304
|
-
style: { flex: 1, minWidth: 0, textAlign: 'left', background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'flex', flexDirection: 'column', gap: 2, font: 'inherit', color: 'inherit' }
|
|
305
|
-
},
|
|
306
|
-
react.createElement('strong', { style: { fontSize: 14, fontWeight: 600, lineHeight: '20px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Tavily Search'),
|
|
307
|
-
react.createElement('span', { style: { fontSize: 12, lineHeight: '16px', color: 'var(--dsw-alias-label-tertiary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, 'Advanced tavily_search tool · key rotation · usage')
|
|
308
|
-
),
|
|
309
|
-
react.createElement('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, flex: 'none' } },
|
|
310
|
-
react.createElement(Switch, { checked: enabled, disabled: busy, label: 'Advanced tavily_search tool enabled', onChange: toggleEnabled }),
|
|
311
|
-
react.createElement('button', {
|
|
312
|
-
type: 'button',
|
|
313
|
-
className: 'dts-icon-btn',
|
|
314
|
-
title: expanded ? 'Collapse' : 'Expand',
|
|
315
|
-
'aria-label': expanded ? 'Collapse' : 'Expand',
|
|
316
|
-
onClick: () => setExpanded((current) => !current),
|
|
317
|
-
style: { width: 26, height: 26, borderRadius: 6, border: 'none', background: 'transparent', color: 'var(--dsw-alias-label-tertiary)', cursor: 'pointer', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: 0 }
|
|
318
|
-
},
|
|
319
|
-
react.createElement('span', { style: { display: 'inline-flex', transform: expanded ? 'rotate(180deg)' : undefined, transition: 'transform .14s var(--ds-ease-in-out)' } },
|
|
320
|
-
react.createElement(_primitives.IconChevronDownOutline14, { size: 12, 'aria-hidden': 'true' })
|
|
321
|
-
)
|
|
322
|
-
)
|
|
323
|
-
)
|
|
324
|
-
),
|
|
325
|
-
expanded && react.createElement('div', { style: { borderTop: '1px solid var(--dsw-alias-border-l2)', padding: '12px 14px', display: 'flex', flexDirection: 'column', gap: 16 } },
|
|
326
|
-
isOff
|
|
327
|
-
? react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
328
|
-
'The advanced tavily_search tool is off. The built-in web_search keeps its native provider and is never replaced.'
|
|
329
|
-
)
|
|
330
|
-
: react.createElement(react.Fragment, null,
|
|
248
|
+
// The Plugins page draws the title, the icon, and the crumb — the
|
|
249
|
+
// `plugins.bundle.config` contract puts the form alone in the entry — so
|
|
250
|
+
// this renders no card chrome, no second heading, and no collapse.
|
|
251
|
+
return react.createElement('div', { style: { display: 'flex', flexDirection: 'column', gap: 16, minWidth: 0 } },
|
|
252
|
+
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)}'),
|
|
253
|
+
react.createElement(react.Fragment, null,
|
|
331
254
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
332
255
|
'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.'
|
|
333
256
|
),
|
|
257
|
+
server !== null && server.writable !== undefined && server.writable.keys === false &&
|
|
258
|
+
react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
259
|
+
'Read-only: these keys come from the environment that launched dsh, so this card cannot change them. Unset TAVILY_API_KEYS / TAVILY_API_KEY in that shell and restart dsh to manage keys here.'
|
|
260
|
+
),
|
|
334
261
|
loadError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(loadError)),
|
|
335
262
|
react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
|
|
336
263
|
react.createElement('thead', null,
|
|
@@ -386,11 +313,15 @@ export function factory(require) {
|
|
|
386
313
|
!isRemoved && react.createElement(IconButton, { icon: 'pencil', title: 'Edit', onClick: () => startReplace(masked), disabled: busy }),
|
|
387
314
|
react.createElement(IconButton, {
|
|
388
315
|
icon: isRemoved ? 'restore' : (confirm[masked] === true ? 'check' : 'trash'),
|
|
389
|
-
|
|
316
|
+
// A key the launching environment supplies is read-only, so the
|
|
317
|
+
// server would refuse. Say so on the control instead.
|
|
318
|
+
title: key.removable === false
|
|
319
|
+
? 'Read-only: this key comes from the environment that launched dsh. Unset it there and restart dsh to change it here.'
|
|
320
|
+
: (isRemoved ? 'Restore' : (confirm[masked] === true ? 'Click again to confirm' : 'Delete')),
|
|
390
321
|
danger: !isRemoved,
|
|
391
322
|
className: isRemoved ? 'dts-icon-btn' : 'dts-icon-btn dts-icon-btn-danger',
|
|
392
323
|
onClick: () => isRemoved ? restore(masked) : markRemoved(masked),
|
|
393
|
-
disabled: busy
|
|
324
|
+
disabled: busy || (key.removable === false && !isRemoved)
|
|
394
325
|
})
|
|
395
326
|
)
|
|
396
327
|
)
|
|
@@ -439,35 +370,27 @@ export function factory(require) {
|
|
|
439
370
|
usageError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usageError)),
|
|
440
371
|
usage !== null && usage.ok === false && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(usage.error))
|
|
441
372
|
)
|
|
442
|
-
)
|
|
443
373
|
)
|
|
444
374
|
}
|
|
445
375
|
|
|
446
|
-
// The
|
|
447
|
-
// `
|
|
376
|
+
// The entry the Plugins page renders. The contract asks for two views:
|
|
377
|
+
// `summary` for the one-liner under the title, and `page` for the form with
|
|
378
|
+
// its own save control. The page supplies the title, icon, and crumb, so the
|
|
379
|
+
// summary is a plain string and the page view is the form alone — matching
|
|
380
|
+
// how the shipped cards (WebSearchCard and friends) are written.
|
|
448
381
|
function TavilyCard(props) {
|
|
449
|
-
if (props && props.view === 'summary')
|
|
382
|
+
if (props && props.view === 'summary') {
|
|
383
|
+
return 'Manage Tavily API keys and the key-usage strategy. The built-in web_search is never replaced.'
|
|
384
|
+
}
|
|
450
385
|
return react.createElement(TavilySettingsCard, null)
|
|
451
386
|
}
|
|
452
387
|
|
|
453
|
-
// One-liner for the summary view. Deliberately static and cheap: the page
|
|
454
|
-
// draws the title, the icon, and the crumb itself, and never asks this entry
|
|
455
|
-
// for anything but `page`.
|
|
456
|
-
function TavilySummary() {
|
|
457
|
-
return react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
458
|
-
'Rotating Tavily API keys for the advanced search tools. The built-in web_search keeps its native provider and is never replaced.'
|
|
459
|
-
)
|
|
460
|
-
}
|
|
461
|
-
|
|
462
388
|
function apply(ctx) {
|
|
463
|
-
// 0.1.6-alpha.2 moved plugin configuration
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// line, and neither throws on the other. Keep both -- dropping the retired
|
|
469
|
-
// name silently removes the card from rc.7 through 0.1.6-alpha.1, and
|
|
470
|
-
// dropping the new one silently removes it from 0.1.6-alpha.2 onward.
|
|
389
|
+
// ONE slot. 0.1.6-alpha.2 moved plugin configuration to the sidebar
|
|
390
|
+
// Plugins page and retired `settings.plugin.item`. This package no longer
|
|
391
|
+
// registers a settings namespace, so the retired keyed slot has no key it
|
|
392
|
+
// could claim. On rc.7 through 0.1.6-alpha.1 the tools and routes work but
|
|
393
|
+
// this card has no host — see `docs/agents/plugin-design.md`.
|
|
471
394
|
ctx.slots.inject('plugins.bundle.config', () => ctx.slots.register({
|
|
472
395
|
name: 'plugins.bundle.config',
|
|
473
396
|
// Keyed by the bundle's package name. The Plugins page renders this on
|
|
@@ -475,18 +398,6 @@ export function factory(require) {
|
|
|
475
398
|
key: '@moguiyu/dsh-tavily',
|
|
476
399
|
id: '@moguiyu/dsh-tavily'
|
|
477
400
|
}, TavilyCard))
|
|
478
|
-
|
|
479
|
-
ctx.slots.inject('settings.plugin.item', () => ctx.slots.register({
|
|
480
|
-
name: 'settings.plugin.item',
|
|
481
|
-
// rc.7 through 0.1.6-alpha.1: keyed by the settings namespace the Host
|
|
482
|
-
// serves (`tavily-search`); the Plugins configuration tab dispatches it
|
|
483
|
-
// when the namespace is present. The id/order/label props keep rc.6
|
|
484
|
-
// list-slot deployments rendering the card as well.
|
|
485
|
-
key: 'tavily-search',
|
|
486
|
-
id: 'tavily-search',
|
|
487
|
-
order: 30,
|
|
488
|
-
label: 'Tavily Search'
|
|
489
|
-
}, TavilyCard))
|
|
490
401
|
}
|
|
491
402
|
|
|
492
403
|
return { apply, inject }
|