dsh-web-search-plugin 0.4.0 → 0.5.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/CHANGELOG.md +52 -1
- package/README.md +72 -67
- package/cordis.patch.yml +10 -7
- package/lib/brave.js +4 -6
- package/lib/client.js +175 -87
- package/lib/deepseek.js +71 -15
- package/lib/index.js +137 -80
- package/lib/providers.js +7 -7
- package/lib/rest.js +10 -5
- package/lib/shared.js +1 -1
- package/lib/tavily.js +2 -2
- package/lib/usage.js +53 -30
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +12 -9
package/lib/client.js
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
* writes once, credential written through the credentials domain) without
|
|
13
13
|
* depending on that package's internal `CardForm`.
|
|
14
14
|
*
|
|
15
|
+
* Adapted to DeepSeek Harness `0.2.x`: the card reads and writes its namespace
|
|
16
|
+
* through `ctx.configForms` (`ConfigForm`), replacing the removed
|
|
17
|
+
* `ctx.settingsScope` service.
|
|
18
|
+
*
|
|
15
19
|
* @module dsh-web-search-plugin/client
|
|
16
20
|
*/
|
|
17
21
|
window.__ModuleLoader__.load({
|
|
@@ -44,6 +48,42 @@ window.__ModuleLoader__.load({
|
|
|
44
48
|
for (const entry of BUILTIN_REST_PROVIDERS) if (entry.keyField === field) return entry;
|
|
45
49
|
return void 0;
|
|
46
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Where each provider issues its keys. The REST rows carry their own
|
|
53
|
+
* `officialUrl`; the three with dedicated branches are listed here so every
|
|
54
|
+
* engine gets the same "get an API key" affordance.
|
|
55
|
+
*/
|
|
56
|
+
const KEY_CONSOLE_URLS = {
|
|
57
|
+
"deepseek-official": "https://platform.deepseek.com/",
|
|
58
|
+
tavily: "https://app.tavily.com/",
|
|
59
|
+
brave: "https://api-dashboard.search.brave.com/"
|
|
60
|
+
};
|
|
61
|
+
/** The console URL for the active engine, when it issues keys. */
|
|
62
|
+
function keyConsoleUrl(provider, restMeta) {
|
|
63
|
+
if (restMeta !== void 0) return restMeta.keyRequired ? restMeta.officialUrl : "";
|
|
64
|
+
return KEY_CONSOLE_URLS[provider] ?? "";
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The "get an API key" link every key-issuing engine shows.
|
|
68
|
+
* @param t - the card's locale reader.
|
|
69
|
+
* @param provider - the active engine id.
|
|
70
|
+
* @param restMeta - the engine's metadata row, when it has one.
|
|
71
|
+
* @param keyed - Tavily only: whether it is in keyed mode.
|
|
72
|
+
* @returns the link element, or null when the engine issues no keys.
|
|
73
|
+
*/
|
|
74
|
+
function keyConsoleLink(t, provider, restMeta, keyed) {
|
|
75
|
+
// Tavily's keyless mode needs no key, so it gets no console link.
|
|
76
|
+
if (provider === "tavily" && keyed !== true) return null;
|
|
77
|
+
const url = keyConsoleUrl(provider, restMeta);
|
|
78
|
+
if (url === "") return null;
|
|
79
|
+
return react.createElement("a", {
|
|
80
|
+
href: url,
|
|
81
|
+
target: "_blank",
|
|
82
|
+
rel: "noopener noreferrer",
|
|
83
|
+
className: "dshws_field_hint",
|
|
84
|
+
style: { color: "var(--dsw-alias-brand-primary,#3b82f6)", textDecoration: "none", padding: "8px 0", display: "inline-block" }
|
|
85
|
+
}, t("getKey") + " ↗");
|
|
86
|
+
}
|
|
47
87
|
/** Credential reference Tavily keyed mode resolves when the section names none. */
|
|
48
88
|
const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
|
|
49
89
|
/** Credential reference Brave resolves when the section names none. */
|
|
@@ -132,7 +172,7 @@ window.__ModuleLoader__.load({
|
|
|
132
172
|
freshnessMonth: "Past month",
|
|
133
173
|
freshnessYear: "Past year",
|
|
134
174
|
proxy: "Proxy",
|
|
135
|
-
proxyPlaceholder: "Leave blank to
|
|
175
|
+
proxyPlaceholder: "Leave blank to inherit the DSH proxy settings.",
|
|
136
176
|
deepseekApiKeyEnv: "Credential reference",
|
|
137
177
|
deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
|
|
138
178
|
deepseekBaseURL: "Endpoint",
|
|
@@ -162,6 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
162
202
|
invalidToast: "Fix the highlighted fields before saving.",
|
|
163
203
|
readOnly: "This deployment stores settings read-only.",
|
|
164
204
|
invalidNumber: "Enter a whole number of at least 1, or leave blank for the default.",
|
|
205
|
+
invalidCredentialRef: "Use an environment variable name: letters, digits, and underscores; the first character must be a letter or underscore.",
|
|
165
206
|
quotaTitle: "Quota",
|
|
166
207
|
quotaPlan: "Plan {plan}",
|
|
167
208
|
quotaUsed: "Used {used} / {limit}",
|
|
@@ -228,7 +269,7 @@ window.__ModuleLoader__.load({
|
|
|
228
269
|
freshnessMonth: "过去一月",
|
|
229
270
|
freshnessYear: "过去一年",
|
|
230
271
|
proxy: "代理",
|
|
231
|
-
proxyPlaceholder: "
|
|
272
|
+
proxyPlaceholder: "留空继承 DSH 全局代理设置",
|
|
232
273
|
deepseekApiKeyEnv: "凭据引用名",
|
|
233
274
|
deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
|
|
234
275
|
deepseekBaseURL: "接口地址",
|
|
@@ -258,6 +299,7 @@ window.__ModuleLoader__.load({
|
|
|
258
299
|
invalidToast: "请先修正标红的字段再保存。",
|
|
259
300
|
readOnly: "本部署的设置为只读。",
|
|
260
301
|
invalidNumber: "请输入不小于 1 的整数;留空表示使用默认值。",
|
|
302
|
+
invalidCredentialRef: "请使用环境变量名:仅含字母、数字和下划线,且以字母或下划线开头。",
|
|
261
303
|
quotaTitle: "额度",
|
|
262
304
|
quotaPlan: "计划 {plan}",
|
|
263
305
|
quotaUsed: "已用 {used} / {limit}",
|
|
@@ -311,6 +353,17 @@ window.__ModuleLoader__.load({
|
|
|
311
353
|
}
|
|
312
354
|
};
|
|
313
355
|
}
|
|
356
|
+
function credentialField(field) {
|
|
357
|
+
return {
|
|
358
|
+
field,
|
|
359
|
+
format: (value) => typeof value === "string" ? value : "",
|
|
360
|
+
parse: (text) => {
|
|
361
|
+
const trimmed = text.trim();
|
|
362
|
+
if (trimmed === "") return { kind: "clear" };
|
|
363
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/u.test(trimmed) ? { kind: "set", value: trimmed } : void 0;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
314
367
|
/** A choice field spec: staged as its raw string. */
|
|
315
368
|
function choiceField(field) {
|
|
316
369
|
return {
|
|
@@ -330,7 +383,7 @@ window.__ModuleLoader__.load({
|
|
|
330
383
|
|
|
331
384
|
/**
|
|
332
385
|
* Staged form over the `dsh-web-search-plugin` settings namespace.
|
|
333
|
-
* Write-only credential fields
|
|
386
|
+
* Write-only credential fields have no section
|
|
334
387
|
* spec and are written through the credentials domain.
|
|
335
388
|
*/
|
|
336
389
|
var SearchCardForm = class {
|
|
@@ -341,33 +394,33 @@ window.__ModuleLoader__.load({
|
|
|
341
394
|
choiceField("provider"),
|
|
342
395
|
choiceField("mode"),
|
|
343
396
|
numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }),
|
|
344
|
-
|
|
397
|
+
credentialField("apiKeyEnv"),
|
|
345
398
|
textField("baseURL"),
|
|
346
399
|
choiceField("searchDepth"),
|
|
347
400
|
choiceField("topic"),
|
|
348
401
|
booleanField("includeAnswer"),
|
|
349
|
-
|
|
402
|
+
credentialField("braveApiKeyEnv"),
|
|
350
403
|
textField("braveBaseURL"),
|
|
351
404
|
textField("country"),
|
|
352
405
|
textField("searchLang"),
|
|
353
406
|
choiceField("freshness"),
|
|
354
407
|
textField("proxy"),
|
|
355
|
-
|
|
408
|
+
credentialField("deepseekApiKeyEnv"),
|
|
356
409
|
textField("deepseekBaseURL"),
|
|
357
410
|
textField("model"),
|
|
358
411
|
textField("apiVersion"),
|
|
359
412
|
numberField("maxTokens", { min: 1 }),
|
|
360
413
|
numberField("maxUses", { min: 1 }),
|
|
361
|
-
|
|
414
|
+
credentialField("serperApiKeyEnv"),
|
|
362
415
|
textField("serperBaseURL"),
|
|
363
|
-
|
|
416
|
+
credentialField("serpapiApiKeyEnv"),
|
|
364
417
|
textField("serpapiBaseURL"),
|
|
365
|
-
|
|
418
|
+
credentialField("exaApiKeyEnv"),
|
|
366
419
|
textField("exaBaseURL"),
|
|
367
420
|
textField("searxngBaseURL"),
|
|
368
|
-
|
|
421
|
+
credentialField("scavioApiKeyEnv"),
|
|
369
422
|
textField("scavioBaseURL"),
|
|
370
|
-
|
|
423
|
+
credentialField("firecrawlApiKeyEnv"),
|
|
371
424
|
textField("firecrawlBaseURL")
|
|
372
425
|
].map((spec) => [spec.field, spec]));
|
|
373
426
|
this.staged = new Map();
|
|
@@ -404,7 +457,7 @@ window.__ModuleLoader__.load({
|
|
|
404
457
|
available: snapshot.status === "ready",
|
|
405
458
|
writable: snapshot.writable,
|
|
406
459
|
dirty: plan.length > 0,
|
|
407
|
-
invalid: plan.some((item) => item.
|
|
460
|
+
invalid: plan.some((item) => item.invalid === true),
|
|
408
461
|
saving: this.saving,
|
|
409
462
|
failed: this.failed,
|
|
410
463
|
toastKey: this.toastKey
|
|
@@ -435,19 +488,19 @@ window.__ModuleLoader__.load({
|
|
|
435
488
|
for (const [field, staged] of this.staged) {
|
|
436
489
|
if (SECRET_FIELDS.has(field)) {
|
|
437
490
|
const value = staged.text.trim();
|
|
438
|
-
if (value !== "") plan.push({ field,
|
|
491
|
+
if (value !== "") plan.push({ field, credential: () => this.writeKey(field, value) });
|
|
439
492
|
continue;
|
|
440
493
|
}
|
|
441
494
|
const spec = this.specs.get(field);
|
|
442
495
|
if (staged.clear) {
|
|
443
|
-
if (this.stored(field)) plan.push({ field,
|
|
496
|
+
if (this.stored(field)) plan.push({ field, op: { op: "unset", path: [field] } });
|
|
444
497
|
continue;
|
|
445
498
|
}
|
|
446
499
|
if (staged.text === spec.format(this.sectionValue(field))) continue;
|
|
447
500
|
const write = spec.parse(staged.text);
|
|
448
|
-
if (write === void 0) plan.push({ field,
|
|
449
|
-
else if (write.kind === "clear") plan.push({ field,
|
|
450
|
-
else plan.push({ field,
|
|
501
|
+
if (write === void 0) plan.push({ field, invalid: true });
|
|
502
|
+
else if (write.kind === "clear") plan.push({ field, op: { op: "unset", path: [field] } });
|
|
503
|
+
else plan.push({ field, op: { op: "set", path: [field], value: write.value } });
|
|
451
504
|
}
|
|
452
505
|
return plan;
|
|
453
506
|
}
|
|
@@ -463,22 +516,31 @@ window.__ModuleLoader__.load({
|
|
|
463
516
|
}
|
|
464
517
|
async save() {
|
|
465
518
|
const plan = this.plan();
|
|
466
|
-
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
467
519
|
if (this.saving) return;
|
|
468
520
|
if (plan.length === 0) {
|
|
469
521
|
this.showToast("nothing");
|
|
470
522
|
return;
|
|
471
523
|
}
|
|
472
|
-
if (
|
|
524
|
+
if (plan.some((item) => item.invalid === true)) {
|
|
473
525
|
this.showToast("invalid");
|
|
474
526
|
return;
|
|
475
527
|
}
|
|
528
|
+
const revision = this.snapshotOf().revision;
|
|
529
|
+
const credentials = plan.filter((item) => item.credential !== void 0);
|
|
530
|
+
const ops = plan.flatMap((item) => item.op === void 0 ? [] : [item.op]);
|
|
531
|
+
for (const item of credentials) {
|
|
532
|
+
// Override even an inherited literal key; unsetting could expose it again.
|
|
533
|
+
if (!ops.some((op) => op.path[0] === item.field)) ops.push({ op: "set", path: [item.field], value: "" });
|
|
534
|
+
}
|
|
476
535
|
this.saving = true;
|
|
477
536
|
this.failed = false;
|
|
478
537
|
this.publish();
|
|
479
538
|
let landed = true;
|
|
480
539
|
try {
|
|
481
|
-
for (const
|
|
540
|
+
for (const item of credentials) {
|
|
541
|
+
if (!await item.credential()) { landed = false; break; }
|
|
542
|
+
}
|
|
543
|
+
if (landed && ops.length > 0) landed = await this.scope.mutate(ops, revision) === true;
|
|
482
544
|
} catch (_saveFailure) {
|
|
483
545
|
landed = false;
|
|
484
546
|
}
|
|
@@ -486,19 +548,23 @@ window.__ModuleLoader__.load({
|
|
|
486
548
|
this.saving = false;
|
|
487
549
|
this.failed = !landed;
|
|
488
550
|
this.showToast(landed ? "saved" : "failed");
|
|
551
|
+
if (landed) void usageStore.sync();
|
|
489
552
|
}
|
|
490
553
|
actions() {
|
|
491
554
|
return {
|
|
492
555
|
edit: (field, text) => {
|
|
556
|
+
if (this.saving) return;
|
|
493
557
|
this.stage(field, { text, clear: false });
|
|
494
558
|
},
|
|
495
559
|
resetField: (field) => {
|
|
560
|
+
if (this.saving) return;
|
|
496
561
|
this.stage(field, { text: this.baseText(field), clear: true });
|
|
497
562
|
},
|
|
498
563
|
save: () => {
|
|
499
564
|
this.save();
|
|
500
565
|
},
|
|
501
566
|
discard: () => {
|
|
567
|
+
if (this.saving) return;
|
|
502
568
|
if (this.staged.size === 0 && !this.failed) return;
|
|
503
569
|
this.staged.clear();
|
|
504
570
|
this.failed = false;
|
|
@@ -506,22 +572,6 @@ window.__ModuleLoader__.load({
|
|
|
506
572
|
}
|
|
507
573
|
};
|
|
508
574
|
}
|
|
509
|
-
async clear(field) {
|
|
510
|
-
try {
|
|
511
|
-
await this.scope.unset(field);
|
|
512
|
-
return true;
|
|
513
|
-
} catch (_clearFailure) {
|
|
514
|
-
return false;
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
async store(field, value) {
|
|
518
|
-
try {
|
|
519
|
-
await this.scope.set(field, value);
|
|
520
|
-
return true;
|
|
521
|
-
} catch (_storeFailure) {
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
575
|
stage(field, edit) {
|
|
526
576
|
this.staged.set(field, edit);
|
|
527
577
|
this.failed = false;
|
|
@@ -556,17 +606,21 @@ window.__ModuleLoader__.load({
|
|
|
556
606
|
if (declared === "tavily" || declared === "brave" || declared === "deepseek-official" || builtinRest(declared) !== void 0) return declared;
|
|
557
607
|
return "deepseek-official";
|
|
558
608
|
}
|
|
609
|
+
credentialRef(field, fallback) {
|
|
610
|
+
const staged = this.staged.get(field);
|
|
611
|
+
const declared = staged === void 0
|
|
612
|
+
? this.sectionValue(field)
|
|
613
|
+
: staged.clear ? this.snapshotOf().base?.[field] : staged.text.trim();
|
|
614
|
+
return typeof declared === "string" && declared.length > 0 ? declared : fallback;
|
|
615
|
+
}
|
|
559
616
|
tavilyRef() {
|
|
560
|
-
|
|
561
|
-
return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_TAVILY_KEY_REF;
|
|
617
|
+
return this.credentialRef("apiKeyEnv", DEFAULT_TAVILY_KEY_REF);
|
|
562
618
|
}
|
|
563
619
|
braveRef() {
|
|
564
|
-
|
|
565
|
-
return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_BRAVE_KEY_REF;
|
|
620
|
+
return this.credentialRef("braveApiKeyEnv", DEFAULT_BRAVE_KEY_REF);
|
|
566
621
|
}
|
|
567
622
|
deepseekRef() {
|
|
568
|
-
|
|
569
|
-
return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_DEEPSEEK_KEY_REF;
|
|
623
|
+
return this.credentialRef("deepseekApiKeyEnv", DEFAULT_DEEPSEEK_KEY_REF);
|
|
570
624
|
}
|
|
571
625
|
/** The credential reference the currently visible engine uses. */
|
|
572
626
|
refOf() {
|
|
@@ -579,9 +633,8 @@ window.__ModuleLoader__.load({
|
|
|
579
633
|
}
|
|
580
634
|
/** Credential reference for a built-in REST provider's key env field. */
|
|
581
635
|
restKeyRef(rest) {
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
return rest.id === "serper" ? "SERPER_API_KEY" : rest.id === "serpapi" ? "SERPAPI_API_KEY" : rest.id === "exa" ? "EXA_API_KEY" : rest.id === "scavio" ? "SCAVIO_API_KEY" : "FIRECRAWL_API_KEY";
|
|
636
|
+
const fallback = rest.id === "serper" ? "SERPER_API_KEY" : rest.id === "serpapi" ? "SERPAPI_API_KEY" : rest.id === "exa" ? "EXA_API_KEY" : rest.id === "scavio" ? "SCAVIO_API_KEY" : "FIRECRAWL_API_KEY";
|
|
637
|
+
return this.credentialRef(rest.keyEnvField, fallback);
|
|
585
638
|
}
|
|
586
639
|
async readCredential() {
|
|
587
640
|
const ref = this.refOf();
|
|
@@ -621,9 +674,11 @@ window.__ModuleLoader__.load({
|
|
|
621
674
|
try {
|
|
622
675
|
const response = await this.remote.credentials.set(ref, value);
|
|
623
676
|
if (!response.ok) return false;
|
|
624
|
-
} catch (_credentialWriteFailure) {
|
|
677
|
+
} catch (_credentialWriteFailure) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
625
680
|
await this.readCredential();
|
|
626
|
-
return
|
|
681
|
+
return true;
|
|
627
682
|
}
|
|
628
683
|
};
|
|
629
684
|
|
|
@@ -681,10 +736,10 @@ window.__ModuleLoader__.load({
|
|
|
681
736
|
firecrawlApiKeyEnv: this.form.field("firecrawlApiKeyEnv"),
|
|
682
737
|
firecrawlBaseURL: this.form.field("firecrawlBaseURL"),
|
|
683
738
|
apiKeyConfigured: this.form.credential.configured,
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
739
|
+
apiKeyWritable: this.form.credential.writable,
|
|
740
|
+
toastKey: this.form.toastKey
|
|
741
|
+
};
|
|
742
|
+
}
|
|
688
743
|
inject() {
|
|
689
744
|
return {
|
|
690
745
|
hooks: { pluginSearchCard: this.store },
|
|
@@ -701,7 +756,7 @@ window.__ModuleLoader__.load({
|
|
|
701
756
|
react.createElement("span", { className: "dshws_badge" }, meta.t("overridden")),
|
|
702
757
|
react.createElement("button", { type: "button", className: "dshws_reset", disabled: meta.disabled === true, onClick: meta.onReset }, meta.t("reset"))) : null),
|
|
703
758
|
children,
|
|
704
|
-
meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t("invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
|
|
759
|
+
meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t(meta.errorKey ?? "invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
|
|
705
760
|
const pair = (left, right) => {
|
|
706
761
|
if (!left && !right) return null;
|
|
707
762
|
if (!left) return right;
|
|
@@ -838,8 +893,9 @@ window.__ModuleLoader__.load({
|
|
|
838
893
|
return snap;
|
|
839
894
|
},
|
|
840
895
|
refresh: () => load(true),
|
|
896
|
+
sync: () => load(false),
|
|
841
897
|
start() {
|
|
842
|
-
void load(
|
|
898
|
+
void load(false);
|
|
843
899
|
const timer = setInterval(() => {
|
|
844
900
|
void load(false);
|
|
845
901
|
}, USAGE_POLL_MS);
|
|
@@ -970,7 +1026,7 @@ window.__ModuleLoader__.load({
|
|
|
970
1026
|
function WebSearchCard(props) {
|
|
971
1027
|
const { t, state, actions } = props;
|
|
972
1028
|
if (!state.available) return null;
|
|
973
|
-
const disabled = !state.writable;
|
|
1029
|
+
const disabled = !state.writable || state.saving;
|
|
974
1030
|
const provider = state.provider.text === "tavily" || state.provider.text === "brave" || state.provider.text === "deepseek-official" || builtinRest(state.provider.text) !== void 0 ? state.provider.text : "deepseek-official";
|
|
975
1031
|
const brave = provider === "brave";
|
|
976
1032
|
const deepseek = provider === "deepseek-official";
|
|
@@ -984,6 +1040,7 @@ window.__ModuleLoader__.load({
|
|
|
984
1040
|
t,
|
|
985
1041
|
overridden: state[field]?.overridden === true,
|
|
986
1042
|
invalid: state[field]?.invalid === true,
|
|
1043
|
+
errorKey: field.endsWith("ApiKeyEnv") || field === "apiKeyEnv" ? "invalidCredentialRef" : "invalidNumber",
|
|
987
1044
|
disabled,
|
|
988
1045
|
onReset: () => actions.resetField(field)
|
|
989
1046
|
});
|
|
@@ -1008,9 +1065,9 @@ window.__ModuleLoader__.load({
|
|
|
1008
1065
|
!state.writable ? react.createElement("p", { className: "dshws_field_hint", style: { padding: "8px 0 0" } }, t("readOnly")) : null,
|
|
1009
1066
|
pair(
|
|
1010
1067
|
row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
|
|
1068
|
+
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek")),
|
|
1011
1069
|
react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
|
|
1012
1070
|
react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
|
|
1013
|
-
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek")),
|
|
1014
1071
|
...BUILTIN_REST_PROVIDERS.map((entry) => react.createElement("option", { value: entry.id, key: entry.id }, entry.name)),
|
|
1015
1072
|
react.createElement("option", { value: "__ddg__", key: "ddg", disabled: true }, t("ddgDisabled"))
|
|
1016
1073
|
] }),
|
|
@@ -1021,16 +1078,17 @@ window.__ModuleLoader__.load({
|
|
|
1021
1078
|
] })) : null
|
|
1022
1079
|
),
|
|
1023
1080
|
react.createElement(QuotaCard, { t, provider, keyed }),
|
|
1081
|
+
keyConsoleLink(t, provider, restMeta, keyed),
|
|
1024
1082
|
deepseek ? pair(
|
|
1025
|
-
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })),
|
|
1083
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: disabled || !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })),
|
|
1026
1084
|
row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
|
|
1027
1085
|
) : null,
|
|
1028
1086
|
brave ? pair(
|
|
1029
|
-
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })),
|
|
1087
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: disabled || !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })),
|
|
1030
1088
|
row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
|
|
1031
1089
|
) : null,
|
|
1032
1090
|
(tavily && keyed) ? pair(
|
|
1033
|
-
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })),
|
|
1091
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: disabled || !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })),
|
|
1034
1092
|
row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
|
|
1035
1093
|
) : null,
|
|
1036
1094
|
deepseek ? row("deepseekBaseURL", t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value) })) : null,
|
|
@@ -1075,15 +1133,8 @@ window.__ModuleLoader__.load({
|
|
|
1075
1133
|
brave ? row("proxy", t("proxy"), null, textInput({ id: "ws-proxy", value: state.proxy.text, disabled, placeholder: t("proxyPlaceholder"), onChange: (e) => actions.edit("proxy", e.target.value) })) : null,
|
|
1076
1134
|
deepseek ? maxResultsRow : null,
|
|
1077
1135
|
restMeta ? react.createElement("div", { className: "dshws_form" },
|
|
1078
|
-
restMeta.keyRequired && restMeta.officialUrl ? react.createElement("a", {
|
|
1079
|
-
href: restMeta.officialUrl,
|
|
1080
|
-
target: "_blank",
|
|
1081
|
-
rel: "noopener noreferrer",
|
|
1082
|
-
className: "dshws_field_hint",
|
|
1083
|
-
style: { color: "var(--dsw-alias-brand-primary,#3b82f6)", textDecoration: "none", padding: "8px 0", display: "inline-block" }
|
|
1084
|
-
}, t("getKey") + " ↗") : null,
|
|
1085
1136
|
restMeta.keyRequired ? pair(
|
|
1086
|
-
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-" + restMeta.id + "-key", value: state[restMeta.keyField]?.text ?? "", disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(restMeta.keyField, e.target.value) })),
|
|
1137
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-" + restMeta.id + "-key", value: state[restMeta.keyField]?.text ?? "", disabled: disabled || !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(restMeta.keyField, e.target.value) })),
|
|
1087
1138
|
row(restMeta.keyEnvField, t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-" + restMeta.id + "-keyref", value: state[restMeta.keyEnvField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.keyEnvField, e.target.value) }))
|
|
1088
1139
|
) : null,
|
|
1089
1140
|
row(restMeta.baseURLField, t("baseURL"), t("baseURLHint"), textInput({ id: "ws-" + restMeta.id + "-base", value: state[restMeta.baseURLField]?.text || restMeta.defaultBaseURL || "", disabled, onChange: (e) => actions.edit(restMeta.baseURLField, e.target.value) })),
|
|
@@ -1112,17 +1163,37 @@ window.__ModuleLoader__.load({
|
|
|
1112
1163
|
}
|
|
1113
1164
|
|
|
1114
1165
|
/** Section component: the "Web search" page body, driven by useSyncExternalStore. */
|
|
1115
|
-
function makeSection(ctx
|
|
1166
|
+
function makeSection(ctx) {
|
|
1116
1167
|
return function WebSearchSection() {
|
|
1117
|
-
|
|
1118
|
-
const t = ctx.locale.bind(NS);
|
|
1119
|
-
if (!state.available) return react.createElement("p", { style: { padding: "16px" } }, t("readOnly"));
|
|
1168
|
+
if (shared.controller === null) return null;
|
|
1120
1169
|
return react.createElement("div", { style: { padding: "16px" } },
|
|
1121
|
-
react.createElement(
|
|
1122
|
-
react.createElement(WebSearchCard, { t: t, state: state, actions: controller.form.actions() })));
|
|
1170
|
+
react.createElement(WebSearchConfigCard, { ctx: ctx }));
|
|
1123
1171
|
};
|
|
1124
1172
|
}
|
|
1125
1173
|
|
|
1174
|
+
/**
|
|
1175
|
+
* The settings card controller, or `null` when the Host does not serve
|
|
1176
|
+
* this namespace.
|
|
1177
|
+
* @type {{controller: SearchCardController | null}}
|
|
1178
|
+
*/
|
|
1179
|
+
const shared = { controller: null };
|
|
1180
|
+
|
|
1181
|
+
/**
|
|
1182
|
+
* The configuration card reads the shared controller's store and draws
|
|
1183
|
+
* the bordered panel in the settings section.
|
|
1184
|
+
* @param props - `ctx` for locale binding.
|
|
1185
|
+
* @returns the card element, or null while the namespace is unserved.
|
|
1186
|
+
*/
|
|
1187
|
+
function WebSearchConfigCard(props) {
|
|
1188
|
+
const controller = shared.controller;
|
|
1189
|
+
if (controller === null) return null;
|
|
1190
|
+
const state = react.useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot);
|
|
1191
|
+
const t = props.ctx.locale.bind(NS);
|
|
1192
|
+
if (!state.available) return react.createElement("p", { style: { padding: "16px" } }, t("readOnly"));
|
|
1193
|
+
return react.createElement("div", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", padding: "4px 16px" } },
|
|
1194
|
+
react.createElement(WebSearchCard, { t: t, state: state, actions: controller.form.actions() }));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1126
1197
|
function syncSettingsNavIcon() {
|
|
1127
1198
|
if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return;
|
|
1128
1199
|
const lists = document.querySelectorAll("[class*='_navList']");
|
|
@@ -1149,26 +1220,43 @@ window.__ModuleLoader__.load({
|
|
|
1149
1220
|
return () => observer.disconnect();
|
|
1150
1221
|
}
|
|
1151
1222
|
|
|
1152
|
-
/** Mount the top-level "Web search" settings section. */
|
|
1153
1223
|
function apply(ctx) {
|
|
1154
1224
|
const t = ctx.locale.bind(NS);
|
|
1155
1225
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-web-search-plugin: card dictionaries");
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1226
|
+
|
|
1227
|
+
// One controller for the settings section, created as soon as the Host
|
|
1228
|
+
// serves this namespace and torn down with it.
|
|
1229
|
+
ctx.effect(() => ctx.configForms.whileServed([NS], () => {
|
|
1230
|
+
shared.controller = new SearchCardController(ctx.configForms.get(NS), ctx.remote);
|
|
1231
|
+
const stopUsage = usageStore.start();
|
|
1232
|
+
const stopCredentials = ctx.remote.$on("credentials/reference-updated", (ref) => {
|
|
1233
|
+
shared.controller?.form.refreshCredential(ref);
|
|
1234
|
+
});
|
|
1235
|
+
const stopNavIcon = startSettingsNavIconSync();
|
|
1236
|
+
// The navigation entry belongs to the same lifetime as its Host form.
|
|
1237
|
+
let stopSection = () => {};
|
|
1238
|
+
const sectionFiber = ctx.slots.inject("settings.section", () => {
|
|
1239
|
+
stopSection = ctx.slots.register({
|
|
1240
|
+
name: "settings.section",
|
|
1241
|
+
id: "web-search",
|
|
1242
|
+
order: 10,
|
|
1243
|
+
label: () => t("nav"),
|
|
1244
|
+
locale: NS
|
|
1245
|
+
}, makeSection(ctx));
|
|
1246
|
+
return stopSection;
|
|
1247
|
+
});
|
|
1248
|
+
return () => {
|
|
1249
|
+
shared.controller = null;
|
|
1250
|
+
stopUsage();
|
|
1251
|
+
stopCredentials();
|
|
1252
|
+
stopNavIcon();
|
|
1253
|
+
stopSection();
|
|
1254
|
+
void sectionFiber.dispose();
|
|
1255
|
+
};
|
|
1256
|
+
}), "dsh-web-search-plugin: forms");
|
|
1169
1257
|
}
|
|
1170
1258
|
|
|
1171
|
-
const inject = ["slots", "locale", "remote", "
|
|
1259
|
+
const inject = ["slots", "locale", "remote", "remote.credentials", "configForms"];
|
|
1172
1260
|
|
|
1173
1261
|
exports.apply = apply;
|
|
1174
1262
|
exports.inject = inject;
|