@240xu/dsh-websearch 2.1.1 → 2.3.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/README.md +15 -0
- package/lib/backends/searxng.js +2 -1
- package/lib/client.js +40 -8
- package/lib/index.js +3 -0
- package/lib/provider.js +37 -9
- package/lib/util/log.js +43 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -31,6 +31,18 @@ DSH([DeepSeek Harness](https://github.com/deepseek-ai/dsh))原生插件:
|
|
|
31
31
|
|
|
32
32
|
**Default enabled set**: `["exa", "parallel", "ddg", "searxng"]` — all keyless. The key-gated backends only join when their `available()` returns true (key present + baseURL reachable).
|
|
33
33
|
|
|
34
|
+
### SearXNG 实例 | Public instances
|
|
35
|
+
|
|
36
|
+
SearXNG 默认指向 `https://searx.be`;公共实例可用性随网络环境波动,失败时错误信息会提示换源。
|
|
37
|
+
在设置面板修改 `searxngBaseURL` 即可切换,常见候选:
|
|
38
|
+
|
|
39
|
+
| 实例 | 备注 |
|
|
40
|
+
|---|---|
|
|
41
|
+
| https://searx.be | 官方默认,部分地区不可达 |
|
|
42
|
+
| https://searx.tiekoetter.com | 稳定性较好 |
|
|
43
|
+
| https://priv.au | 隐私友好 |
|
|
44
|
+
| 自托管 | 最可靠,支持 API Key(`searxngApiKeyEnv`) |
|
|
45
|
+
|
|
34
46
|
## Install | 安装
|
|
35
47
|
|
|
36
48
|
### 方式一:作为 profile 依赖安装(推荐,规范做法)
|
|
@@ -81,6 +93,9 @@ pnpm install
|
|
|
81
93
|
|
|
82
94
|
**v2.1 结果策略**:设置面板新增「结果策略」分区。时间/语言过滤按各后端能力自动映射——Brave `freshness/country/search_lang`、Tavily `time_range`、Serper `tbs/hl/gl`(Google 日期语法)、SearXNG `time_range/language`、DDG `df`;不支持的后端自动忽略对应维度。内部超时以真实原因失败(backend "<id>" timed out after Nms),不再被静默降级为取消;Mojeek 的 API Key 改走 Authorization 头,不再出现在 URL 中。
|
|
83
95
|
|
|
96
|
+
**结果健康遥测(默认开)**:每次搜索的 `content` 尾部附一行 `[websearch backends] exa ✓5ms/3 · ddg ✗30000ms`,
|
|
97
|
+
模型可据此自诊断并建议用户调整设置;设置面板「结果策略」可关闭(`resultTelemetry`)。
|
|
98
|
+
|
|
84
99
|
**11 个后端开关**:`enableExa` / `enableParallel` / `enableDdg` / `enableSearxng`(默认开);`enableBrave` / `enableTavily` / `enableSerper` / `enableMojeek` / `enableDeepseek` / `enableAnthropic` / `enableOpenai`(默认关)
|
|
85
100
|
|
|
86
101
|
**API Key 授权**(credential-ref,填环境变量名,实际 key 存于 credentials 服务或环境变量):
|
package/lib/backends/searxng.js
CHANGED
|
@@ -66,7 +66,8 @@ export const searxngBackend = {
|
|
|
66
66
|
});
|
|
67
67
|
} catch (error) {
|
|
68
68
|
if (signal?.aborted === true || isAbortError(error)) throw searchAborted(signal, error);
|
|
69
|
-
throw new WebError("SearXNG request failed: " + error
|
|
69
|
+
throw new WebError("SearXNG request failed: " + error +
|
|
70
|
+
" (instance unreachable? point searxngBaseURL at another public instance)", "WEB_PROVIDER_ERROR", { cause: error });
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
if (!response.ok) {
|
package/lib/client.js
CHANGED
|
@@ -31,7 +31,7 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
dedupStrategy: ["url", "url+title"],
|
|
32
32
|
};
|
|
33
33
|
/** Boolean settings staged as "true"/"false" strings by the selects. */
|
|
34
|
-
const BOOLEAN_FIELDS = ["rerank", "safeSearch"];
|
|
34
|
+
const BOOLEAN_FIELDS = ["rerank", "safeSearch", "resultTelemetry"];
|
|
35
35
|
const KEYLESS = BACKENDS.filter((b) => !b.keyEnv);
|
|
36
36
|
const KEYED = BACKENDS.filter((b) => b.keyEnv);
|
|
37
37
|
|
|
@@ -88,6 +88,8 @@ window.__ModuleLoader__.load({
|
|
|
88
88
|
dedupUrl: "按链接",
|
|
89
89
|
dedupTitle: "链接 + 标题(合并转载)",
|
|
90
90
|
langPlaceholder: "如 en、zh-CN,留空为默认",
|
|
91
|
+
envProvided: "环境变量提供", readOnly: "只读,无需填写", conflict: "设置已在其他端被修改,请重试", keyFailed: "该 Key 保存失败",
|
|
92
|
+
telemetryLabel: "结果健康遥测",
|
|
91
93
|
};
|
|
92
94
|
const en = {
|
|
93
95
|
nav: "Web Search",
|
|
@@ -131,6 +133,11 @@ window.__ModuleLoader__.load({
|
|
|
131
133
|
dedupUrl: "By URL",
|
|
132
134
|
dedupTitle: "URL + title (merge syndication)",
|
|
133
135
|
langPlaceholder: "e.g. en, zh-CN; blank = backend default",
|
|
136
|
+
envProvided: "provided by env",
|
|
137
|
+
readOnly: "read-only, nothing to fill",
|
|
138
|
+
conflict: "Changed elsewhere - please retry",
|
|
139
|
+
keyFailed: "failed to save this key",
|
|
140
|
+
telemetryLabel: "Result health telemetry",
|
|
134
141
|
};
|
|
135
142
|
|
|
136
143
|
//#region styles
|
|
@@ -169,6 +176,8 @@ window.__ModuleLoader__.load({
|
|
|
169
176
|
this.keyState = new Map(); // envRef -> {configured}
|
|
170
177
|
this.saving = false;
|
|
171
178
|
this.failed = false;
|
|
179
|
+
this.conflict = false;
|
|
180
|
+
this.failedKeys = new Set();
|
|
172
181
|
this.savedAt = 0;
|
|
173
182
|
this.store = createStore(this.projection());
|
|
174
183
|
this.scope.subscribe(() => this.republish());
|
|
@@ -199,6 +208,8 @@ window.__ModuleLoader__.load({
|
|
|
199
208
|
const s = this.staged.get(field);
|
|
200
209
|
return s ? (s.clear ? "" : s.text) : undefined;
|
|
201
210
|
},
|
|
211
|
+
conflict: this.conflict,
|
|
212
|
+
failedKeys: [...this.failedKeys],
|
|
202
213
|
saving: this.saving,
|
|
203
214
|
failed: this.failed,
|
|
204
215
|
version: (this.staged.size + ":" + [...this.staged.keys()].join(",")),
|
|
@@ -223,7 +234,7 @@ window.__ModuleLoader__.load({
|
|
|
223
234
|
const res = await this.connection.api.credentials.describe({ refs: [ref] });
|
|
224
235
|
if (!res.result.ok) return;
|
|
225
236
|
const view = res.result.value.credentials[ref];
|
|
226
|
-
this.keyState.set(ref, { configured: view?.configured ?? false });
|
|
237
|
+
this.keyState.set(ref, { configured: view?.configured ?? false, writable: view?.writable !== false });
|
|
227
238
|
} catch { /* ignore */ }
|
|
228
239
|
this.republish();
|
|
229
240
|
}
|
|
@@ -274,15 +285,26 @@ window.__ModuleLoader__.load({
|
|
|
274
285
|
for (const [ref, staged] of this.stagedKeys) {
|
|
275
286
|
const value = staged.text.trim();
|
|
276
287
|
if (value !== "") {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
288
|
+
try {
|
|
289
|
+
await this.connection.api.credentials.set({ ref, value });
|
|
290
|
+
this.keyState.set(ref, { configured: true, writable: true });
|
|
291
|
+
doneKeys.push(ref);
|
|
292
|
+
this.failedKeys.delete(ref);
|
|
293
|
+
} catch (keyErr) {
|
|
294
|
+
// env-shadowed reference (fail-loud doctrine): isolate so other keys still land.
|
|
295
|
+
this.failed = true;
|
|
296
|
+
this.failedKeys.add(ref);
|
|
297
|
+
void keyErr;
|
|
298
|
+
}
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
for (const f of doneFields) this.staged.delete(f);
|
|
283
302
|
for (const k of doneKeys) this.stagedKeys.delete(k);
|
|
284
303
|
if (!this.failed && (doneFields.length > 0 || doneKeys.length > 0)) this.savedAt = Date.now();
|
|
285
|
-
} catch {
|
|
304
|
+
} catch (err) {
|
|
305
|
+
this.failed = true;
|
|
306
|
+
this.conflict = String(err).includes("SETTINGS_CONFLICT");
|
|
307
|
+
}
|
|
286
308
|
this.saving = false;
|
|
287
309
|
this.republish();
|
|
288
310
|
}
|
|
@@ -297,7 +319,7 @@ window.__ModuleLoader__.load({
|
|
|
297
319
|
reset: (f) => this.reset(f),
|
|
298
320
|
editKey: (r, t) => this.editKey(r, t),
|
|
299
321
|
save: () => this.save(),
|
|
300
|
-
discard: () => { this.staged.clear(); this.stagedKeys.clear(); this.failed = false; this.republish(); },
|
|
322
|
+
discard: () => { this.staged.clear(); this.stagedKeys.clear(); this.failed = false; this.conflict = false; this.failedKeys.clear(); this.republish(); },
|
|
301
323
|
};
|
|
302
324
|
}
|
|
303
325
|
}
|
|
@@ -445,6 +467,12 @@ window.__ModuleLoader__.load({
|
|
|
445
467
|
options: boolOptions,
|
|
446
468
|
onChange: (v) => card.edit("safeSearch", v),
|
|
447
469
|
}),
|
|
470
|
+
react_jsx_runtime.jsx(SelectField, {
|
|
471
|
+
label: props.t("telemetryLabel"),
|
|
472
|
+
value: selVal("resultTelemetry", "true"),
|
|
473
|
+
options: boolOptions,
|
|
474
|
+
onChange: (v) => card.edit("resultTelemetry", v),
|
|
475
|
+
}),
|
|
448
476
|
] }),
|
|
449
477
|
react_jsx_runtime.jsx("h4", { className: "wsc-sectionTitle", children: props.t("keysTitle") }),
|
|
450
478
|
react_jsx_runtime.jsx("div", { className: "wsc-grid", children: BACKENDS.filter((b) => b.keyEnv).map((b) =>
|
|
@@ -458,15 +486,18 @@ window.__ModuleLoader__.load({
|
|
|
458
486
|
children: props.t("getKey"),
|
|
459
487
|
}),
|
|
460
488
|
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") }),
|
|
489
|
+
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
490
|
] }),
|
|
462
491
|
] }),
|
|
463
492
|
react_jsx_runtime.jsx("input", {
|
|
464
493
|
className: "wsc-keyInput", type: "password", autoComplete: "off",
|
|
465
|
-
|
|
494
|
+
disabled: state.keyState[b.keyEnv]?.configured && state.keyState[b.keyEnv]?.writable === false,
|
|
495
|
+
placeholder: (state.keyState[b.keyEnv]?.configured && state.keyState[b.keyEnv]?.writable === false) ? props.t("readOnly") : b.keyEnv,
|
|
466
496
|
value: state.stagedKeys[b.keyEnv] ?? "",
|
|
467
497
|
onChange: (e) => card.editKey(b.keyEnv, e.target.value),
|
|
468
498
|
}),
|
|
469
499
|
react_jsx_runtime.jsx("p", { className: "wsc-hint", children: props.t("keyHint") }),
|
|
500
|
+
state.failedKeys.includes(b.keyEnv) && react_jsx_runtime.jsx("p", { className: "wsc-hint", style: { color: "#e5534b" }, children: props.t("keyFailed") }),
|
|
470
501
|
] }, b.id)
|
|
471
502
|
) }),
|
|
472
503
|
state.stagedCount > 0 && react_jsx_runtime.jsx("p", { style: { margin: 0, fontSize: 12, opacity: .6 }, children: props.t("unsavedHint") }),
|
|
@@ -474,6 +505,7 @@ window.__ModuleLoader__.load({
|
|
|
474
505
|
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
506
|
(state.stagedCount > 0) && react_jsx_runtime.jsx("button", { className: "wsc-btn wsc-btnGhost", onClick: () => card.discard(), children: props.t("discard") }),
|
|
476
507
|
state.failed && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e5534b" }, children: props.t("failed") }),
|
|
508
|
+
state.conflict && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#e6a700" }, children: props.t("conflict") }),
|
|
477
509
|
!state.failed && !state.saving && state.savedAt > 0 && react_jsx_runtime.jsx("span", { className: "wsc-status", style: { color: "#2ea043" }, children: props.t("savedOk") }),
|
|
478
510
|
] }),
|
|
479
511
|
] });
|
package/lib/index.js
CHANGED
|
@@ -141,6 +141,8 @@ export const Config = z.object({
|
|
|
141
141
|
.description("去重策略:url 仅按链接;url+title 额外合并同标题的转载镜像"),
|
|
142
142
|
rerank: z.boolean().default(false)
|
|
143
143
|
.description("按查询词相关性重排序(确定性、并列时保持后端优先级)"),
|
|
144
|
+
resultTelemetry: z.boolean().default(true)
|
|
145
|
+
.description("在搜索结果尾部附一行后端健康遥测(状态+耗时),模型可据此自诊断"),
|
|
144
146
|
|
|
145
147
|
// Enable toggles (one per backend)
|
|
146
148
|
enableExa: z.boolean().default(true).description(DEFAULT_LABELS.exa),
|
|
@@ -265,6 +267,7 @@ export function resolveOptions(ctx, config) {
|
|
|
265
267
|
// v2.1 shaping options consumed by the provider merge pipeline.
|
|
266
268
|
dedupStrategy: config?.dedupStrategy === "url+title" ? "url+title" : "url",
|
|
267
269
|
rerank: config?.rerank === true,
|
|
270
|
+
resultTelemetry: config?.resultTelemetry !== false,
|
|
268
271
|
filters: {
|
|
269
272
|
recency: typeof config?.recency === "string" && config.recency !== "any" ? config.recency : undefined,
|
|
270
273
|
lang: language.length > 0 ? language : undefined,
|
package/lib/provider.js
CHANGED
|
@@ -119,6 +119,7 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
119
119
|
const effectiveRequest =
|
|
120
120
|
Object.keys(filters).length > 0 ? { ...request, filters } : request;
|
|
121
121
|
|
|
122
|
+
const telemetry = [];
|
|
122
123
|
// Concurrency limiter
|
|
123
124
|
const limit = createLimiter(opts.concurrency ?? 6);
|
|
124
125
|
|
|
@@ -128,6 +129,7 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
128
129
|
// reclassified as WEB_PROVIDER_ERROR so timeouts surface as real
|
|
129
130
|
// failures instead of being silently demoted like user cancellations.
|
|
130
131
|
const runBackend = ({ id, backend, beOpts, hooks }) => {
|
|
132
|
+
const startedAtMs = Date.now();
|
|
131
133
|
const controller = new AbortController();
|
|
132
134
|
const mergedSignal = signal
|
|
133
135
|
? AbortSignal.any([signal, controller.signal])
|
|
@@ -135,6 +137,16 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
135
137
|
const timeoutId = setTimeout(() => controller.abort(), backendTimeoutMs);
|
|
136
138
|
return backend
|
|
137
139
|
.search(effectiveRequest, mergedSignal, beOpts, hooks)
|
|
140
|
+
.then(
|
|
141
|
+
(value) => {
|
|
142
|
+
telemetry.push({ id, ok: true, ms: Date.now() - startedAtMs, count: value?.sources?.length ?? 0 });
|
|
143
|
+
return value;
|
|
144
|
+
},
|
|
145
|
+
(err) => {
|
|
146
|
+
telemetry.push({ id, ok: false, ms: Date.now() - startedAtMs, error: String(err instanceof Error ? err.message : err).slice(0, 80) });
|
|
147
|
+
throw err;
|
|
148
|
+
},
|
|
149
|
+
)
|
|
138
150
|
.catch((err) => {
|
|
139
151
|
if (controller.signal.aborted && !(signal && signal.aborted)) {
|
|
140
152
|
throw new WebError(
|
|
@@ -170,7 +182,10 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
170
182
|
let hardError = null;
|
|
171
183
|
const failures = [];
|
|
172
184
|
|
|
185
|
+
let backendIndex = 0;
|
|
173
186
|
for (const settled of results) {
|
|
187
|
+
const failedId = eligible[backendIndex] ? eligible[backendIndex].id : "unknown";
|
|
188
|
+
backendIndex++;
|
|
174
189
|
if (settled.status === "fulfilled") {
|
|
175
190
|
if (mergedContent === undefined && typeof settled.value?.content === "string" && settled.value.content.length > 0) {
|
|
176
191
|
mergedContent = settled.value.content;
|
|
@@ -182,9 +197,9 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
182
197
|
if (err instanceof WebError && err.code === "WEB_ABORTED") {
|
|
183
198
|
// Per-backend abort → soft unless caller signal fired.
|
|
184
199
|
if (signal?.aborted === true) hardAborted = true;
|
|
185
|
-
else failures.push({ id:
|
|
200
|
+
else failures.push({ id: failedId, err });
|
|
186
201
|
} else {
|
|
187
|
-
failures.push({ id:
|
|
202
|
+
failures.push({ id: failedId, err });
|
|
188
203
|
if (hardError === null) hardError = err;
|
|
189
204
|
}
|
|
190
205
|
}
|
|
@@ -208,20 +223,33 @@ export function createUnifiedSearchProvider({ ctx, resolveOptions, backends }) {
|
|
|
208
223
|
// If we got ANY sources, return them even if some backends failed.
|
|
209
224
|
if (ordered.length > 0) {
|
|
210
225
|
const sources = ordered.slice(0, maxResults);
|
|
226
|
+
let outContent = mergedContent;
|
|
227
|
+
if (opts.resultTelemetry !== false && telemetry.length > 0) {
|
|
228
|
+
const line = "[websearch backends] " + telemetry
|
|
229
|
+
.map((t) => t.id + (t.ok ? " ✓" : " ✗") + t.ms + "ms" + (t.ok ? "/" + t.count : ""))
|
|
230
|
+
.join(" · ");
|
|
231
|
+
outContent = outContent === undefined ? line : outContent + "\n\n" + line;
|
|
232
|
+
}
|
|
211
233
|
return {
|
|
212
234
|
sources,
|
|
213
|
-
...(
|
|
235
|
+
...(outContent !== undefined ? { content: outContent } : {}),
|
|
214
236
|
truncated: ordered.length > maxResults,
|
|
215
237
|
};
|
|
216
238
|
}
|
|
217
239
|
|
|
218
|
-
// All enabled backends failed
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
240
|
+
// All enabled backends failed - synthesize one meaningful error that
|
|
241
|
+
// names each backend's own reason so the caller (model or user) can act.
|
|
242
|
+
const detail = failures.slice(0, 4)
|
|
243
|
+
.map((f) => f.id + ": " + String(f.err instanceof Error ? f.err.message : f.err).slice(0, 120))
|
|
244
|
+
.join("; ");
|
|
245
|
+
const allKeyMissing =
|
|
246
|
+
failures.length > 0 &&
|
|
247
|
+
failures.every((f) => f.err instanceof WebError && f.err.code === "WEB_PROVIDER_UNAVAILABLE");
|
|
248
|
+
const hint = allKeyMissing
|
|
249
|
+
? " (every enabled backend is missing its API key - add keys in Settings > Web Search, or enable a keyless backend)"
|
|
250
|
+
: "";
|
|
223
251
|
throw new WebError(
|
|
224
|
-
`unified search: all enabled backends failed (${failures.length})
|
|
252
|
+
`unified search: all enabled backends failed (${failures.length}) - ${detail}` + hint,
|
|
225
253
|
"WEB_PROVIDER_ERROR",
|
|
226
254
|
{ cause: hardError ?? undefined },
|
|
227
255
|
);
|
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.3.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": {
|