dsh-web-search-plugin 0.4.1 → 0.5.0
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 +42 -1
- package/README.md +70 -66
- package/cordis.patch.yml +4 -3
- package/lib/client.js +173 -85
- package/lib/deepseek.js +71 -15
- package/lib/index.js +135 -77
- package/lib/providers.js +7 -7
- package/lib/rest.js +10 -5
- package/lib/shared.js +1 -1
- package/lib/tavily.js +2 -2
- package/lib/usage.js +53 -30
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +11 -8
package/lib/usage.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Local usage cache for Tavily keyed searches and Brave rate-limit headers.
|
|
3
3
|
*
|
|
4
|
-
* Tavily: each keyed search sends `include_usage` and adds those credits to
|
|
5
|
-
*
|
|
6
|
-
* plan_limit and the vendor's own used count.
|
|
7
|
-
* `max(local, remote)`.
|
|
8
|
-
*
|
|
4
|
+
* Tavily: each keyed search sends `include_usage` and adds those credits to an
|
|
5
|
+
* in-memory counter for the current credential only. A slower `GET /usage`
|
|
6
|
+
* poll supplies plan name / plan_limit and the vendor's own used count.
|
|
7
|
+
* Displayed used is `max(local, remote)`. Switching keys discards the old
|
|
8
|
+
* account's data; a billing-cycle change resets the local counter.
|
|
9
9
|
*
|
|
10
10
|
* Brave: there is no usage endpoint. Limit / remaining / reset come from
|
|
11
11
|
* `X-RateLimit-*` on each search response and are cached as-is.
|
|
@@ -13,14 +13,17 @@
|
|
|
13
13
|
* @module dsh-web-search-plugin/usage
|
|
14
14
|
*/
|
|
15
15
|
import { homedir } from "node:os";
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
16
17
|
import { dirname, join } from "node:path";
|
|
17
18
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
18
19
|
import { USER_AGENT, resolveApiKey } from "./shared.js";
|
|
19
20
|
|
|
20
|
-
/** Persist path under `$DSH_HOME/storages`. */
|
|
21
|
+
/** Persist path for Brave rate-limit data under `$DSH_HOME/storages`. */
|
|
21
22
|
export const USAGE_FILE = join(process.env.DSH_HOME || join(homedir(), ".dsh"), "storages", "dsh-web-search-usage.json");
|
|
22
23
|
/** Tavily `/usage` is itself rate-limited (~10 / 10 min); poll slower than that. */
|
|
23
24
|
export const TAVILY_USAGE_POLL_MS = 10 * 60 * 1000;
|
|
25
|
+
/** Keep manual refreshes from exhausting Tavily's `/usage` request budget. */
|
|
26
|
+
export const TAVILY_USAGE_MIN_INTERVAL_MS = 10 * 1000;
|
|
24
27
|
/** Coalesce disk writes. */
|
|
25
28
|
const SAVE_DEBOUNCE_MS = 800;
|
|
26
29
|
|
|
@@ -198,6 +201,8 @@ export class UsageTracker {
|
|
|
198
201
|
this.state = emptyState();
|
|
199
202
|
this.saveTimer = null;
|
|
200
203
|
this.hydrated = false;
|
|
204
|
+
this.tavilyKeyFingerprint = null;
|
|
205
|
+
this.lastTavilyManualAttemptAt = 0;
|
|
201
206
|
}
|
|
202
207
|
|
|
203
208
|
/** Load the previous process's cache; missing file is a first run. */
|
|
@@ -205,11 +210,8 @@ export class UsageTracker {
|
|
|
205
210
|
try {
|
|
206
211
|
const raw = JSON.parse(await readFile(USAGE_FILE, "utf8"));
|
|
207
212
|
if (raw && typeof raw === "object") {
|
|
208
|
-
this.state = {
|
|
209
|
-
|
|
210
|
-
tavily: { ...emptyState().tavily, ...(raw.tavily && typeof raw.tavily === "object" ? raw.tavily : {}) },
|
|
211
|
-
brave: { ...emptyState().brave, ...(raw.brave && typeof raw.brave === "object" ? raw.brave : {}) }
|
|
212
|
-
};
|
|
213
|
+
this.state.brave = { ...emptyState().brave, ...(raw.brave && typeof raw.brave === "object" ? raw.brave : {}) };
|
|
214
|
+
if (Object.hasOwn(raw, "tavily")) this.scheduleSave();
|
|
213
215
|
}
|
|
214
216
|
} catch {
|
|
215
217
|
/* first run */
|
|
@@ -257,9 +259,27 @@ export class UsageTracker {
|
|
|
257
259
|
};
|
|
258
260
|
}
|
|
259
261
|
|
|
260
|
-
|
|
262
|
+
activateTavilyKey(apiKey, baseURL) {
|
|
263
|
+
const fingerprint = createHash("sha256").update(baseURL).update("\0").update(apiKey).digest("hex");
|
|
264
|
+
if (fingerprint !== this.tavilyKeyFingerprint) {
|
|
265
|
+
this.tavilyKeyFingerprint = fingerprint;
|
|
266
|
+
this.state.tavily = emptyState().tavily;
|
|
267
|
+
this.lastTavilyManualAttemptAt = 0;
|
|
268
|
+
}
|
|
269
|
+
return fingerprint;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async recordTavilyCredits(credits, apiKey) {
|
|
273
|
+
if (typeof apiKey !== "string" || apiKey.length === 0) return;
|
|
274
|
+
let options;
|
|
275
|
+
try {
|
|
276
|
+
options = this.getTavilyOptions?.();
|
|
277
|
+
if (options?.mode !== "keyed" || await resolveApiKey(options, undefined, "Tavily usage has no API key") !== apiKey) return;
|
|
278
|
+
} catch {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.activateTavilyKey(apiKey, options.baseURL);
|
|
261
282
|
this.state.tavily = mergeTavilyCredits(this.state.tavily, credits);
|
|
262
|
-
this.scheduleSave();
|
|
263
283
|
}
|
|
264
284
|
|
|
265
285
|
recordBraveHeaders(headers) {
|
|
@@ -279,18 +299,27 @@ export class UsageTracker {
|
|
|
279
299
|
const options = this.getTavilyOptions?.();
|
|
280
300
|
if (options == null || options.mode !== "keyed") return this.snapshot();
|
|
281
301
|
if (!URL.canParse(options.baseURL)) return this.snapshot();
|
|
282
|
-
const last = this.state.tavily.fetchedAt || 0;
|
|
283
|
-
const hasIdentity = this.state.tavily.plan != null || this.state.tavily.limit != null;
|
|
284
|
-
const interval = hasIdentity ? TAVILY_USAGE_POLL_MS : 60_000;
|
|
285
|
-
if (!force && last > 0 && (Date.now() - last) < interval) return this.snapshot();
|
|
286
302
|
let apiKey;
|
|
287
303
|
try {
|
|
288
304
|
apiKey = await resolveApiKey(options, signal, "Tavily usage has no API key");
|
|
289
305
|
} catch (error) {
|
|
290
|
-
this.
|
|
291
|
-
this.
|
|
306
|
+
this.tavilyKeyFingerprint = null;
|
|
307
|
+
this.state.tavily = { ...emptyState().tavily, error: String(error?.message ?? error), fetchedAt: Date.now() };
|
|
292
308
|
return this.snapshot();
|
|
293
309
|
}
|
|
310
|
+
const fingerprint = this.activateTavilyKey(apiKey, options.baseURL);
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
if (force && this.lastTavilyManualAttemptAt > 0 && now - this.lastTavilyManualAttemptAt < TAVILY_USAGE_MIN_INTERVAL_MS) return this.snapshot();
|
|
313
|
+
const last = this.state.tavily.fetchedAt || 0;
|
|
314
|
+
const hasIdentity = this.state.tavily.plan != null || this.state.tavily.limit != null;
|
|
315
|
+
const interval = hasIdentity ? TAVILY_USAGE_POLL_MS : 60_000;
|
|
316
|
+
if (!force && last > 0 && now - last < interval) return this.snapshot();
|
|
317
|
+
if (force) this.lastTavilyManualAttemptAt = now;
|
|
318
|
+
const isCurrent = () => fingerprint === this.tavilyKeyFingerprint;
|
|
319
|
+
const fail = (message) => {
|
|
320
|
+
if (isCurrent()) this.state.tavily = { ...this.state.tavily, error: message, fetchedAt: Date.now() };
|
|
321
|
+
return this.snapshot();
|
|
322
|
+
};
|
|
294
323
|
const endpoint = `${options.baseURL.replace(/\/+$/u, "")}/usage`;
|
|
295
324
|
let response;
|
|
296
325
|
try {
|
|
@@ -305,30 +334,24 @@ export class UsageTracker {
|
|
|
305
334
|
...signal !== void 0 ? { signal } : {}
|
|
306
335
|
});
|
|
307
336
|
} catch (error) {
|
|
308
|
-
|
|
309
|
-
this.scheduleSave();
|
|
310
|
-
return this.snapshot();
|
|
337
|
+
return fail(String(error?.message ?? error));
|
|
311
338
|
}
|
|
312
339
|
if (!response.ok) {
|
|
313
|
-
|
|
314
|
-
this.scheduleSave();
|
|
315
|
-
return this.snapshot();
|
|
340
|
+
return fail(`Tavily /usage HTTP ${response.status}`);
|
|
316
341
|
}
|
|
317
342
|
let body;
|
|
318
343
|
try {
|
|
319
344
|
body = await response.json();
|
|
320
345
|
} catch (error) {
|
|
321
|
-
|
|
322
|
-
this.scheduleSave();
|
|
323
|
-
return this.snapshot();
|
|
346
|
+
return fail(String(error?.message ?? error));
|
|
324
347
|
}
|
|
348
|
+
if (!isCurrent()) return this.snapshot();
|
|
325
349
|
const remote = parseTavilyUsageBody(body);
|
|
326
350
|
if (remote === null) {
|
|
327
351
|
this.state.tavily = { ...this.state.tavily, error: "Tavily /usage returned an unreadable body", fetchedAt: Date.now() };
|
|
328
352
|
} else {
|
|
329
353
|
this.state.tavily = mergeTavilyRemote(this.state.tavily, remote);
|
|
330
354
|
}
|
|
331
|
-
this.scheduleSave();
|
|
332
355
|
return this.snapshot();
|
|
333
356
|
}
|
|
334
357
|
|
|
@@ -336,7 +359,7 @@ export class UsageTracker {
|
|
|
336
359
|
if (this.saveTimer !== null) return;
|
|
337
360
|
this.saveTimer = setTimeout(() => {
|
|
338
361
|
this.saveTimer = null;
|
|
339
|
-
const payload = JSON.stringify(this.state);
|
|
362
|
+
const payload = JSON.stringify({ version: 1, brave: this.state.brave });
|
|
340
363
|
mkdir(dirname(USAGE_FILE), { recursive: true })
|
|
341
364
|
.then(() => writeFile(USAGE_FILE, payload))
|
|
342
365
|
.catch(() => { /* in-memory cache still serves the UI */ });
|
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-web-search-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily, Brave, Serper, SerpApi, Exa, SearXNG, Scavio, and Firecrawl backends",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"main": "lib/index.js",
|
|
19
19
|
"files": [
|
|
20
20
|
"lib",
|
|
21
|
+
"locale",
|
|
21
22
|
"cordis.patch.yml",
|
|
22
23
|
"CHANGELOG.md",
|
|
23
24
|
"README.md",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
"default": "./lib/client.js"
|
|
35
36
|
},
|
|
36
37
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
38
|
+
"./locale/*.json": "./locale/*.json",
|
|
37
39
|
"./package.json": "./package.json"
|
|
38
40
|
},
|
|
39
41
|
"dsh": {
|
|
@@ -51,18 +53,19 @@
|
|
|
51
53
|
}
|
|
52
54
|
},
|
|
53
55
|
"scripts": {
|
|
54
|
-
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/providers.js && node --check lib/rest.js && node --check lib/tavily.js && node --check lib/brave.js && node --check lib/deepseek.js && node --check lib/usage.js"
|
|
56
|
+
"check": "node --check lib/index.js && node --check lib/client.js && node --check lib/shared.js && node --check lib/providers.js && node --check lib/rest.js && node --check lib/tavily.js && node --check lib/brave.js && node --check lib/deepseek.js && node --check lib/usage.js",
|
|
57
|
+
"test": "node --experimental-vm-modules --test test/*.test.mjs"
|
|
55
58
|
},
|
|
56
59
|
"dependencies": {
|
|
57
60
|
"undici": "^6.21.0"
|
|
58
61
|
},
|
|
59
62
|
"peerDependencies": {
|
|
60
|
-
"@deepseek-ai/dsh-web": "^0.
|
|
61
|
-
"@deepseek-ai/dsh-credentials": "^0.
|
|
62
|
-
"@deepseek-ai/dsh-settings": "^0.
|
|
63
|
-
"@deepseek-ai/dsh-launch-environment": "^0.
|
|
64
|
-
"@deepseek-ai/schemastery": "
|
|
65
|
-
"@deepseek-ai/cordis": "
|
|
63
|
+
"@deepseek-ai/dsh-web": "^0.2.0-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-credentials": "^0.2.0-rc.2",
|
|
65
|
+
"@deepseek-ai/dsh-settings": "^0.2.0-rc.2",
|
|
66
|
+
"@deepseek-ai/dsh-launch-environment": "^0.2.0-rc.2",
|
|
67
|
+
"@deepseek-ai/schemastery": "~3.18.4",
|
|
68
|
+
"@deepseek-ai/cordis": "~4.0.4"
|
|
66
69
|
},
|
|
67
70
|
"license": "MIT",
|
|
68
71
|
"repository": {
|