dsh-web-search-plugin 0.4.0 → 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/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 a
5
- * high-water local counter. A slower `GET /usage` poll supplies plan name /
6
- * plan_limit and the vendor's own used count. Displayed used is
7
- * `max(local, remote)`. A drop in remote used, or a plan/limit change, is
8
- * treated as a new billing cycle (local resets to remote).
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
- version: 1,
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
- recordTavilyCredits(credits) {
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.state.tavily = { ...this.state.tavily, error: String(error?.message ?? error), fetchedAt: Date.now() };
291
- this.scheduleSave();
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
- this.state.tavily = { ...this.state.tavily, error: String(error?.message ?? error), fetchedAt: Date.now() };
309
- this.scheduleSave();
310
- return this.snapshot();
337
+ return fail(String(error?.message ?? error));
311
338
  }
312
339
  if (!response.ok) {
313
- this.state.tavily = { ...this.state.tavily, error: `Tavily /usage HTTP ${response.status}`, fetchedAt: Date.now() };
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
- this.state.tavily = { ...this.state.tavily, error: String(error?.message ?? error), fetchedAt: Date.now() };
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
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Web search",
4
+ "description": "Unified web search for the DeepSeek Harness seam (ctx.web): DeepSeek (official), Tavily, Brave, Serper, SerpApi, Exa, SearXNG, Scavio, and Firecrawl. Switch engines from settings."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "网页搜索",
4
+ "description": "为 DeepSeek Harness 接缝(ctx.web)提供统一网页搜索:DeepSeek(官方)、Tavily、Brave、Serper、SerpApi、Exa、SearXNG、Scavio、Firecrawl,可在设置中切换引擎。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-web-search-plugin",
3
- "version": "0.4.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,13 +18,14 @@
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",
24
25
  "LICENSE"
25
26
  ],
26
27
  "engines": {
27
- "node": ">=18"
28
+ "node": "^22.19.0 || >=24.0.0"
28
29
  },
29
30
  "exports": {
30
31
  ".": {
@@ -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.1.2-alpha.4",
61
- "@deepseek-ai/dsh-credentials": "^0.1.2-alpha.4",
62
- "@deepseek-ai/dsh-settings": "^0.1.2-alpha.4",
63
- "@deepseek-ai/dsh-launch-environment": "^0.1.2-alpha.4",
64
- "@deepseek-ai/schemastery": "^3.18.2",
65
- "@deepseek-ai/cordis": "^4.0.2"
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": {