@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 +7 -0
- package/examples/cordis.patch.yml +19 -11
- package/lib/client.js +46 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# @240xu/dsh-websearch
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@240xu/dsh-websearch)
|
|
4
|
+
[](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
|
-
#
|
|
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
|
-
#
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
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:
|
|
11
|
+
name: '@deepseek-ai/dsh-web-search-deepseek'
|
|
13
12
|
disabled: true
|
|
14
13
|
- id: web
|
|
15
|
-
name:
|
|
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
|
-
//
|
|
189
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
+
"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",
|