@moguiyu/dsh-tavily 0.3.0 → 0.3.2
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/lib/client.js +10 -2
- package/package.json +7 -4
- package/src/backend.js +100 -21
- package/src/client.js +10 -2
package/lib/client.js
CHANGED
|
@@ -249,6 +249,10 @@ window.__ModuleLoader__.load({
|
|
|
249
249
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
250
250
|
'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.'
|
|
251
251
|
),
|
|
252
|
+
server !== null && server.writable !== undefined && server.writable.keys === false &&
|
|
253
|
+
react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
254
|
+
'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.'
|
|
255
|
+
),
|
|
252
256
|
loadError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(loadError)),
|
|
253
257
|
react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
|
|
254
258
|
react.createElement('thead', null,
|
|
@@ -304,11 +308,15 @@ window.__ModuleLoader__.load({
|
|
|
304
308
|
!isRemoved && react.createElement(IconButton, { icon: 'pencil', title: 'Edit', onClick: () => startReplace(masked), disabled: busy }),
|
|
305
309
|
react.createElement(IconButton, {
|
|
306
310
|
icon: isRemoved ? 'restore' : (confirm[masked] === true ? 'check' : 'trash'),
|
|
307
|
-
|
|
311
|
+
// A key the launching environment supplies is read-only, so the
|
|
312
|
+
// server would refuse. Say so on the control instead.
|
|
313
|
+
title: key.removable === false
|
|
314
|
+
? 'Read-only: this key comes from the environment that launched dsh. Unset it there and restart dsh to change it here.'
|
|
315
|
+
: (isRemoved ? 'Restore' : (confirm[masked] === true ? 'Click again to confirm' : 'Delete')),
|
|
308
316
|
danger: !isRemoved,
|
|
309
317
|
className: isRemoved ? 'dts-icon-btn' : 'dts-icon-btn dts-icon-btn-danger',
|
|
310
318
|
onClick: () => isRemoved ? restore(masked) : markRemoved(masked),
|
|
311
|
-
disabled: busy
|
|
319
|
+
disabled: busy || (key.removable === false && !isRemoved)
|
|
312
320
|
})
|
|
313
321
|
)
|
|
314
322
|
)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moguiyu/dsh-tavily",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -37,7 +37,10 @@
|
|
|
37
37
|
"0.1.5-rc.1": "compatible",
|
|
38
38
|
"0.1.5-rc.2": "compatible",
|
|
39
39
|
"0.1.6-alpha.1": "compatible",
|
|
40
|
-
"0.1.6-alpha.2": "compatible"
|
|
40
|
+
"0.1.6-alpha.2": "compatible",
|
|
41
|
+
"0.1.7-rc.2": "compatible",
|
|
42
|
+
"0.2.0-rc.1": "compatible",
|
|
43
|
+
"0.2.0-rc.2": "compatible"
|
|
41
44
|
}
|
|
42
45
|
},
|
|
43
46
|
"client": {
|
|
@@ -65,8 +68,8 @@
|
|
|
65
68
|
},
|
|
66
69
|
"peerDependencies": {
|
|
67
70
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
68
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0",
|
|
69
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0"
|
|
71
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0",
|
|
72
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7 || ^0.1.1-rc.1 || ^0.1.2-alpha.2 || ^0.1.3-alpha.1 || ^0.1.5-0 || ^0.1.6-0 || ^0.1.7-0 || ^0.2.0-0"
|
|
70
73
|
},
|
|
71
74
|
"devDependencies": {
|
|
72
75
|
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2"
|
package/src/backend.js
CHANGED
|
@@ -98,27 +98,74 @@ async function fetchUsageFor(key) {
|
|
|
98
98
|
return details.usage !== null ? details.usage : details.planUsage
|
|
99
99
|
}
|
|
100
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
|
+
*/
|
|
101
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) {
|
|
102
143
|
const out = []
|
|
103
144
|
const seen = new Set()
|
|
104
|
-
for (const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
} catch {
|
|
109
|
-
continue
|
|
110
|
-
}
|
|
111
|
-
if (hit === undefined || typeof hit.value !== 'string') continue
|
|
112
|
-
for (const key of parseKeyList(hit.value)) {
|
|
113
|
-
if (!seen.has(key)) {
|
|
114
|
-
seen.add(key)
|
|
115
|
-
out.push(key)
|
|
116
|
-
}
|
|
145
|
+
for (const key of values) {
|
|
146
|
+
if (!seen.has(key)) {
|
|
147
|
+
seen.add(key)
|
|
148
|
+
out.push(key)
|
|
117
149
|
}
|
|
118
150
|
}
|
|
119
151
|
return out
|
|
120
152
|
}
|
|
121
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
|
+
|
|
122
169
|
/**
|
|
123
170
|
* Register the Tavily key/usage routes on `ctx.webServer`:
|
|
124
171
|
* `/api/tavily-usage` and `/api/tavily-manager`.
|
|
@@ -135,7 +182,7 @@ export function installBackend(ctx) {
|
|
|
135
182
|
}
|
|
136
183
|
|
|
137
184
|
// ── key manager payload ────────────────────────────────────────────────────
|
|
138
|
-
async function buildManagerPayload(keys) {
|
|
185
|
+
async function buildManagerPayload(keys, source = 'list') {
|
|
139
186
|
const state = readState(MANAGER_STATE)
|
|
140
187
|
const keySavedAt = state.keySavedAt !== undefined && typeof state.keySavedAt === 'object' ? state.keySavedAt : {}
|
|
141
188
|
if (Object.keys(keySavedAt).length === 0 && credentials !== undefined) {
|
|
@@ -168,13 +215,22 @@ export function installBackend(ctx) {
|
|
|
168
215
|
if (b.savedAt === null) return -1
|
|
169
216
|
return a.savedAt < b.savedAt ? -1 : 1
|
|
170
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
|
|
171
224
|
return {
|
|
172
225
|
keys: display.map((entry) => ({
|
|
173
226
|
masked: entry.masked,
|
|
174
227
|
savedAt: entry.savedAt,
|
|
175
228
|
primary: entry.masked === primaryMasked,
|
|
229
|
+
removable,
|
|
176
230
|
})),
|
|
177
231
|
strategy: isValidStrategy(state.strategy) ? state.strategy : 'rotate',
|
|
232
|
+
source,
|
|
233
|
+
writable: { keys: keysWritable, primary: primaryWritable },
|
|
178
234
|
}
|
|
179
235
|
}
|
|
180
236
|
|
|
@@ -184,7 +240,7 @@ export function installBackend(ctx) {
|
|
|
184
240
|
handler: async (_req, res) => {
|
|
185
241
|
if (credentials === undefined) return send(res, 500, { ok: false, error: 'credentials service unavailable' })
|
|
186
242
|
try {
|
|
187
|
-
const keys = await collectStoredKeys(credentials)
|
|
243
|
+
const { values: keys } = await collectStoredKeys(credentials)
|
|
188
244
|
if (keys.length === 0) return send(res, 200, { ok: false, error: 'no Tavily API key configured' })
|
|
189
245
|
const state = readState(MANAGER_STATE)
|
|
190
246
|
const keySavedAt = state.keySavedAt !== undefined && typeof state.keySavedAt === 'object' ? state.keySavedAt : {}
|
|
@@ -234,13 +290,13 @@ export function installBackend(ctx) {
|
|
|
234
290
|
const url = new URL(req.url || '/', 'http://x')
|
|
235
291
|
if (req.method === 'GET') {
|
|
236
292
|
const reveal = url.searchParams.get('reveal')
|
|
237
|
-
const keys = await collectStoredKeys(credentials)
|
|
293
|
+
const { values: keys, source } = await collectStoredKeys(credentials)
|
|
238
294
|
if (reveal !== null) {
|
|
239
295
|
const match = keys.find((key) => maskValue(key) === reveal)
|
|
240
296
|
if (match === undefined) return send(res, 404, { ok: false, error: 'key not found' })
|
|
241
297
|
return send(res, 200, { ok: true, value: match })
|
|
242
298
|
}
|
|
243
|
-
return send(res, 200, { ok: true, ...(await buildManagerPayload(keys)) })
|
|
299
|
+
return send(res, 200, { ok: true, ...(await buildManagerPayload(keys, source)) })
|
|
244
300
|
}
|
|
245
301
|
if (req.method === 'POST') {
|
|
246
302
|
const body = await readBody(req)
|
|
@@ -252,7 +308,7 @@ export function installBackend(ctx) {
|
|
|
252
308
|
? body.remove.filter((item) => typeof item === 'string' && item.length > 0)
|
|
253
309
|
: []
|
|
254
310
|
|
|
255
|
-
let stored = await collectStoredKeys(credentials)
|
|
311
|
+
let { values: stored, source } = await collectStoredKeys(credentials)
|
|
256
312
|
if (removeMasked.length > 0) {
|
|
257
313
|
stored = stored.filter((key) => !removeMasked.includes(maskValue(key)))
|
|
258
314
|
}
|
|
@@ -260,9 +316,32 @@ export function installBackend(ctx) {
|
|
|
260
316
|
for (const value of addValues) {
|
|
261
317
|
if (!values.includes(value)) values.push(value)
|
|
262
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
|
+
|
|
263
342
|
if (values.length === 0) {
|
|
264
343
|
await credentials.unset('TAVILY_API_KEYS')
|
|
265
|
-
await credentials
|
|
344
|
+
await syncPrimary(credentials, null)
|
|
266
345
|
} else {
|
|
267
346
|
if (strategy !== 'rotate') {
|
|
268
347
|
const usageRows = await Promise.all(values.map(async (value) => ({ value, usage: await fetchUsageFor(value) })))
|
|
@@ -273,7 +352,7 @@ export function installBackend(ctx) {
|
|
|
273
352
|
values = orderKeys(values, strategy, usageOf)
|
|
274
353
|
}
|
|
275
354
|
await credentials.set('TAVILY_API_KEYS', values.join(','))
|
|
276
|
-
await credentials
|
|
355
|
+
await syncPrimary(credentials, values[0])
|
|
277
356
|
}
|
|
278
357
|
|
|
279
358
|
const state = readState(MANAGER_STATE)
|
|
@@ -285,7 +364,7 @@ export function installBackend(ctx) {
|
|
|
285
364
|
}
|
|
286
365
|
writeState(MANAGER_STATE, { keySavedAt, strategy })
|
|
287
366
|
|
|
288
|
-
return send(res, 200, { ok: true, ...(await buildManagerPayload(values)) })
|
|
367
|
+
return send(res, 200, { ok: true, ...(await buildManagerPayload(values, source)) })
|
|
289
368
|
}
|
|
290
369
|
return send(res, 405, { ok: false, error: 'method not allowed' })
|
|
291
370
|
} catch (error) {
|
package/src/client.js
CHANGED
|
@@ -254,6 +254,10 @@ export function factory(require) {
|
|
|
254
254
|
react.createElement('p', { style: { margin: 0, fontSize: 13, color: 'var(--dsw-alias-label-tertiary)' } },
|
|
255
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.'
|
|
256
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
|
+
),
|
|
257
261
|
loadError !== null && react.createElement('p', { style: { margin: 0, fontSize: 12, color: 'var(--dsw-alias-state-error-primary)' } }, String(loadError)),
|
|
258
262
|
react.createElement('table', { style: { width: '100%', borderCollapse: 'collapse', fontSize: 13 } },
|
|
259
263
|
react.createElement('thead', null,
|
|
@@ -309,11 +313,15 @@ export function factory(require) {
|
|
|
309
313
|
!isRemoved && react.createElement(IconButton, { icon: 'pencil', title: 'Edit', onClick: () => startReplace(masked), disabled: busy }),
|
|
310
314
|
react.createElement(IconButton, {
|
|
311
315
|
icon: isRemoved ? 'restore' : (confirm[masked] === true ? 'check' : 'trash'),
|
|
312
|
-
|
|
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')),
|
|
313
321
|
danger: !isRemoved,
|
|
314
322
|
className: isRemoved ? 'dts-icon-btn' : 'dts-icon-btn dts-icon-btn-danger',
|
|
315
323
|
onClick: () => isRemoved ? restore(masked) : markRemoved(masked),
|
|
316
|
-
disabled: busy
|
|
324
|
+
disabled: busy || (key.removable === false && !isRemoved)
|
|
317
325
|
})
|
|
318
326
|
)
|
|
319
327
|
)
|