@240xu/dsh-websearch 2.0.4 → 2.0.6

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.
Files changed (2) hide show
  1. package/lib/client.js +55 -9
  2. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -25,6 +25,8 @@ window.__ModuleLoader__.load({
25
25
  { id: "openai", keyEnv: "OPENAI_API_KEY", label: "OpenAI" },
26
26
  ];
27
27
  const GLOBAL_FIELDS = ["numResults", "concurrency", "backendTimeoutMs"];
28
+ const KEYLESS = BACKENDS.filter((b) => !b.keyEnv);
29
+ const KEYED = BACKENDS.filter((b) => b.keyEnv);
28
30
 
29
31
  /** Where the user goes to get/rotate each key. */
30
32
  const KEY_URLS = {
@@ -39,6 +41,12 @@ window.__ModuleLoader__.load({
39
41
 
40
42
  const zh = {
41
43
  nav: "搜索",
44
+ groupFree: "免密钥(开箱即用)",
45
+ groupKeyed: "需要 API Key",
46
+ statusEnabled: "已启用后端",
47
+ statusKeys: "已配置密钥",
48
+ keyMissing: "缺密钥",
49
+ unsavedHint: "有未保存的修改", savedOk: "已保存 ✓",
42
50
  title: "统一搜索 (Unified Search)",
43
51
  description: "聚合网络搜索:11 个后端并发 fan-out、URL 去重合并。开关后端、填 API Key、调参数,保存即生效。",
44
52
  save: "保存",
@@ -56,9 +64,16 @@ window.__ModuleLoader__.load({
56
64
  keyUnset: "未配置",
57
65
  togglesTitle: "启用后端",
58
66
  globalsTitle: "全局参数",
67
+ keysTitle: "API 密钥(保存后写入凭证库,立即生效)",
59
68
  };
60
69
  const en = {
61
70
  nav: "Web Search",
71
+ groupFree: "Keyless (ready to use)",
72
+ groupKeyed: "Requires API key",
73
+ statusEnabled: "Backends enabled",
74
+ statusKeys: "Keys configured",
75
+ keyMissing: "key missing",
76
+ unsavedHint: "You have unsaved changes", savedOk: "Saved ✓",
62
77
  title: "Unified Search",
63
78
  description: "Aggregated web search: 11 backends fan out concurrently, URL-deduped. Toggle backends, set API keys, tune globals — saves apply immediately.",
64
79
  save: "Save",
@@ -76,6 +91,7 @@ window.__ModuleLoader__.load({
76
91
  keyUnset: "not set",
77
92
  togglesTitle: "Enabled backends",
78
93
  globalsTitle: "Global parameters",
94
+ keysTitle: "API keys (saved to the credential store, effective immediately)",
79
95
  };
80
96
 
81
97
  //#region styles
@@ -114,6 +130,7 @@ window.__ModuleLoader__.load({
114
130
  this.keyState = new Map(); // envRef -> {configured}
115
131
  this.saving = false;
116
132
  this.failed = false;
133
+ this.savedAt = 0;
117
134
  this.store = createStore(this.projection());
118
135
  this.scope.subscribe(() => this.republish());
119
136
  }
@@ -138,6 +155,7 @@ window.__ModuleLoader__.load({
138
155
  keyState: Object.fromEntries(this.keyState),
139
156
  stagedKeys: Object.fromEntries([...this.stagedKeys].map(([k, v]) => [k, v.text])),
140
157
  stagedCount: this.staged.size + this.stagedKeys.size,
158
+ savedAt: this.savedAt,
141
159
  stagedText: (field) => {
142
160
  const s = this.staged.get(field);
143
161
  return s ? (s.clear ? "" : s.text) : undefined;
@@ -150,13 +168,14 @@ window.__ModuleLoader__.load({
150
168
 
151
169
  republish() { this.store.set(this.projection()); }
152
170
 
153
- edit(field, text) { this.staged.set(field, { text }); this.republish(); }
171
+ edit(field, text) { this.savedAt = 0; this.staged.set(field, { text }); this.republish(); }
154
172
  reset(field) { this.staged.delete(field); this.republish(); }
155
- editKey(envRef, text) { this.stagedKeys.set(envRef, { text }); this.republish(); }
173
+ editKey(envRef, text) { this.savedAt = 0; this.stagedKeys.set(envRef, { text }); this.republish(); }
156
174
 
157
175
  /** Toggles write immediately (no staging); globals/keys stage + save. */
158
176
  async setToggle(id, checked) {
159
177
  await this.scope.set("enable" + id.charAt(0).toUpperCase() + id.slice(1), checked);
178
+
160
179
  this.republish();
161
180
  }
162
181
 
@@ -211,6 +230,7 @@ window.__ModuleLoader__.load({
211
230
  }
212
231
  for (const f of doneFields) this.staged.delete(f);
213
232
  for (const k of doneKeys) this.stagedKeys.delete(k);
233
+ if (!this.failed && (doneFields.length > 0 || doneKeys.length > 0)) this.savedAt = Date.now();
214
234
  } catch { this.failed = true; }
215
235
  this.saving = false;
216
236
  this.republish();
@@ -237,13 +257,18 @@ window.__ModuleLoader__.load({
237
257
  // The Host serves schema-resolved values, so defaults arrive materialized;
238
258
  // no client-side default list to drift against the server.
239
259
  const checked = props.value === true;
240
- return react_jsx_runtime.jsxs("label", { className: "wsc-toggle", children: [
260
+ // Status dot: green keyless-or-keyed, amber enabled-but-key-missing.
261
+ const needsKey = !!b.keyEnv && !props.keyConfigured;
262
+ const dotColor = needsKey ? (checked ? "#e6a700" : "#888") : "#2ea043";
263
+ return react_jsx_runtime.jsxs("label", { className: "wsc-toggle", style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }, children: [
241
264
  react_jsx_runtime.jsx("input", {
242
265
  type: "checkbox",
243
266
  checked,
244
267
  onChange: (e) => props.onChange(e.target.checked),
245
268
  }),
269
+ react_jsx_runtime.jsx("span", { style: { width: 8, height: 8, borderRadius: "50%", background: dotColor, display: "inline-block" } }),
246
270
  react_jsx_runtime.jsx("span", { children: b.label }),
271
+ needsKey && checked && react_jsx_runtime.jsx("span", { style: { fontSize: 10, opacity: .65 }, children: props.t("keyMissing") }),
247
272
  ] });
248
273
  }
249
274
 
@@ -258,13 +283,32 @@ window.__ModuleLoader__.load({
258
283
 
259
284
  const toggleField = (id) => "enable" + id.charAt(0).toUpperCase() + id.slice(1);
260
285
 
286
+ // Status summary: enabled backends + configured keys at a glance.
287
+ const enabledCount = BACKENDS.filter((b) => state.value[toggleField(b.id)] === true).length;
288
+ const keyedBackends = BACKENDS.filter((b) => b.keyEnv);
289
+ const keysReady = keyedBackends.filter((b) => state.keyState[b.keyEnv]?.configured).length;
290
+
261
291
  return react_jsx_runtime.jsxs("div", { className: "wsc-card", children: [
292
+ 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: [
293
+ react_jsx_runtime.jsxs("span", { children: [props.t("statusEnabled"), ": ", react_jsx_runtime.jsx("b", { children: enabledCount + "/" + BACKENDS.length })] }),
294
+ react_jsx_runtime.jsxs("span", { children: [props.t("statusKeys"), ": ", react_jsx_runtime.jsx("b", { children: keysReady + "/" + keyedBackends.length })] }),
295
+ ] }),
262
296
  react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .65 }, children: props.t("description") }),
263
- react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("togglesTitle") }),
264
- react_jsx_runtime.jsx("div", { className: "wsc-toggles", children: BACKENDS.map((b) =>
297
+ react_jsx_runtime.jsx("h5", { className: "wsc-sectionTitle", children: props.t("groupFree") }),
298
+ react_jsx_runtime.jsx("div", { className: "wsc-toggles", children: KEYLESS.map((b) =>
299
+ react_jsx_runtime.jsx(ToggleRow, {
300
+ backend: b,
301
+ value: state.value[toggleField(b.id)],
302
+ keyConfigured: true,
303
+ onChange: (checked) => card.setToggle(b.id, checked),
304
+ }, b.id)
305
+ ) }),
306
+ react_jsx_runtime.jsx("h5", { className: "wsc-sectionTitle", children: props.t("groupKeyed") }),
307
+ react_jsx_runtime.jsx("div", { className: "wsc-toggles", children: KEYED.map((b) =>
265
308
  react_jsx_runtime.jsx(ToggleRow, {
266
309
  backend: b,
267
310
  value: state.value[toggleField(b.id)],
311
+ keyConfigured: !!state.keyState[b.keyEnv]?.configured,
268
312
  onChange: (checked) => card.setToggle(b.id, checked),
269
313
  }, b.id)
270
314
  ) }),
@@ -283,7 +327,7 @@ window.__ModuleLoader__.load({
283
327
  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" }),
284
328
  ] }),
285
329
  ] }),
286
- react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: "API Keys" }),
330
+ react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("keysTitle") }),
287
331
  react_jsx_runtime.jsx("div", { className: "wsc-grid", children: BACKENDS.filter((b) => b.keyEnv).map((b) =>
288
332
  react_jsx_runtime.jsxs("div", { children: [
289
333
  react_jsx_runtime.jsxs("div", { className: "wsc-fieldHead", children: [
@@ -306,10 +350,12 @@ window.__ModuleLoader__.load({
306
350
  react_jsx_runtime.jsx("p", { className: "wsc-hint", children: props.t("keyHint") }),
307
351
  ] }, b.id)
308
352
  ) }),
353
+ state.stagedCount > 0 && react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .6 }, children: props.t("unsavedHint") }),
309
354
  react_jsx_runtime.jsxs("div", { className: "wsc-actions", children: [
310
- react_jsx_runtime.jsx("button", { className: "wsc-btn", disabled: state.saving, onClick: () => card.save(), children: state.saving ? props.t("saving") : props.t("save") }),
355
+ 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") }),
311
356
  (state.stagedCount > 0) && react_jsx_runtime.jsx("button", { className: "wsc-btn wsc-btnGhost", onClick: () => card.discard(), children: props.t("discard") }),
312
- state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", children: props.t("failed") }),
357
+ state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e5534b" }, children: props.t("failed") }),
358
+ !state.failed && !state.saving && state.savedAt > 0 && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#2ea043" }, children: props.t("savedOk") }),
313
359
  ] }),
314
360
  ] });
315
361
  }
@@ -328,7 +374,7 @@ window.__ModuleLoader__.load({
328
374
  name: "settings.section",
329
375
  id: "websearch",
330
376
  order: 16,
331
- label: () => t("nav"),
377
+ label: () => ctx.locale.bind(NS)("nav"),
332
378
  locale: NS,
333
379
  inject: () => controller.inject(),
334
380
  }, UnifiedSearchSection));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@240xu/dsh-websearch",
3
- "version": "2.0.4",
3
+ "version": "2.0.6",
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",