dsh-web-search-pro 0.2.0 → 0.2.3
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/CHANGELOG.md +179 -0
- package/README.md +197 -32
- package/assets/skills/dsh-web-search-pro/SKILL.md +14 -13
- package/assets/skills/dsh-web-search-pro/references/config.md +36 -0
- package/assets/skills/dsh-web-search-pro/references/sources.md +19 -4
- package/assets/skills/dsh-web-search-pro/references/troubleshooting.md +3 -1
- package/catalog/sources.v1.json +22 -12
- package/lib/actions/approval.js +50 -1
- package/lib/actions/approval.js.map +1 -1
- package/lib/actions/config.js +274 -0
- package/lib/actions/config.js.map +1 -0
- package/lib/actions/errors.js +8 -1
- package/lib/actions/errors.js.map +1 -1
- package/lib/actions/format.js +10 -0
- package/lib/actions/format.js.map +1 -1
- package/lib/actions/index-view.js +2 -2
- package/lib/actions/index-view.js.map +1 -1
- package/lib/actions/read.js +7 -3
- package/lib/actions/read.js.map +1 -1
- package/lib/actions/registry.js +3 -1
- package/lib/actions/registry.js.map +1 -1
- package/lib/actions/run.js +2 -2
- package/lib/actions/run.js.map +1 -1
- package/lib/actions/schema.js +18 -0
- package/lib/actions/schema.js.map +1 -1
- package/lib/actions/search.js +12 -6
- package/lib/actions/search.js.map +1 -1
- package/lib/actions/sources.js +116 -41
- package/lib/actions/sources.js.map +1 -1
- package/lib/actions/types.js +19 -1
- package/lib/actions/types.js.map +1 -1
- package/lib/browser-access.js +25 -0
- package/lib/browser-access.js.map +1 -1
- package/lib/catalog/recommend.js +11 -3
- package/lib/catalog/recommend.js.map +1 -1
- package/lib/catalog/schema.js +3 -1
- package/lib/catalog/schema.js.map +1 -1
- package/lib/cli/builtin-specs.js +341 -0
- package/lib/cli/builtin-specs.js.map +1 -0
- package/lib/cli/chain.js +193 -0
- package/lib/cli/chain.js.map +1 -0
- package/lib/cli/chains-spec.js +118 -0
- package/lib/cli/chains-spec.js.map +1 -0
- package/lib/cli/engine.js +99 -0
- package/lib/cli/engine.js.map +1 -0
- package/lib/cli/opencli-sites.js +83 -0
- package/lib/cli/opencli-sites.js.map +1 -0
- package/lib/cli/opencli.js +153 -0
- package/lib/cli/opencli.js.map +1 -0
- package/lib/cli/probe.js +128 -0
- package/lib/cli/probe.js.map +1 -0
- package/lib/cli/runner.js +564 -0
- package/lib/cli/runner.js.map +1 -0
- package/lib/cli/spec.js +496 -0
- package/lib/cli/spec.js.map +1 -0
- package/lib/cli/time-text.js +79 -0
- package/lib/cli/time-text.js.map +1 -0
- package/lib/cli/trial.js +57 -0
- package/lib/cli/trial.js.map +1 -0
- package/lib/client/SettingsCard.js +14 -32
- package/lib/client/SettingsCard.js.map +1 -1
- package/lib/client/agent-mark.js +27 -0
- package/lib/client/agent-mark.js.map +1 -0
- package/lib/client/card-frame.js +118 -0
- package/lib/client/card-frame.js.map +1 -0
- package/lib/client/cli-form.js +257 -0
- package/lib/client/cli-form.js.map +1 -0
- package/lib/client/field-docs.js +130 -0
- package/lib/client/field-docs.js.map +1 -0
- package/lib/client/field-keys.js +81 -0
- package/lib/client/field-keys.js.map +1 -0
- package/lib/client/field-rows.js +71 -0
- package/lib/client/field-rows.js.map +1 -0
- package/lib/client/fill.js +3 -0
- package/lib/client/fill.js.map +1 -0
- package/lib/client/form-specs.js +15 -1
- package/lib/client/form-specs.js.map +1 -1
- package/lib/client/form.js +296 -27
- package/lib/client/form.js.map +1 -1
- package/lib/client/group-advanced.js +23 -0
- package/lib/client/group-advanced.js.map +1 -0
- package/lib/client/group-agent.js +28 -0
- package/lib/client/group-agent.js.map +1 -0
- package/lib/client/group-cli.js +136 -0
- package/lib/client/group-cli.js.map +1 -0
- package/lib/client/group-common.js +29 -0
- package/lib/client/group-common.js.map +1 -0
- package/lib/client/group-judge.js +67 -0
- package/lib/client/group-judge.js.map +1 -0
- package/lib/client/group-order.js +201 -0
- package/lib/client/group-order.js.map +1 -0
- package/lib/client/group-runtime.js +33 -0
- package/lib/client/group-runtime.js.map +1 -0
- package/lib/client/group-sources.js +83 -0
- package/lib/client/group-sources.js.map +1 -0
- package/lib/client/groups-basic.js +36 -0
- package/lib/client/groups-basic.js.map +1 -0
- package/lib/client/index.js.map +1 -1
- package/lib/client/json-list.js +50 -0
- package/lib/client/json-list.js.map +1 -0
- package/lib/client/locales.js +666 -215
- package/lib/client/locales.js.map +1 -1
- package/lib/client/order.js +192 -0
- package/lib/client/order.js.map +1 -0
- package/lib/client/overlay-ops.js +46 -0
- package/lib/client/overlay-ops.js.map +1 -0
- package/lib/client/sources-table.js +1 -0
- package/lib/client/sources-table.js.map +1 -1
- package/lib/client/styles.js +74 -47
- package/lib/client/styles.js.map +1 -1
- package/lib/client/text-view.js +98 -0
- package/lib/client/text-view.js.map +1 -0
- package/lib/client/ui-kit.js +120 -0
- package/lib/client/ui-kit.js.map +1 -0
- package/lib/client/validators.js +22 -0
- package/lib/client/validators.js.map +1 -1
- package/lib/client.js +9151 -1987
- package/lib/client.js.map +1 -1
- package/lib/config-fillers.js +53 -0
- package/lib/config-fillers.js.map +1 -0
- package/lib/config-groups.js +49 -0
- package/lib/config-groups.js.map +1 -0
- package/lib/config.js +87 -107
- package/lib/config.js.map +1 -1
- package/lib/default-chain.js +66 -0
- package/lib/default-chain.js.map +1 -0
- package/lib/deps.js +109 -102
- package/lib/deps.js.map +1 -1
- package/lib/engine-error.js +19 -0
- package/lib/engine-error.js.map +1 -0
- package/lib/engines.js +32 -115
- package/lib/engines.js.map +1 -1
- package/lib/fetch.js +23 -4
- package/lib/fetch.js.map +1 -1
- package/lib/index.js +30 -5
- package/lib/index.js.map +1 -1
- package/lib/overlay-types.js +88 -0
- package/lib/overlay-types.js.map +1 -0
- package/lib/overlay.js +683 -0
- package/lib/overlay.js.map +1 -0
- package/lib/pipeline/candidates.js +27 -1
- package/lib/pipeline/candidates.js.map +1 -1
- package/lib/pipeline/compile.js +63 -0
- package/lib/pipeline/compile.js.map +1 -1
- package/lib/pipeline/confidence.js +64 -0
- package/lib/pipeline/confidence.js.map +1 -0
- package/lib/pipeline/fusion.js +50 -3
- package/lib/pipeline/fusion.js.map +1 -1
- package/lib/pipeline/plan.js +2 -2
- package/lib/pipeline/plan.js.map +1 -1
- package/lib/pipeline/render.js +20 -2
- package/lib/pipeline/render.js.map +1 -1
- package/lib/pipeline/run.js +21 -7
- package/lib/pipeline/run.js.map +1 -1
- package/lib/pipeline/sources-spec.js +19 -3
- package/lib/pipeline/sources-spec.js.map +1 -1
- package/lib/providers/builtin.js +13 -14
- package/lib/providers/builtin.js.map +1 -1
- package/lib/providers/deepseek.js +348 -0
- package/lib/providers/deepseek.js.map +1 -0
- package/lib/providers/http.js.map +1 -1
- package/lib/providers/keyed-meta.js +7 -0
- package/lib/providers/keyed-meta.js.map +1 -1
- package/lib/providers/platforms.js +118 -58
- package/lib/providers/platforms.js.map +1 -1
- package/lib/providers/registry.js.map +1 -1
- package/lib/restart.js +47 -0
- package/lib/restart.js.map +1 -0
- package/lib/router.js +203 -27
- package/lib/router.js.map +1 -1
- package/lib/rpc-types.js +15 -0
- package/lib/rpc-types.js.map +1 -0
- package/lib/rpc.js +85 -0
- package/lib/rpc.js.map +1 -0
- package/lib/tools.js +10 -4
- package/lib/tools.js.map +1 -1
- package/lib/types/actions/approval.d.ts +11 -0
- package/lib/types/actions/config.d.ts +54 -0
- package/lib/types/actions/format.d.ts +9 -2
- package/lib/types/actions/run.d.ts +1 -0
- package/lib/types/actions/schema.d.ts +7 -1
- package/lib/types/actions/sources.d.ts +29 -3
- package/lib/types/actions/types.d.ts +28 -2
- package/lib/types/browser-access.d.ts +17 -0
- package/lib/types/catalog/recommend.d.ts +4 -0
- package/lib/types/catalog/schema.d.ts +2 -0
- package/lib/types/cli/builtin-specs.d.ts +38 -0
- package/lib/types/cli/chain.d.ts +83 -0
- package/lib/types/cli/chains-spec.d.ts +48 -0
- package/lib/types/cli/engine.d.ts +50 -0
- package/lib/types/cli/opencli-sites.d.ts +24 -0
- package/lib/types/cli/opencli.d.ts +77 -0
- package/lib/types/cli/probe.d.ts +53 -0
- package/lib/types/cli/runner.d.ts +126 -0
- package/lib/types/cli/spec.d.ts +269 -0
- package/lib/types/cli/time-text.d.ts +12 -0
- package/lib/types/cli/trial.d.ts +19 -0
- package/lib/types/client/SettingsCard.d.ts +17 -1
- package/lib/types/client/agent-mark.d.ts +22 -0
- package/lib/types/client/card-frame.d.ts +35 -0
- package/lib/types/client/cli-form.d.ts +97 -0
- package/lib/types/client/field-docs.d.ts +15 -0
- package/lib/types/client/field-keys.d.ts +41 -0
- package/lib/types/client/field-rows.d.ts +70 -0
- package/lib/types/client/fill.d.ts +2 -0
- package/lib/types/client/form-specs.d.ts +12 -3
- package/lib/types/client/form.d.ts +79 -1
- package/lib/types/client/group-advanced.d.ts +9 -0
- package/lib/types/client/group-agent.d.ts +20 -0
- package/lib/types/client/group-cli.d.ts +52 -0
- package/lib/types/client/group-common.d.ts +12 -0
- package/lib/types/client/group-judge.d.ts +17 -0
- package/lib/types/client/group-order.d.ts +26 -0
- package/lib/types/client/group-runtime.d.ts +15 -0
- package/lib/types/client/group-sources.d.ts +46 -0
- package/lib/types/client/groups-basic.d.ts +18 -0
- package/lib/types/client/index.d.ts +8 -0
- package/lib/types/client/json-list.d.ts +21 -0
- package/lib/types/client/locales.d.ts +1079 -196
- package/lib/types/client/order.d.ts +102 -0
- package/lib/types/client/overlay-ops.d.ts +37 -0
- package/lib/types/client/styles.d.ts +56 -41
- package/lib/types/client/text-view.d.ts +22 -0
- package/lib/types/client/ui-kit.d.ts +200 -0
- package/lib/types/client/validators.d.ts +6 -0
- package/lib/types/config-fillers.d.ts +9 -0
- package/lib/types/config-groups.d.ts +23 -0
- package/lib/types/config.d.ts +265 -216
- package/lib/types/default-chain.d.ts +63 -0
- package/lib/types/deps.d.ts +25 -23
- package/lib/types/engine-error.d.ts +12 -0
- package/lib/types/engines.d.ts +45 -10
- package/lib/types/fetch.d.ts +12 -3
- package/lib/types/index.d.ts +164 -146
- package/lib/types/overlay-types.d.ts +98 -0
- package/lib/types/overlay.d.ts +141 -0
- package/lib/types/pipeline/candidates.d.ts +19 -0
- package/lib/types/pipeline/compile.d.ts +11 -0
- package/lib/types/pipeline/confidence.d.ts +35 -0
- package/lib/types/pipeline/fusion.d.ts +23 -1
- package/lib/types/pipeline/render.d.ts +16 -0
- package/lib/types/pipeline/run.d.ts +3 -1
- package/lib/types/pipeline/sources-spec.d.ts +5 -2
- package/lib/types/pipeline/types.d.ts +37 -0
- package/lib/types/providers/deepseek.d.ts +130 -0
- package/lib/types/providers/http.d.ts +2 -1
- package/lib/types/providers/keyed-meta.d.ts +5 -0
- package/lib/types/providers/platforms.d.ts +15 -3
- package/lib/types/providers/registry.d.ts +10 -0
- package/lib/types/restart.d.ts +52 -0
- package/lib/types/router.d.ts +69 -9
- package/lib/types/rpc-types.d.ts +79 -0
- package/lib/types/rpc.d.ts +41 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/util.d.ts +16 -1
- package/lib/util.js +17 -4
- package/lib/util.js.map +1 -1
- package/package.json +51 -43
- package/scripts/check-client-bundle.mjs +1 -1
- package/scripts/cli-login-check.mjs +214 -0
- package/lib/client/RubricEditor.js +0 -47
- package/lib/client/RubricEditor.js.map +0 -1
- package/lib/client/fields.js +0 -31
- package/lib/client/fields.js.map +0 -1
- package/lib/types/client/RubricEditor.d.ts +0 -6
- package/lib/types/client/fields.d.ts +0 -65
package/lib/router.js
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
* @module web-search-pro/router
|
|
5
5
|
*/
|
|
6
6
|
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
7
|
+
import { effectiveChain } from "./default-chain.js";
|
|
7
8
|
import { EngineError, } from "./engines.js";
|
|
8
|
-
import { customKeyProblem, customPlatformAdapter } from "./providers/platforms.js";
|
|
9
|
+
import { customCliAdapter, customCliProviderId, customKeyProblem, customPlatformAdapter } from "./providers/platforms.js";
|
|
10
|
+
import { customCliPlatformIssues, isBackendId, platformBackendIssues } from "./cli/chains-spec.js";
|
|
11
|
+
import { DEFAULT_CUSTOM_CONFIDENCE, resolveCliAdapters } from "./cli/spec.js";
|
|
9
12
|
import { SourceUnavailableError } from "./providers/unavailable.js";
|
|
10
13
|
import { defaultProviderRegistry, costTierOf, routeIdOf } from "./providers/index.js";
|
|
11
14
|
import { BOCHA_FALLBACK_KEY_ENV, BOCHA_KEY_ENV } from "./providers/bocha.js";
|
|
@@ -22,8 +25,25 @@ import { createPlatformCacheKey, createSearchCacheKey } from "./cache-key.js";
|
|
|
22
25
|
import { allAttemptsBenign, allAttemptsEmpty, BackendRegistry, NoBackendError } from "./backend-registry.js";
|
|
23
26
|
import { ExaClient } from "./exa-client.js";
|
|
24
27
|
import { SingleFlight } from "./singleflight.js";
|
|
25
|
-
import { mergeCandidates } from "./pipeline/candidates.js";
|
|
28
|
+
import { mergeCandidates, provenanceOf } from "./pipeline/candidates.js";
|
|
29
|
+
import { guaranteeSeats } from "./pipeline/confidence.js";
|
|
26
30
|
import { fuseCandidates } from "./pipeline/fusion.js";
|
|
31
|
+
function originExtra(origin) {
|
|
32
|
+
return origin.via || origin.tag || origin.confidence !== undefined ? { origin } : {};
|
|
33
|
+
}
|
|
34
|
+
function originFromExtra(extra) {
|
|
35
|
+
if (!extra)
|
|
36
|
+
return {};
|
|
37
|
+
try {
|
|
38
|
+
const origin = JSON.parse(extra).origin;
|
|
39
|
+
return origin && typeof origin === 'object'
|
|
40
|
+
? { ...typeof origin.via === 'string' ? { via: origin.via } : {}, ...typeof origin.tag === 'string' ? { tag: origin.tag } : {}, ...typeof origin.confidence === 'number' ? { confidence: origin.confidence } : {} }
|
|
41
|
+
: {};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
27
47
|
export class SearchRouter {
|
|
28
48
|
ctx;
|
|
29
49
|
config;
|
|
@@ -40,6 +60,11 @@ export class SearchRouter {
|
|
|
40
60
|
syncedRevision = -1;
|
|
41
61
|
/** Custom platforms (settings `customPlatforms`) this router registered: key -> spec signature + unregister. A key the registry refused stays here with its problem so it is not retried on every call. */
|
|
42
62
|
custom = new Map();
|
|
63
|
+
/** User-defined CLI adapters (settings `cliAdapters`) this router registered as `custom-cli:<id>` platforms, with the diagnostics of the entries it ignored. */
|
|
64
|
+
customCli = new Map();
|
|
65
|
+
cliAdapterDiagnostics = [];
|
|
66
|
+
/** Confidence of each registered custom CLI by route id (its spec's, default 0.5), rebuilt with the registrations. */
|
|
67
|
+
customConfidence = new Map();
|
|
43
68
|
/** Latest local probe per route id (read by providerStatuses for the credential dimension). */
|
|
44
69
|
readiness = new Map();
|
|
45
70
|
/** Last real call per route id: feeds the health dimension (never inferred from a local probe). */
|
|
@@ -101,6 +126,35 @@ export class SearchRouter {
|
|
|
101
126
|
this.custom.set(key, { sig, off: () => { }, problem: 'custom platform "' + key + '" was not registered: ' + (error instanceof Error ? error.message : String(error)) });
|
|
102
127
|
}
|
|
103
128
|
}
|
|
129
|
+
this.syncCustomCliAdapters();
|
|
130
|
+
}
|
|
131
|
+
/** The same for `cliAdapters`: valid specs become `custom-cli:<id>` platforms; an invalid entry is ignored and reported. */
|
|
132
|
+
syncCustomCliAdapters() {
|
|
133
|
+
const { specs, diagnostics } = resolveCliAdapters(this.dynamic().cliAdapters);
|
|
134
|
+
this.cliAdapterDiagnostics = diagnostics;
|
|
135
|
+
this.customConfidence = new Map([...specs].map(([id, spec]) => [customCliProviderId(id), spec.confidence ?? DEFAULT_CUSTOM_CONFIDENCE]));
|
|
136
|
+
const next = new Map([...specs].map(([id, spec]) => [id, JSON.stringify(spec)]));
|
|
137
|
+
for (const [id, entry] of [...this.customCli])
|
|
138
|
+
if (next.get(id) !== entry.sig) {
|
|
139
|
+
entry.off();
|
|
140
|
+
this.customCli.delete(id);
|
|
141
|
+
}
|
|
142
|
+
for (const [id, sig] of next) {
|
|
143
|
+
if (this.customCli.has(id))
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
this.customCli.set(id, { sig, off: this.registry.register(customCliAdapter(id, specs.get(id))) });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.customCli.set(id, { sig, off: () => { }, problem: 'cliAdapters.' + id + ' was not registered as ' + customCliProviderId(id) + ': ' + (error instanceof Error ? error.message : String(error)) });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Configuration problems of the CLI adapter layer for `sources.status`: ignored `cliAdapters` entries and `platformBackends` that name nothing usable. */
|
|
154
|
+
cliAdapterProblems() {
|
|
155
|
+
this.syncCustomPlatforms();
|
|
156
|
+
const cfg = this.dynamic();
|
|
157
|
+
return [...this.cliAdapterDiagnostics, ...[...this.customCli.values()].flatMap(entry => (entry.problem ? [entry.problem] : [])), ...customCliPlatformIssues(cfg.cliAdapters), ...platformBackendIssues(cfg.platformBackends, cfg.cliAdapters)];
|
|
104
158
|
}
|
|
105
159
|
/** Custom platforms the registry could not take (key clash, bad key), for `sources.status`. */
|
|
106
160
|
customPlatformProblems() {
|
|
@@ -112,6 +166,9 @@ export class SearchRouter {
|
|
|
112
166
|
for (const entry of this.custom.values())
|
|
113
167
|
entry.off();
|
|
114
168
|
this.custom.clear();
|
|
169
|
+
for (const entry of this.customCli.values())
|
|
170
|
+
entry.off();
|
|
171
|
+
this.customCli.clear();
|
|
115
172
|
}
|
|
116
173
|
backendFor(id) {
|
|
117
174
|
return {
|
|
@@ -156,7 +213,9 @@ export class SearchRouter {
|
|
|
156
213
|
const outcome = await engine.search(input.query, input.count, input.signal, this.withBindings(id, input.options));
|
|
157
214
|
ticket?.settle(1, 'request counted by the router (the adapter booked none)');
|
|
158
215
|
this.outcomes.set(id, { ok: true, at: new Date().toISOString() });
|
|
159
|
-
|
|
216
|
+
// The source's confidence rides with every row it returned (only when below 1: the built-in sources add nothing).
|
|
217
|
+
const confidence = this.confidenceOf(id, outcome.backend);
|
|
218
|
+
return { ...outcome, via: outcome.via ?? engine.id, ...confidence < 1 ? { sources: outcome.sources.map(source => ({ ...source, confidence })) } : {} };
|
|
160
219
|
}
|
|
161
220
|
catch (error) {
|
|
162
221
|
const code = error?.code;
|
|
@@ -219,17 +278,53 @@ export class SearchRouter {
|
|
|
219
278
|
return { deps: await this.deps(false), config: this.dynamic(), ...cli ? { cli } : {} };
|
|
220
279
|
}
|
|
221
280
|
/**
|
|
222
|
-
* The configured `engines` list with the user's `sources.*`
|
|
223
|
-
*
|
|
281
|
+
* The default engine order for a search that names no `engines`: the configured `engines` list with the user's `sources.*`
|
|
282
|
+
* preferences applied, then the default rule for "常用" (common) sources.
|
|
283
|
+
* 1. an explicit `engines` of a call never comes through here;
|
|
284
|
+
* 2. `sources.disabled` ids are removed, `sources.priority` ids that are in the list move to the front in their listed order;
|
|
285
|
+
* 3. a common source (DeepSeek Search) whose key resolves leads what is left, while `engines` is still the default list (a
|
|
286
|
+
* list the user wrote is theirs: it keeps its order, and a common source in it keeps the position given). `sources.disabled`
|
|
287
|
+
* and `evidence.sourcePolicy: anonymous-only` switch it off.
|
|
224
288
|
*/
|
|
225
|
-
preferred(ids) {
|
|
226
|
-
const
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
289
|
+
async preferred(ids) {
|
|
290
|
+
const cfg = this.dynamic();
|
|
291
|
+
const src = cfg.sources;
|
|
292
|
+
return effectiveChain({
|
|
293
|
+
engines: ids,
|
|
294
|
+
priority: this.canonicalIds(src?.priority ?? []),
|
|
295
|
+
disabled: this.canonicalIds(src?.disabled ?? []),
|
|
296
|
+
anonymousOnly: cfg.evidence?.sourcePolicy === 'anonymous-only',
|
|
297
|
+
commonReady: await this.commonReady(),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
/** Route ids of the common ("常用") sources that are ready to run now (key resolved), best `priority` first. */
|
|
301
|
+
async commonReady() {
|
|
302
|
+
const common = this.registry.list({ operation: 'search' }).filter(a => a.descriptor.common && a.descriptor.kind !== 'platform');
|
|
303
|
+
if (!common.length)
|
|
304
|
+
return [];
|
|
305
|
+
const env = await this.probeEnv();
|
|
306
|
+
const out = [];
|
|
307
|
+
for (const adapter of common) {
|
|
308
|
+
try {
|
|
309
|
+
const readiness = await adapter.probeLocal(env);
|
|
310
|
+
if (readiness.available && readiness.credential === 'configured')
|
|
311
|
+
out.push({ id: routeIdOf(adapter.descriptor), priority: adapter.descriptor.priority ?? 100 });
|
|
312
|
+
}
|
|
313
|
+
catch { /* a probe that throws is not ready */ }
|
|
314
|
+
}
|
|
315
|
+
return out.sort((a, b) => a.priority - b.priority).map(x => x.id);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Key and endpoint of the DeepSeek Fetch API for the fetch pipeline, or undefined when no key resolves (settings literal ->
|
|
319
|
+
* DSH credentials ref -> process environment, the same order as the search side). Resolved per call; the key is never cached.
|
|
320
|
+
*/
|
|
321
|
+
async deepseekAccess() {
|
|
322
|
+
const cfg = this.dynamic();
|
|
323
|
+
const { sourceKeys, sourceBaseUrls } = await this.keyedSources(cfg, (ref, literal) => this.resolveKey(ref, literal));
|
|
324
|
+
const apiKey = sourceKeys?.deepseek;
|
|
325
|
+
if (!apiKey)
|
|
326
|
+
return undefined;
|
|
327
|
+
return { apiKey, ...sourceBaseUrls?.deepseek ? { baseUrl: sourceBaseUrls.deepseek } : {}, allowProxyFakeIp: cfg.allowProxyFakeIp, usage: this.usageRecorder() };
|
|
233
328
|
}
|
|
234
329
|
/** Alias / full id -> route id; ids the registry does not know are kept as written (the backend then reports them unknown). */
|
|
235
330
|
canonicalIds(ids) {
|
|
@@ -242,10 +337,9 @@ export class SearchRouter {
|
|
|
242
337
|
return diagnostics;
|
|
243
338
|
const cfg = this.dynamic();
|
|
244
339
|
const exaKey = await this.resolveKey(cfg.exaApiKeyEnv, cfg.exaApiKey);
|
|
340
|
+
// Platform providers (bilibili, youtube, ...) report their own chain readiness (src/cli/chain.ts); only Exa's MCP route is overruled here.
|
|
245
341
|
const requiredCli = new Map([
|
|
246
342
|
...exaKey ? [] : [['exa', 'mcporter']],
|
|
247
|
-
['bilibili', 'bili'],
|
|
248
|
-
['youtube', 'yt-dlp'],
|
|
249
343
|
]);
|
|
250
344
|
return diagnostics.map(diagnostic => {
|
|
251
345
|
const dependency = requiredCli.get(diagnostic.id);
|
|
@@ -310,7 +404,7 @@ export class SearchRouter {
|
|
|
310
404
|
try {
|
|
311
405
|
const selected = await this.backends.runSelected({ query: call.query, count: call.count, signal: call.signal, skipSeam: opts.skipSeam ?? false, ...call.options ? { options: call.options } : {} }, { preferred: this.canonicalIds([call.id]), signal: call.signal });
|
|
312
406
|
const sources = selected.value.sources;
|
|
313
|
-
return sources.length ? { state: 'ok', sources } : { state: 'empty' };
|
|
407
|
+
return sources.length ? { state: 'ok', sources, ...selected.value.backend ? { backend: selected.value.backend } : {}, ...selected.value.notes?.length ? { notes: selected.value.notes } : {} } : { state: 'empty' };
|
|
314
408
|
}
|
|
315
409
|
catch (error) {
|
|
316
410
|
if (call.signal.aborted)
|
|
@@ -328,6 +422,33 @@ export class SearchRouter {
|
|
|
328
422
|
return { state: 'error', message: error instanceof Error ? error.message : String(error) };
|
|
329
423
|
}
|
|
330
424
|
}
|
|
425
|
+
/**
|
|
426
|
+
* The credential variables of the CLI adapters under the names the tools read, each resolved like the plugin's other secrets:
|
|
427
|
+
* settings literal -> DSH credentials ref (`twitterAuthTokenEnv`) -> process environment. Values only ever reach a command's environment.
|
|
428
|
+
*/
|
|
429
|
+
async cliEnv() {
|
|
430
|
+
const cfg = this.dynamic();
|
|
431
|
+
const out = {};
|
|
432
|
+
const authToken = await this.resolveKey(cfg.twitterAuthTokenEnv ?? 'TWITTER_AUTH_TOKEN', cfg.twitterAuthToken);
|
|
433
|
+
const ct0 = await this.resolveKey(cfg.twitterCt0Env ?? 'TWITTER_CT0', cfg.twitterCt0);
|
|
434
|
+
if (authToken)
|
|
435
|
+
out.TWITTER_AUTH_TOKEN = authToken;
|
|
436
|
+
if (ct0)
|
|
437
|
+
out.TWITTER_CT0 = ct0;
|
|
438
|
+
return out;
|
|
439
|
+
}
|
|
440
|
+
/** Sync twin of {@link cliEnv} for `available()` checks: settings literal and process environment only. */
|
|
441
|
+
cliEnvSync() {
|
|
442
|
+
const cfg = this.dynamic();
|
|
443
|
+
const out = {};
|
|
444
|
+
const authToken = cfg.twitterAuthToken || process.env[cfg.twitterAuthTokenEnv ?? 'TWITTER_AUTH_TOKEN'];
|
|
445
|
+
const ct0 = cfg.twitterCt0 || process.env[cfg.twitterCt0Env ?? 'TWITTER_CT0'];
|
|
446
|
+
if (authToken)
|
|
447
|
+
out.TWITTER_AUTH_TOKEN = authToken;
|
|
448
|
+
if (ct0)
|
|
449
|
+
out.TWITTER_CT0 = ct0;
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
331
452
|
/** Resolve a key through credentials first, then process env. */
|
|
332
453
|
async resolveKey(ref, literal) {
|
|
333
454
|
if (literal && literal.length > 0)
|
|
@@ -357,8 +478,10 @@ export class SearchRouter {
|
|
|
357
478
|
const semanticScholarApiKey = await this.resolveKey(SEMANTICSCHOLAR_KEY_ENV);
|
|
358
479
|
const anysearchApiKey = await this.resolveKey(ANYSEARCH_KEY_ENV);
|
|
359
480
|
const keyed = await this.keyedSources(cfg, (ref, literal) => this.resolveKey(ref, literal));
|
|
481
|
+
const cliEnv = await this.cliEnv();
|
|
360
482
|
return {
|
|
361
483
|
...keyed,
|
|
484
|
+
...Object.keys(cliEnv).length ? { cliEnv } : {},
|
|
362
485
|
...openalexApiKey ? { openalexApiKey } : {},
|
|
363
486
|
...semanticScholarApiKey ? { semanticScholarApiKey } : {},
|
|
364
487
|
...anysearchApiKey ? { anysearchApiKey } : {},
|
|
@@ -395,8 +518,10 @@ export class SearchRouter {
|
|
|
395
518
|
const semanticScholarApiKey = process.env[SEMANTICSCHOLAR_KEY_ENV];
|
|
396
519
|
const anysearchApiKey = process.env[ANYSEARCH_KEY_ENV];
|
|
397
520
|
const keyed = this.keyedSourcesSync(cfg);
|
|
521
|
+
const cliEnv = this.cliEnvSync();
|
|
398
522
|
return {
|
|
399
523
|
...keyed,
|
|
524
|
+
...Object.keys(cliEnv).length ? { cliEnv } : {},
|
|
400
525
|
...openalexApiKey ? { openalexApiKey } : {},
|
|
401
526
|
...semanticScholarApiKey ? { semanticScholarApiKey } : {},
|
|
402
527
|
...anysearchApiKey ? { anysearchApiKey } : {},
|
|
@@ -477,8 +602,31 @@ export class SearchRouter {
|
|
|
477
602
|
}
|
|
478
603
|
/** Problems of the `sources.*` settings (unknown ids, bad numbers), for `sources.status`. */
|
|
479
604
|
sourceDiagnostics() {
|
|
605
|
+
this.syncCustomPlatforms();
|
|
480
606
|
const src = this.dynamic().sources;
|
|
481
|
-
return resolveSources({ priority: src?.priority, disabled: src?.disabled, budget: src?.budget }, id => this.registry.routeId(id) !== undefined).diagnostics;
|
|
607
|
+
return resolveSources({ priority: src?.priority, disabled: src?.disabled, budget: src?.budget, confidence: src?.confidence }, id => this.registry.routeId(id) !== undefined, isBackendId).diagnostics;
|
|
608
|
+
}
|
|
609
|
+
/** The confidence of one platform backend as a leg of a chain (`xhs`, `custom-cli:echo`): its own, without the provider's. */
|
|
610
|
+
backendConfidenceOf(id) {
|
|
611
|
+
return this.confidenceOf(id);
|
|
612
|
+
}
|
|
613
|
+
/** `sources.confidence` by canonical id (a provider's route id; a platform backend id as written), valid entries only. */
|
|
614
|
+
confidenceOverrides() {
|
|
615
|
+
const resolved = resolveSources({ confidence: this.dynamic().sources?.confidence }, id => this.registry.routeId(id) !== undefined, isBackendId);
|
|
616
|
+
return new Map(Object.entries(resolved.confidence).map(([id, value]) => [this.registry.routeId(id) ?? id, value]));
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* The confidence (0..1) of what `route` returned, through `backend` when it is a platform chain: `sources.confidence` of the
|
|
620
|
+
* provider times that of the backend that answered, each defaulting to the custom CLI's own (spec `confidence`, 0.5) and to 1
|
|
621
|
+
* for every built-in source, so the built-in ones are 1 unless the user set something.
|
|
622
|
+
*/
|
|
623
|
+
confidenceOf(route, backend) {
|
|
624
|
+
this.syncCustomPlatforms();
|
|
625
|
+
const overrides = this.confidenceOverrides();
|
|
626
|
+
const own = (id) => overrides.get(id) ?? this.customConfidence.get(id) ?? 1;
|
|
627
|
+
const provider = this.registry.routeId(route) ?? route;
|
|
628
|
+
const via = backend === undefined || backend === provider ? undefined : this.registry.routeId(backend) ?? backend;
|
|
629
|
+
return Math.round(own(provider) * (via === undefined ? 1 : own(via)) * 10_000) / 10_000;
|
|
482
630
|
}
|
|
483
631
|
/** Counts a metered, non-model request (Bocha search) in the usage ledger; best effort, never throws into the search. */
|
|
484
632
|
usageRecorder() {
|
|
@@ -544,19 +692,34 @@ export class SearchRouter {
|
|
|
544
692
|
const health = cooling ? 'cooldown' : last ? (last.ok ? 'ready' : 'error') : 'unknown';
|
|
545
693
|
const credential = last?.code === 'ENGINE_AUTH' ? 'rejected' : local.credential;
|
|
546
694
|
const reason = !available ? (diag?.reason ?? local.reason) : undefined;
|
|
695
|
+
let chain;
|
|
696
|
+
let chainReport;
|
|
697
|
+
if (adapter.chain) {
|
|
698
|
+
try {
|
|
699
|
+
chainReport = await adapter.chain(env);
|
|
700
|
+
chain = chainReport.entries.map(entry => ({ ...entry, confidence: this.backendConfidenceOf(entry.id) }));
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
chain = undefined;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
547
706
|
out.push({
|
|
548
707
|
id: d.id, route, aliases: [...d.aliases], label: d.label, kind: d.kind ?? 'web', ...d.domains?.length ? { domains: [...d.domains] } : {}, ...d.needsBrowser ? { needsBrowser: d.needsBrowser } : {}, operations: [...d.operations], taskProfiles: [...d.taskProfiles],
|
|
549
708
|
languages: [...d.languages], regions: [...d.regions], resultKinds: [...d.resultKinds],
|
|
550
709
|
...d.sourceFamily ? { sourceFamily: d.sourceFamily } : {},
|
|
551
710
|
requirements: d.requirements.map(({ env, ...r }) => ({ ...r, ...env ? { env: [...env] } : {} })), supportedFilters: [...d.supportedFilters], costModel: { ...d.costModel }, costTier: local.costTier ?? costTierOf(d),
|
|
552
711
|
...d.verification?.live ? {} : { unverified: true },
|
|
712
|
+
...d.common ? { common: true } : {},
|
|
713
|
+
confidence: this.confidenceOf(route),
|
|
714
|
+
...chain ? { chain } : {},
|
|
553
715
|
...this.requestBudgetOf(route) ? { budget: this.requestBudgetOf(route) } : {},
|
|
554
716
|
readiness: {
|
|
555
717
|
available,
|
|
718
|
+
...chainReport ? { state: chainReport.state } : {},
|
|
556
719
|
...local.installation ? { installation: local.installation } : {},
|
|
557
720
|
...credential ? { credential } : {},
|
|
558
721
|
health,
|
|
559
|
-
...reason ? { reason } : {},
|
|
722
|
+
...reason ? { reason } : chainReport?.state === 'login_unverified' && chainReport.reason ? { reason: chainReport.reason } : {},
|
|
560
723
|
...local.diagnosticCode ? { diagnosticCode: local.diagnosticCode } : {},
|
|
561
724
|
lastLocalCheck: now,
|
|
562
725
|
...last?.ok ? { lastRemoteSuccess: last.at } : {},
|
|
@@ -575,7 +738,7 @@ export class SearchRouter {
|
|
|
575
738
|
const query = platform ? platform.query : opts.query.trim();
|
|
576
739
|
if (!platform && !query)
|
|
577
740
|
throw new Error('query must be a non-empty string');
|
|
578
|
-
const ids = platform ? [platform.id] : opts.engines && opts.engines.length ? this.canonicalIds(opts.engines) : this.preferred(this.canonicalIds(cfg.engines));
|
|
741
|
+
const ids = platform ? [platform.id] : opts.engines && opts.engines.length ? this.canonicalIds(opts.engines) : await this.preferred(this.canonicalIds(cfg.engines));
|
|
579
742
|
const nq = normQuery(platform ? query || platform.url || platform.id : query);
|
|
580
743
|
const count = Math.min(Math.max(opts.count, 1), 20);
|
|
581
744
|
const multi = !platform && opts.multi && ids.length > 1;
|
|
@@ -628,11 +791,12 @@ export class SearchRouter {
|
|
|
628
791
|
if (detail?.requestedCount === undefined || detail.requestedCount >= count) {
|
|
629
792
|
const result = {
|
|
630
793
|
...detail?.content ? { content: detail.content } : {},
|
|
631
|
-
sources: shapeSources(cached.rows.map(r => ({ url: r.url, title: r.title, snippet: r.snippet, publishedAt: r.published })), count),
|
|
794
|
+
sources: shapeSources(cached.rows.map(r => ({ url: r.url, title: r.title, snippet: r.snippet, publishedAt: r.published, ...originFromExtra(r.extra) })), count),
|
|
632
795
|
engine: detail?.engine ?? ids[0] ?? 'unknown',
|
|
633
796
|
enginesTried: detail?.enginesTried ?? ids,
|
|
634
797
|
fromCache: true,
|
|
635
798
|
availableCount: cached.rows.length,
|
|
799
|
+
...detail?.backend ? { backend: detail.backend } : {},
|
|
636
800
|
...detail?.fallbackNote ? { fallbackNote: detail.fallbackNote } : {},
|
|
637
801
|
};
|
|
638
802
|
this.memory.set(memoryKey, result);
|
|
@@ -648,6 +812,7 @@ export class SearchRouter {
|
|
|
648
812
|
let fallbackNote;
|
|
649
813
|
let availableCount;
|
|
650
814
|
let persistExtras;
|
|
815
|
+
let backendId;
|
|
651
816
|
if (multi) {
|
|
652
817
|
// Every engine goes through the registry (probe, cooldown, quality gate,
|
|
653
818
|
// attempts) in parallel; failures of one never cancel the others.
|
|
@@ -674,18 +839,19 @@ export class SearchRouter {
|
|
|
674
839
|
throw new Error('all engines failed: ' + failures);
|
|
675
840
|
}
|
|
676
841
|
const merged = mergeCandidates(outputs);
|
|
677
|
-
|
|
842
|
+
// A source with a confidence below 1 weighs a little less in the fusion, and its best result keeps a place in the list (confidence.ts).
|
|
843
|
+
const ranked = guaranteeSeats(fuseCandidates(merged, {
|
|
678
844
|
k: cfg.rrfConstant,
|
|
679
845
|
freshnessBoost: cfg.freshnessBoost,
|
|
680
846
|
freshnessDays: cfg.freshnessDays,
|
|
681
847
|
authorityBoost: cfg.authorityBoost,
|
|
682
848
|
authorityDomains: cfg.authorityDomains,
|
|
683
849
|
nProviders: outputs.length,
|
|
684
|
-
}).slice(0, count);
|
|
850
|
+
}), count, ranked => ranked.candidate.contributions).slice(0, count);
|
|
685
851
|
availableCount = merged.length;
|
|
686
|
-
outcome = { sources: ranked.map(({ candidate }) => ({ url: candidate.url, ...candidate.title ? { title: candidate.title } : {}, ...candidate.snippet ? { snippet: candidate.snippet } : {}, ...candidate.publishedAt ? { publishedAt: candidate.publishedAt } : {} })) };
|
|
687
|
-
// Per-source provenance survives in results.extra (no schema change);
|
|
688
|
-
persistExtras = ranked.map(({ candidate, score }) => JSON.stringify({ contributions: candidate.contributions, score: Number(score.toFixed(6)) }));
|
|
852
|
+
outcome = { sources: ranked.map(({ candidate }) => ({ url: candidate.url, ...candidate.title ? { title: candidate.title } : {}, ...candidate.snippet ? { snippet: candidate.snippet } : {}, ...candidate.publishedAt ? { publishedAt: candidate.publishedAt } : {}, ...provenanceOf(candidate) })) };
|
|
853
|
+
// Per-source provenance survives in results.extra (no schema change); a cache hit reads back the origin (`via`, `tag`, `confidence`) from it.
|
|
854
|
+
persistExtras = ranked.map(({ candidate, score }) => JSON.stringify({ contributions: candidate.contributions, score: Number(score.toFixed(6)), ...originExtra(provenanceOf(candidate)) }));
|
|
689
855
|
usedId = 'multi(' + ids.join('+') + ')';
|
|
690
856
|
}
|
|
691
857
|
else {
|
|
@@ -701,6 +867,8 @@ export class SearchRouter {
|
|
|
701
867
|
outcome = selected.value;
|
|
702
868
|
usedId = platform ? platform.id : selected.id;
|
|
703
869
|
viaId = selected.value.via ?? selected.id;
|
|
870
|
+
if (platform)
|
|
871
|
+
backendId = selected.value.backend ?? viaId;
|
|
704
872
|
enginesTried.push(...ids.slice(0, Math.max(ids.indexOf(selected.id) + 1, 1)));
|
|
705
873
|
// P1-1: explain the fallback (e.g. ddg returned results but none had snippets). A single platform has nothing to fall back to.
|
|
706
874
|
const lowQualityAttempts = platform ? [] : selected.attempts.filter(a => a.outcome === 'low-quality');
|
|
@@ -714,6 +882,10 @@ export class SearchRouter {
|
|
|
714
882
|
const spent = selected.attempts.filter(a => a.outcome === 'skipped' && /request budget used up/.test(a.detail ?? ''));
|
|
715
883
|
if (spent.length)
|
|
716
884
|
fallbackNote = (fallbackNote ? fallbackNote + '\n' : '') + 'skipped: ' + spent.map(a => a.id + ': ' + a.detail).join('; ');
|
|
885
|
+
// Notes of the engine itself: chain legs passed over (with reasons), results dropped for lack of a link.
|
|
886
|
+
const engineNotes = selected.value.notes ?? [];
|
|
887
|
+
if (engineNotes.length)
|
|
888
|
+
fallbackNote = (fallbackNote ? fallbackNote + '\n' : '') + engineNotes.join('\n');
|
|
717
889
|
}
|
|
718
890
|
catch (error) {
|
|
719
891
|
if (signal?.aborted)
|
|
@@ -743,8 +915,11 @@ export class SearchRouter {
|
|
|
743
915
|
engine: platform ? viaId ?? finalId : finalId,
|
|
744
916
|
status: 'ok',
|
|
745
917
|
cacheKey,
|
|
746
|
-
detail: JSON.stringify({ ...finalOutcome.content ? { content: finalOutcome.content } : {}, engine: finalId, enginesTried, requestedCount: count, ...fallbackNote ? { fallbackNote } : {} }),
|
|
747
|
-
},
|
|
918
|
+
detail: JSON.stringify({ ...finalOutcome.content ? { content: finalOutcome.content } : {}, engine: finalId, ...backendId ? { backend: backendId } : {}, enginesTried, requestedCount: count, ...fallbackNote ? { fallbackNote } : {} }),
|
|
919
|
+
}, finalOutcome.sources.map((source, i) => {
|
|
920
|
+
const extra = persistExtras ? persistExtras[i] : JSON.stringify(originExtra({ ...source.via ? { via: source.via } : {}, ...source.tag ? { tag: source.tag } : {}, ...source.confidence !== undefined ? { confidence: source.confidence } : {} }));
|
|
921
|
+
return extra === '{}' ? source : { ...source, extra };
|
|
922
|
+
}), platform ? 'platform-' + platform.id : finalId));
|
|
748
923
|
const result = {
|
|
749
924
|
...outcome.content ? { content: outcome.content } : {},
|
|
750
925
|
sources: shapeSources(outcome.sources, count),
|
|
@@ -752,6 +927,7 @@ export class SearchRouter {
|
|
|
752
927
|
enginesTried,
|
|
753
928
|
fromCache: false,
|
|
754
929
|
availableCount,
|
|
930
|
+
...backendId ? { backend: backendId } : {},
|
|
755
931
|
...fallbackNote ? { fallbackNote } : {},
|
|
756
932
|
};
|
|
757
933
|
// 4. Warm the in-process LRU (memory-only; survives across SQLite hits).
|