@240xu/dsh-websearch 2.0.3 → 2.0.5

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # @240xu/dsh-websearch
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/@240xu/dsh-websearch.svg)](https://www.npmjs.com/package/@240xu/dsh-websearch)
4
+ [![license](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+
3
6
  > Aggregated web search provider for [DeepSeek Harness](https://github.com/deepseek-ai/dsh) — one plugin id `unified` that fans out to eleven backends concurrently, merges + URL-dedups results, and stays usable even when some backends go down.
4
7
 
5
8
  A pure-Cordis drop-in: registers ONE provider at `ctx.web` so the `dsh-web` selection rule never fires `WEB_PROVIDER_AMBIGUOUS`. Zero-config search out of the box (Exa + Parallel + DuckDuckGo + SearXNG are keyless); API-key backends (DeepSeek / Anthropic / OpenAI / Brave / Tavily / Serper / Mojeek) auto-activate once their key is supplied via the credentials service or env.
@@ -57,6 +60,10 @@ pnpm install
57
60
 
58
61
  ## Settings | 配置
59
62
 
63
+ > v2.0.3 起,配置界面位于 **Settings → 左侧栏「搜索 / Web Search」**(顶级分区,与通用/模型/插件同级)。
64
+ > 每个 API Key 字段旁有 **「获取 Key ↗」** 直达对应平台控制台;保存即写入凭证库并立即生效。
65
+
66
+
60
67
  插件向 DSH 设置面板注册 `unified-search` namespace(Settings → Unified Search),全部字段扁平化、开箱可编辑:
61
68
 
62
69
  **全局**
@@ -1,17 +1,25 @@
1
- # ── dsh-websearch v2.0: unified free web search ───────────────────────
2
- # Registers provider id "unified" that fans out to 11 backends concurrently:
3
- # keyless: exa, parallel, ddg, searxng
4
- # key-gated: deepseek, anthropic, openai, brave, tavily, serper, mojeek
5
- # (activate when their API key is set via Settings > Unified Search)
1
+ # dsh-websearch v2.0.3 — user-side profile patch example
6
2
  #
7
- # web-search-deepseek stays disabled because the unified plugin supersedes it.
8
- # web.searchProvider pins the built-in web_search tool onto the unified provider.
9
- - id: unified-search
10
- name: "@deepseek-ai/dsh-websearch"
3
+ # The plugin itself is registered by its own bundle manifest
4
+ # (dsh.bundle in package.json + cordis.patch.yml at the repo root),
5
+ # so after installing it there is NOTHING to add for the plugin row.
6
+ #
7
+ # What you DO need in your profile patch: pin the built-in web_search
8
+ # tool onto the unified provider and disable the superseded official one.
9
+
11
10
  - id: web-search-deepseek
12
- name: "@deepseek-ai/dsh-web-search-deepseek"
11
+ name: '@deepseek-ai/dsh-web-search-deepseek'
13
12
  disabled: true
14
13
  - id: web
15
- name: "@deepseek-ai/dsh-web"
14
+ name: '@deepseek-ai/dsh-web'
16
15
  config:
17
16
  searchProvider: unified
17
+
18
+ # Install:
19
+ # dsh plugin --profile web add github:240xu/dsh-websearch
20
+ # or (after npm publish):
21
+ # dsh plugin --profile web add @240xu/dsh-websearch
22
+ #
23
+ # Then open Settings -> Web Search to toggle backends, store API keys
24
+ # (each field links straight to the provider's key dashboard), and tune
25
+ # concurrency / timeouts.
package/lib/client.js CHANGED
@@ -39,6 +39,10 @@ window.__ModuleLoader__.load({
39
39
 
40
40
  const zh = {
41
41
  nav: "搜索",
42
+ statusEnabled: "已启用后端",
43
+ statusKeys: "已配置密钥",
44
+ keyMissing: "缺密钥",
45
+ unsavedHint: "有未保存的修改", savedOk: "已保存 ✓",
42
46
  title: "统一搜索 (Unified Search)",
43
47
  description: "聚合网络搜索:11 个后端并发 fan-out、URL 去重合并。开关后端、填 API Key、调参数,保存即生效。",
44
48
  save: "保存",
@@ -56,9 +60,14 @@ window.__ModuleLoader__.load({
56
60
  keyUnset: "未配置",
57
61
  togglesTitle: "启用后端",
58
62
  globalsTitle: "全局参数",
63
+ keysTitle: "API 密钥(保存后写入凭证库,立即生效)",
59
64
  };
60
65
  const en = {
61
66
  nav: "Web Search",
67
+ statusEnabled: "Backends enabled",
68
+ statusKeys: "Keys configured",
69
+ keyMissing: "key missing",
70
+ unsavedHint: "You have unsaved changes", savedOk: "Saved ✓",
62
71
  title: "Unified Search",
63
72
  description: "Aggregated web search: 11 backends fan out concurrently, URL-deduped. Toggle backends, set API keys, tune globals — saves apply immediately.",
64
73
  save: "Save",
@@ -76,6 +85,7 @@ window.__ModuleLoader__.load({
76
85
  keyUnset: "not set",
77
86
  togglesTitle: "Enabled backends",
78
87
  globalsTitle: "Global parameters",
88
+ keysTitle: "API keys (saved to the credential store, effective immediately)",
79
89
  };
80
90
 
81
91
  //#region styles
@@ -114,6 +124,7 @@ window.__ModuleLoader__.load({
114
124
  this.keyState = new Map(); // envRef -> {configured}
115
125
  this.saving = false;
116
126
  this.failed = false;
127
+ this.savedAt = 0;
117
128
  this.store = createStore(this.projection());
118
129
  this.scope.subscribe(() => this.republish());
119
130
  }
@@ -138,6 +149,7 @@ window.__ModuleLoader__.load({
138
149
  keyState: Object.fromEntries(this.keyState),
139
150
  stagedKeys: Object.fromEntries([...this.stagedKeys].map(([k, v]) => [k, v.text])),
140
151
  stagedCount: this.staged.size + this.stagedKeys.size,
152
+ savedAt: this.savedAt,
141
153
  stagedText: (field) => {
142
154
  const s = this.staged.get(field);
143
155
  return s ? (s.clear ? "" : s.text) : undefined;
@@ -150,13 +162,14 @@ window.__ModuleLoader__.load({
150
162
 
151
163
  republish() { this.store.set(this.projection()); }
152
164
 
153
- edit(field, text) { this.staged.set(field, { text }); this.republish(); }
165
+ edit(field, text) { this.savedAt = 0; this.staged.set(field, { text }); this.republish(); }
154
166
  reset(field) { this.staged.delete(field); this.republish(); }
155
- editKey(envRef, text) { this.stagedKeys.set(envRef, { text }); this.republish(); }
167
+ editKey(envRef, text) { this.savedAt = 0; this.stagedKeys.set(envRef, { text }); this.republish(); }
156
168
 
157
169
  /** Toggles write immediately (no staging); globals/keys stage + save. */
158
170
  async setToggle(id, checked) {
159
171
  await this.scope.set("enable" + id.charAt(0).toUpperCase() + id.slice(1), checked);
172
+
160
173
  this.republish();
161
174
  }
162
175
 
@@ -185,8 +198,15 @@ window.__ModuleLoader__.load({
185
198
  if (staged.clear || staged.text === "") { await this.scope.unset(field); doneFields.push(field); continue; }
186
199
  if (GLOBAL_FIELDS.includes(field)) {
187
200
  const num = Number(staged.text);
188
- // Non-numeric input: keep the draft so the user can correct it.
189
- if (Number.isNaN(num)) { this.failed = true; continue; }
201
+ // Mirror the server schema bounds client-side so bad input is
202
+ // caught in the UI instead of a failed Host round-trip.
203
+ const BOUNDS = {
204
+ numResults: [1, 50],
205
+ concurrency: [1, 10],
206
+ backendTimeoutMs: [1000, 120000],
207
+ };
208
+ const [lo, hi] = BOUNDS[field] ?? [-Infinity, Infinity];
209
+ if (Number.isNaN(num) || num < lo || num > hi) { this.failed = true; continue; }
190
210
  await this.scope.set(field, num);
191
211
  doneFields.push(field);
192
212
  } else {
@@ -204,6 +224,7 @@ window.__ModuleLoader__.load({
204
224
  }
205
225
  for (const f of doneFields) this.staged.delete(f);
206
226
  for (const k of doneKeys) this.stagedKeys.delete(k);
227
+ if (!this.failed && (doneFields.length > 0 || doneKeys.length > 0)) this.savedAt = Date.now();
207
228
  } catch { this.failed = true; }
208
229
  this.saving = false;
209
230
  this.republish();
@@ -230,13 +251,18 @@ window.__ModuleLoader__.load({
230
251
  // The Host serves schema-resolved values, so defaults arrive materialized;
231
252
  // no client-side default list to drift against the server.
232
253
  const checked = props.value === true;
233
- return react_jsx_runtime.jsxs("label", { className: "wsc-toggle", children: [
254
+ // Status dot: green keyless-or-keyed, amber enabled-but-key-missing.
255
+ const needsKey = !!b.keyEnv && !props.keyConfigured;
256
+ const dotColor = needsKey ? (checked ? "#e6a700" : "#888") : "#2ea043";
257
+ return react_jsx_runtime.jsxs("label", { className: "wsc-toggle", style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
234
258
  react_jsx_runtime.jsx("input", {
235
259
  type: "checkbox",
236
260
  checked,
237
261
  onChange: (e) => props.onChange(e.target.checked),
238
262
  }),
263
+ react_jsx_runtime.jsx("span", { style: { width: 8, height: 8, borderRadius: "50%", background: dotColor, display: "inline-block" } }),
239
264
  react_jsx_runtime.jsx("span", { children: b.label }),
265
+ needsKey && checked && react_jsx_runtime.jsx("span", { style: { fontSize: 10, opacity: .65 }, children: props.t("keyMissing") }),
240
266
  ] });
241
267
  }
242
268
 
@@ -251,13 +277,23 @@ window.__ModuleLoader__.load({
251
277
 
252
278
  const toggleField = (id) => "enable" + id.charAt(0).toUpperCase() + id.slice(1);
253
279
 
280
+ // Status summary: enabled backends + configured keys at a glance.
281
+ const enabledCount = BACKENDS.filter((b) => state.value[toggleField(b.id)] === true).length;
282
+ const keyedBackends = BACKENDS.filter((b) => b.keyEnv);
283
+ const keysReady = keyedBackends.filter((b) => state.keyState[b.keyEnv]?.configured).length;
284
+
254
285
  return react_jsx_runtime.jsxs("div", { className: "wsc-card", children: [
286
+ react_jsx_runtime.jsx("div", { style: { display: "flex", gap: 14, alignItems: "center", padding: "10px 12px", marginBottom: 10, borderRadius: 8, background: "rgba(127,127,127,.08)", fontSize: 13 }, children: [
287
+ react_jsx_runtime.jsxs("span", { children: [props.t("statusEnabled"), ": ", react_jsx_runtime.jsx("b", { children: enabledCount + "/" + BACKENDS.length })] }),
288
+ react_jsx_runtime.jsxs("span", { children: [props.t("statusKeys"), ": ", react_jsx_runtime.jsx("b", { children: keysReady + "/" + keyedBackends.length })] }),
289
+ ] }),
255
290
  react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .65 }, children: props.t("description") }),
256
291
  react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("togglesTitle") }),
257
292
  react_jsx_runtime.jsx("div", { className: "wsc-toggles", children: BACKENDS.map((b) =>
258
293
  react_jsx_runtime.jsx(ToggleRow, {
259
294
  backend: b,
260
295
  value: state.value[toggleField(b.id)],
296
+ keyConfigured: !b.keyEnv || !!state.keyState[b.keyEnv]?.configured,
261
297
  onChange: (checked) => card.setToggle(b.id, checked),
262
298
  }, b.id)
263
299
  ) }),
@@ -276,7 +312,7 @@ window.__ModuleLoader__.load({
276
312
  react_jsx_runtime.jsx("input", { className: "wsc-input", value: state.stagedText("backendTimeoutMs") ?? String(state.value.backendTimeoutMs ?? ""), onChange: e => card.edit("backendTimeoutMs", e.target.value), inputMode: "numeric" }),
277
313
  ] }),
278
314
  ] }),
279
- react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: "API Keys" }),
315
+ react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("keysTitle") }),
280
316
  react_jsx_runtime.jsx("div", { className: "wsc-grid", children: BACKENDS.filter((b) => b.keyEnv).map((b) =>
281
317
  react_jsx_runtime.jsxs("div", { children: [
282
318
  react_jsx_runtime.jsxs("div", { className: "wsc-fieldHead", children: [
@@ -299,10 +335,12 @@ window.__ModuleLoader__.load({
299
335
  react_jsx_runtime.jsx("p", { className: "wsc-hint", children: props.t("keyHint") }),
300
336
  ] }, b.id)
301
337
  ) }),
338
+ state.stagedCount > 0 && react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .6 }, children: props.t("unsavedHint") }),
302
339
  react_jsx_runtime.jsxs("div", { className: "wsc-actions", children: [
303
- react_jsx_runtime.jsx("button", { className: "wsc-btn", disabled: state.saving, onClick: () => card.save(), children: state.saving ? props.t("saving") : props.t("save") }),
340
+ react_jsx_runtime.jsx("button", { className: "wsc-btn", disabled: state.saving || state.stagedCount === 0, onClick: () => card.save(), children: state.saving ? props.t("saving") : props.t("save") }),
304
341
  (state.stagedCount > 0) && react_jsx_runtime.jsx("button", { className: "wsc-btn wsc-btnGhost", onClick: () => card.discard(), children: props.t("discard") }),
305
- state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", children: props.t("failed") }),
342
+ state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e5534b" }, children: props.t("failed") }),
343
+ !state.failed && !state.saving && state.savedAt > 0 && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#2ea043" }, children: props.t("savedOk") }),
306
344
  ] }),
307
345
  ] });
308
346
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-websearch",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends — Exa + Parallel + DDG + SearXNG (keyless), DeepSeek + Anthropic + OpenAI + Brave + Tavily + Serper + Mojeek (key-gated via Settings credentials). Registers provider id \"unified\" for zero-config web_search tool.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",