@240xu/dsh-websearch 2.1.0 → 2.2.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/backends/ddg.js +1 -1
- package/lib/client.js +31 -7
- package/lib/provider.js +17 -8
- package/lib/util/log.js +43 -29
- package/package.json +2 -2
package/lib/backends/ddg.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// text, and .result__snippet text. This is a fallback tier: Exa/Parallel
|
|
6
6
|
// are richer, DDG is the zero-dependency safety-net.
|
|
7
7
|
|
|
8
|
-
import { searchAborted, isAbortError } from "../util/abort.js";
|
|
8
|
+
import { searchAborted, isAbortError, throwIfSearchAborted } from "../util/abort.js";
|
|
9
9
|
import { applyDdgFilters, normalizeFilters } from "../util/filters.js";
|
|
10
10
|
import { WebError } from "@deepseek-ai/dsh-web";
|
|
11
11
|
|
package/lib/client.js
CHANGED
|
@@ -88,6 +88,7 @@ window.__ModuleLoader__.load({
|
|
|
88
88
|
dedupUrl: "按链接",
|
|
89
89
|
dedupTitle: "链接 + 标题(合并转载)",
|
|
90
90
|
langPlaceholder: "如 en、zh-CN,留空为默认",
|
|
91
|
+
envProvided: "环境变量提供", readOnly: "只读,无需填写", conflict: "设置已在其他端被修改,请重试", keyFailed: "该 Key 保存失败",
|
|
91
92
|
};
|
|
92
93
|
const en = {
|
|
93
94
|
nav: "Web Search",
|
|
@@ -131,6 +132,10 @@ window.__ModuleLoader__.load({
|
|
|
131
132
|
dedupUrl: "By URL",
|
|
132
133
|
dedupTitle: "URL + title (merge syndication)",
|
|
133
134
|
langPlaceholder: "e.g. en, zh-CN; blank = backend default",
|
|
135
|
+
envProvided: "provided by env",
|
|
136
|
+
readOnly: "read-only, nothing to fill",
|
|
137
|
+
conflict: "Changed elsewhere - please retry",
|
|
138
|
+
keyFailed: "failed to save this key",
|
|
134
139
|
};
|
|
135
140
|
|
|
136
141
|
//#region styles
|
|
@@ -169,6 +174,8 @@ window.__ModuleLoader__.load({
|
|
|
169
174
|
this.keyState = new Map(); // envRef -> {configured}
|
|
170
175
|
this.saving = false;
|
|
171
176
|
this.failed = false;
|
|
177
|
+
this.conflict = false;
|
|
178
|
+
this.failedKeys = new Set();
|
|
172
179
|
this.savedAt = 0;
|
|
173
180
|
this.store = createStore(this.projection());
|
|
174
181
|
this.scope.subscribe(() => this.republish());
|
|
@@ -199,6 +206,8 @@ window.__ModuleLoader__.load({
|
|
|
199
206
|
const s = this.staged.get(field);
|
|
200
207
|
return s ? (s.clear ? "" : s.text) : undefined;
|
|
201
208
|
},
|
|
209
|
+
conflict: this.conflict,
|
|
210
|
+
failedKeys: [...this.failedKeys],
|
|
202
211
|
saving: this.saving,
|
|
203
212
|
failed: this.failed,
|
|
204
213
|
version: (this.staged.size + ":" + [...this.staged.keys()].join(",")),
|
|
@@ -223,7 +232,7 @@ window.__ModuleLoader__.load({
|
|
|
223
232
|
const res = await this.connection.api.credentials.describe({ refs: [ref] });
|
|
224
233
|
if (!res.result.ok) return;
|
|
225
234
|
const view = res.result.value.credentials[ref];
|
|
226
|
-
this.keyState.set(ref, { configured: view?.configured ?? false });
|
|
235
|
+
this.keyState.set(ref, { configured: view?.configured ?? false, writable: view?.writable !== false });
|
|
227
236
|
} catch { /* ignore */ }
|
|
228
237
|
this.republish();
|
|
229
238
|
}
|
|
@@ -274,15 +283,26 @@ window.__ModuleLoader__.load({
|
|
|
274
283
|
for (const [ref, staged] of this.stagedKeys) {
|
|
275
284
|
const value = staged.text.trim();
|
|
276
285
|
if (value !== "") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
286
|
+
try {
|
|
287
|
+
await this.connection.api.credentials.set({ ref, value });
|
|
288
|
+
this.keyState.set(ref, { configured: true, writable: true });
|
|
289
|
+
doneKeys.push(ref);
|
|
290
|
+
this.failedKeys.delete(ref);
|
|
291
|
+
} catch (keyErr) {
|
|
292
|
+
// env-shadowed reference (fail-loud doctrine): isolate so other keys still land.
|
|
293
|
+
this.failed = true;
|
|
294
|
+
this.failedKeys.add(ref);
|
|
295
|
+
void keyErr;
|
|
296
|
+
}
|
|
280
297
|
}
|
|
281
298
|
}
|
|
282
299
|
for (const f of doneFields) this.staged.delete(f);
|
|
283
300
|
for (const k of doneKeys) this.stagedKeys.delete(k);
|
|
284
301
|
if (!this.failed && (doneFields.length > 0 || doneKeys.length > 0)) this.savedAt = Date.now();
|
|
285
|
-
} catch {
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.failed = true;
|
|
304
|
+
this.conflict = String(err).includes("SETTINGS_CONFLICT");
|
|
305
|
+
}
|
|
286
306
|
this.saving = false;
|
|
287
307
|
this.republish();
|
|
288
308
|
}
|
|
@@ -297,7 +317,7 @@ window.__ModuleLoader__.load({
|
|
|
297
317
|
reset: (f) => this.reset(f),
|
|
298
318
|
editKey: (r, t) => this.editKey(r, t),
|
|
299
319
|
save: () => this.save(),
|
|
300
|
-
discard: () => { this.staged.clear(); this.stagedKeys.clear(); this.failed = false; this.republish(); },
|
|
320
|
+
discard: () => { this.staged.clear(); this.stagedKeys.clear(); this.failed = false; this.conflict = false; this.failedKeys.clear(); this.republish(); },
|
|
301
321
|
};
|
|
302
322
|
}
|
|
303
323
|
}
|
|
@@ -458,15 +478,18 @@ window.__ModuleLoader__.load({
|
|
|
458
478
|
children: props.t("getKey"),
|
|
459
479
|
}),
|
|
460
480
|
react_jsx_runtime.jsx("span", { className: "wsc-badge" + (state.keyState[b.keyEnv]?.configured ? " wsc-badgeOn" : ""), children: state.keyState[b.keyEnv]?.configured ? props.t("keySet") : props.t("keyUnset") }),
|
|
481
|
+
state.keyState[b.keyEnv]?.configured && state.keyState[b.keyEnv]?.writable === false && react_jsx_runtime.jsx("span", { className: "wsc-badge", style: { opacity: .75 }, children: props.t("envProvided") }),
|
|
461
482
|
] }),
|
|
462
483
|
] }),
|
|
463
484
|
react_jsx_runtime.jsx("input", {
|
|
464
485
|
className: "wsc-keyInput", type: "password", autoComplete: "off",
|
|
465
|
-
|
|
486
|
+
disabled: state.keyState[b.keyEnv]?.configured && state.keyState[b.keyEnv]?.writable === false,
|
|
487
|
+
placeholder: (state.keyState[b.keyEnv]?.configured && state.keyState[b.keyEnv]?.writable === false) ? props.t("readOnly") : b.keyEnv,
|
|
466
488
|
value: state.stagedKeys[b.keyEnv] ?? "",
|
|
467
489
|
onChange: (e) => card.editKey(b.keyEnv, e.target.value),
|
|
468
490
|
}),
|
|
469
491
|
react_jsx_runtime.jsx("p", { className: "wsc-hint", children: props.t("keyHint") }),
|
|
492
|
+
state.failedKeys.includes(b.keyEnv) && react_jsx_runtime.jsx("p", { className: "wsc-hint", style: { color: "#e5534b" }, children: props.t("keyFailed") }),
|
|
470
493
|
] }, b.id)
|
|
471
494
|
) }),
|
|
472
495
|
state.stagedCount > 0 && react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .6 }, children: props.t("unsavedHint") }),
|
|
@@ -474,6 +497,7 @@ window.__ModuleLoader__.load({
|
|
|
474
497
|
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") }),
|
|
475
498
|
(state.stagedCount > 0) && react_jsx_runtime.jsx("button", { className: "wsc-btn wsc-btnGhost", onClick: () => card.discard(), children: props.t("discard") }),
|
|
476
499
|
state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e5534b" }, children: props.t("failed") }),
|
|
500
|
+
state.conflict && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e6a700" }, children: props.t("conflict") }),
|
|
477
501
|
!state.failed && !state.saving && state.savedAt > 0 && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#2ea043" }, children: props.t("savedOk") }),
|
|
478
502
|
] }),
|
|
479
503
|
] });
|
package/lib/provider.js
CHANGED
|
@@ -170,7 +170,10 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
170
170
|
let hardError = null;
|
|
171
171
|
const failures = [];
|
|
172
172
|
|
|
173
|
+
let backendIndex = 0;
|
|
173
174
|
for (const settled of results) {
|
|
175
|
+
const failedId = eligible[backendIndex] ? eligible[backendIndex].id : "unknown";
|
|
176
|
+
backendIndex++;
|
|
174
177
|
if (settled.status === "fulfilled") {
|
|
175
178
|
if (mergedContent === undefined && typeof settled.value?.content === "string" && settled.value.content.length > 0) {
|
|
176
179
|
mergedContent = settled.value.content;
|
|
@@ -182,9 +185,9 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
182
185
|
if (err instanceof WebError && err.code === "WEB_ABORTED") {
|
|
183
186
|
// Per-backend abort → soft unless caller signal fired.
|
|
184
187
|
if (signal?.aborted === true) hardAborted = true;
|
|
185
|
-
else failures.push({ id:
|
|
188
|
+
else failures.push({ id: failedId, err });
|
|
186
189
|
} else {
|
|
187
|
-
failures.push({ id:
|
|
190
|
+
failures.push({ id: failedId, err });
|
|
188
191
|
if (hardError === null) hardError = err;
|
|
189
192
|
}
|
|
190
193
|
}
|
|
@@ -215,13 +218,19 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
215
218
|
};
|
|
216
219
|
}
|
|
217
220
|
|
|
218
|
-
// All enabled backends failed
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
// All enabled backends failed - synthesize one meaningful error that
|
|
222
|
+
// names each backend's own reason so the caller (model or user) can act.
|
|
223
|
+
const detail = failures.slice(0, 4)
|
|
224
|
+
.map((f) => f.id + ": " + String(f.err instanceof Error ? f.err.message : f.err).slice(0, 120))
|
|
225
|
+
.join("; ");
|
|
226
|
+
const allKeyMissing =
|
|
227
|
+
failures.length > 0 &&
|
|
228
|
+
failures.every((f) => f.err instanceof WebError && f.err.code === "WEB_PROVIDER_UNAVAILABLE");
|
|
229
|
+
const hint = allKeyMissing
|
|
230
|
+
? " (every enabled backend is missing its API key - add keys in Settings > Web Search, or enable a keyless backend)"
|
|
231
|
+
: "";
|
|
223
232
|
throw new WebError(
|
|
224
|
-
`unified search: all enabled backends failed (${failures.length})
|
|
233
|
+
`unified search: all enabled backends failed (${failures.length}) - ${detail}` + hint,
|
|
225
234
|
"WEB_PROVIDER_ERROR",
|
|
226
235
|
{ cause: hardError ?? undefined },
|
|
227
236
|
);
|
package/lib/util/log.js
CHANGED
|
@@ -1,51 +1,65 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module dsh-websearch/util/log
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* so debugging / token-cost attribution is possible from the dsh session log.
|
|
6
|
-
* Mirrors the official dsh-web-search-deepseek `recordRequest` pattern but
|
|
7
|
-
* is per-backend (each backend records its own endpoint + body so it is
|
|
8
|
-
* auditable which backend fired which request).
|
|
4
|
+
* Per-backend request/outcome diagnostics.
|
|
9
5
|
*
|
|
10
|
-
*
|
|
11
|
-
* the
|
|
12
|
-
*
|
|
6
|
+
* IMPORTANT: these intentionally go to the HOST logger (ctx.logger), NOT to
|
|
7
|
+
* the session ledger. The session event vocabulary is a closed, generated set
|
|
8
|
+
* (@deepseek-ai/dsh-session known-event-types): the persistence read path
|
|
9
|
+
* refuses to interpret ANY session containing an envelope type outside that
|
|
10
|
+
* set unless the ENVELOPE carries ignorable: true -- and the runtime
|
|
11
|
+
* session.append() API provides no way to set that envelope marker (its opts
|
|
12
|
+
* carry only sourceEventSeqs/surfaceOp). A third-party plugin writing custom
|
|
13
|
+
* types therefore poisons the whole session log: writes succeed, the next
|
|
14
|
+
* continue/load refuses. Mirroring the in-repo web/deepseek-search-llm-request
|
|
15
|
+
* pattern is not available to out-of-repo packages by construction.
|
|
16
|
+
*
|
|
17
|
+
* Both emitters are best-effort and never throw: diagnostics must not break
|
|
18
|
+
* search.
|
|
13
19
|
*/
|
|
14
20
|
|
|
21
|
+
/** One-line summary safe for the host log. */
|
|
22
|
+
function summarize(request) {
|
|
23
|
+
try {
|
|
24
|
+
const body = request && request.body ? JSON.stringify(request.body) : "";
|
|
25
|
+
const trimmed = body.length > 200 ? body.slice(0, 200) + "..." : body;
|
|
26
|
+
return String((request && request.endpoint) || "") +
|
|
27
|
+
(request && request.toolName ? " tool=" + request.toolName : "") +
|
|
28
|
+
(trimmed ? " body=" + trimmed : "");
|
|
29
|
+
} catch {
|
|
30
|
+
return String(request && request.endpoint);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
|
-
* Record one backend request to the
|
|
35
|
+
* Record one backend request to the host log.
|
|
17
36
|
*
|
|
18
|
-
* @param {object} ctx - the Cordis plugin context (
|
|
19
|
-
* @param {string} backendId - e.g.
|
|
20
|
-
* @param {object} request - { endpoint, toolName, body
|
|
21
|
-
* filtered by the caller (do not pass Authorization/x-api-key here).
|
|
37
|
+
* @param {object} ctx - the Cordis plugin context (ctx.logger).
|
|
38
|
+
* @param {string} backendId - e.g. exa / parallel / brave.
|
|
39
|
+
* @param {object} request - { endpoint, toolName, body } - never includes secrets.
|
|
22
40
|
*/
|
|
23
41
|
export function recordBackendRequest(ctx, backendId, request) {
|
|
24
42
|
try {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (session?.append) {
|
|
29
|
-
session.append("web/unified-search-backend-request", { backendId, ...request, ignorable: true });
|
|
30
|
-
}
|
|
43
|
+
const logger = ctx && ctx.logger;
|
|
44
|
+
if (!logger || typeof logger.debug !== "function") return;
|
|
45
|
+
logger.debug("[websearch:" + backendId + "] request " + summarize(request));
|
|
31
46
|
} catch {
|
|
32
|
-
//
|
|
47
|
+
// best-effort; never fatal
|
|
33
48
|
}
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
37
|
-
* Record a backend's final outcome (
|
|
38
|
-
* Pairs with recordBackendRequest; useful for token-cost attribution.
|
|
52
|
+
* Record a backend's final outcome (sources count / error message) to the host log.
|
|
39
53
|
*/
|
|
40
54
|
export function recordBackendOutcome(ctx, backendId, outcome) {
|
|
41
55
|
try {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
const logger = ctx && ctx.logger;
|
|
57
|
+
if (!logger) return;
|
|
58
|
+
const status = outcome && typeof outcome.status === "string" ? outcome.status : "";
|
|
59
|
+
const line = "[websearch:" + backendId + "] outcome " + JSON.stringify(outcome ?? {});
|
|
60
|
+
if (status === "error" && typeof logger.warn === "function") logger.warn(line);
|
|
61
|
+
else if (typeof logger.debug === "function") logger.debug(line);
|
|
48
62
|
} catch {
|
|
49
63
|
// best-effort
|
|
50
64
|
}
|
|
51
|
-
}
|
|
65
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@240xu/dsh-websearch",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "Unified web search provider for DeepSeek Harness (ctx.web): 11 backends \u2014 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",
|
|
7
7
|
"exports": {
|