dsh-web-search-plugin 0.1.1 → 0.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/client.js CHANGED
@@ -1,7 +1,9 @@
1
1
  /**
2
- * Browser half of `dsh-web-search-plugin`: a plugin card under the
3
- * "settings → Plugins → Plugin configuration" surface that edits the
4
- * `dsh-web-search-plugin` settings namespace and its `TAVILY_API_KEY` credential.
2
+ * Browser half of `dsh-web-search-plugin`: a top-level "Web search" settings
3
+ * section (settings → Web search) that edits the `dsh-web-search-plugin`
4
+ * settings namespace. The card switches the DeepSeek / Tavily / Brave backend
5
+ * and writes each engine's credential through the credentials domain
6
+ * (`DEEPSEEK_API_KEY` / `TAVILY_API_KEY` / `BRAVE_API_KEY` by default).
5
7
  *
6
8
  * This bundle is a self-contained hand-written module in the exact wire format
7
9
  * the client module system expects: one `window.__ModuleLoader__.load` call
@@ -21,28 +23,45 @@ window.__ModuleLoader__.load({
21
23
  let react = require("react");
22
24
  let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
23
25
 
24
- /** Settings namespace of the Tavily search provider. */
26
+ /** Settings namespace of this plugin. */
25
27
  const NS = "dsh-web-search-plugin";
26
- /** Credential reference the provider resolves when the section names none. */
27
- const DEFAULT_API_KEY_REF = "TAVILY_API_KEY";
28
- /** Form field the credential control stages under. */
29
- const API_KEY_FIELD = "apiKey";
28
+ /** Credential reference Tavily keyed mode resolves when the section names none. */
29
+ const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
30
+ /** Credential reference Brave resolves when the section names none. */
31
+ const DEFAULT_BRAVE_KEY_REF = "BRAVE_API_KEY";
32
+ /** Credential reference DeepSeek resolves when the section names none. */
33
+ const DEFAULT_DEEPSEEK_KEY_REF = "DEEPSEEK_API_KEY";
34
+ /** Form field the Tavily write-only credential control stages under. */
35
+ const TAVILY_API_KEY_FIELD = "apiKey";
36
+ /** Form field the Brave write-only credential control stages under. */
37
+ const BRAVE_API_KEY_FIELD = "braveApiKey";
38
+ /** Form field the DeepSeek write-only credential control stages under. */
39
+ const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
40
+ const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD]);
30
41
 
31
42
  /** Locale bundles for the card. */
32
43
  const en = {
33
- nav: "Web search (Tavily)",
34
- description: "The Tavily search provider. Keyless by default; switch to keyed and add a Tavily API key for higher limits.",
44
+ nav: "Web search",
45
+ description: "Tavily (keyless by default) or Brave Search. Switching engines here is enough — web.searchProvider stays dsh-web-search.",
46
+ provider: "Search engine",
47
+ providerTavily: "Tavily",
48
+ providerBrave: "Brave Search",
49
+ providerDeepseek: "DeepSeek (official)",
35
50
  mode: "Auth mode",
36
51
  modeKeyless: "Keyless (free, rate-limited)",
37
52
  modeKeyed: "API key",
38
53
  apiKey: "API key",
39
54
  apiKeyHint: "Stored outside the settings file. Leave blank to keep the current key.",
40
55
  apiKeySet: "A key is configured.",
41
- apiKeyUnset: "No key is configured; keyed search is unavailable until one is.",
56
+ apiKeyUnset: "No key is configured; keyed / Brave search is unavailable until one is.",
42
57
  apiKeyEnv: "Credential reference",
43
- apiKeyEnvHint: "Name of the credential/env var resolved for keyed search.",
58
+ apiKeyEnvHint: "Name of the credential/env var resolved for keyed Tavily search.",
59
+ braveApiKeyEnv: "Credential reference",
60
+ braveApiKeyEnvHint: "Name of the credential/env var resolved for Brave search.",
44
61
  baseURL: "Endpoint",
45
62
  baseURLHint: "Leave blank to use the provider default.",
63
+ braveBaseURL: "Endpoint",
64
+ braveBaseURLHint: "Leave blank to use the Brave Search API default.",
46
65
  maxResults: "Max results",
47
66
  maxResultsHint: "Sources returned per search (1-20).",
48
67
  searchDepth: "Search depth",
@@ -53,6 +72,30 @@ window.__ModuleLoader__.load({
53
72
  topic: "Topic",
54
73
  topicGeneral: "General",
55
74
  topicNews: "News",
75
+ country: "Country",
76
+ countryHint: "ISO 2-letter code (e.g. cn). Leave blank for Brave's default.",
77
+ searchLang: "Search language",
78
+ searchLangHint: "e.g. zh-hans or en. Leave blank for Brave's default.",
79
+ freshness: "Freshness",
80
+ freshnessAny: "Any time",
81
+ freshnessDay: "Past day",
82
+ freshnessWeek: "Past week",
83
+ freshnessMonth: "Past month",
84
+ freshnessYear: "Past year",
85
+ proxy: "Proxy",
86
+ proxyHint: "HTTP(S) proxy URL. Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
87
+ deepseekApiKeyEnv: "Credential reference",
88
+ deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
89
+ deepseekBaseURL: "Endpoint",
90
+ deepseekBaseURLHint: "Leave blank to use the DeepSeek Anthropic-compatible API default.",
91
+ model: "Model",
92
+ modelHint: "Anthropic-format model name for the search request.",
93
+ apiVersion: "API version",
94
+ apiVersionHint: "anthropic-version header value.",
95
+ maxTokens: "Max tokens",
96
+ maxTokensHint: "Upper bound on generated tokens for the search request.",
97
+ maxUses: "Max uses",
98
+ maxUsesHint: "Maximum web_search server-tool uses per request.",
56
99
  expand: "Show settings",
57
100
  collapse: "Hide settings",
58
101
  save: "Save",
@@ -65,19 +108,27 @@ window.__ModuleLoader__.load({
65
108
  };
66
109
  /** Simplified Chinese copy. */
67
110
  const zh = {
68
- nav: "网页搜索(Tavily)",
69
- description: "Tavily 搜索提供方。默认 keyless 免费使用;切换到 API key 并填入 Tavily key 可提高限额。",
111
+ nav: "网页搜索",
112
+ description: "Tavily(默认 keyless)或 Brave Search。在此切换引擎即可,不必改 web.searchProvider。",
113
+ provider: "搜索引擎",
114
+ providerTavily: "Tavily",
115
+ providerBrave: "Brave Search",
116
+ providerDeepseek: "DeepSeek(官方)",
70
117
  mode: "认证模式",
71
118
  modeKeyless: "Keyless(免费、限流)",
72
119
  modeKeyed: "API key",
73
120
  apiKey: "API Key",
74
121
  apiKeyHint: "不写入设置文件。留空表示保持当前密钥。",
75
122
  apiKeySet: "已配置密钥。",
76
- apiKeyUnset: "未配置密钥;keyed 模式下搜索不可用。",
123
+ apiKeyUnset: "未配置密钥;keyed / Brave 模式下搜索不可用。",
77
124
  apiKeyEnv: "凭据引用名",
78
125
  apiKeyEnvHint: "keyed 模式解析的凭据/环境变量名。",
126
+ braveApiKeyEnv: "凭据引用名",
127
+ braveApiKeyEnvHint: "Brave 搜索解析的凭据/环境变量名。",
79
128
  baseURL: "接口地址",
80
129
  baseURLHint: "留空则使用提供方默认地址。",
130
+ braveBaseURL: "接口地址",
131
+ braveBaseURLHint: "留空则使用 Brave Search API 默认地址。",
81
132
  maxResults: "结果数量",
82
133
  maxResultsHint: "每次搜索返回的源数量(1-20)。",
83
134
  searchDepth: "搜索深度",
@@ -88,6 +139,30 @@ window.__ModuleLoader__.load({
88
139
  topic: "主题",
89
140
  topicGeneral: "通用",
90
141
  topicNews: "新闻",
142
+ country: "国家/地区",
143
+ countryHint: "ISO 两位码(如 cn)。留空使用 Brave 默认。",
144
+ searchLang: "搜索语言",
145
+ searchLangHint: "如 zh-hans、en。留空使用 Brave 默认。",
146
+ freshness: "时效",
147
+ freshnessAny: "不限",
148
+ freshnessDay: "过去一天",
149
+ freshnessWeek: "过去一周",
150
+ freshnessMonth: "过去一月",
151
+ freshnessYear: "过去一年",
152
+ proxy: "代理",
153
+ proxyHint: "HTTP(S) 代理 URL。留空则使用 HTTPS_PROXY / HTTP_PROXY。",
154
+ deepseekApiKeyEnv: "凭据引用名",
155
+ deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
156
+ deepseekBaseURL: "接口地址",
157
+ deepseekBaseURLHint: "留空则使用 DeepSeek Anthropic 兼容 API 默认地址。",
158
+ model: "模型",
159
+ modelHint: "搜索请求使用的 Anthropic 格式模型名。",
160
+ apiVersion: "API 版本",
161
+ apiVersionHint: "anthropic-version 请求头值。",
162
+ maxTokens: "最大 token",
163
+ maxTokensHint: "搜索请求生成 token 的上限。",
164
+ maxUses: "最大调用次数",
165
+ maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
91
166
  expand: "展开设置",
92
167
  collapse: "收起设置",
93
168
  save: "保存",
@@ -123,7 +198,7 @@ window.__ModuleLoader__.load({
123
198
  }
124
199
  };
125
200
  }
126
- /** A choice field spec (mode/searchDepth/topic): staged as its raw string. */
201
+ /** A choice field spec: staged as its raw string. */
127
202
  function choiceField(field) {
128
203
  return {
129
204
  field,
@@ -141,16 +216,36 @@ window.__ModuleLoader__.load({
141
216
  }
142
217
 
143
218
  /**
144
- * Staged form over the `dsh-web-search-plugin` settings namespace. Modeled on
145
- * the official cards' `CardForm` contract: a draft map, save writes every
146
- * staged edit once, and the API key is written through the credentials
147
- * domain addressed by the reference the section names.
219
+ * Staged form over the `dsh-web-search-plugin` settings namespace.
220
+ * Write-only credential fields (`apiKey`, `braveApiKey`) have no section
221
+ * spec and are written through the credentials domain.
148
222
  */
149
- var TavilyCardForm = class {
223
+ var SearchCardForm = class {
150
224
  constructor(scope, api) {
151
225
  this.scope = scope;
152
226
  this.api = api;
153
- this.specs = new Map([numberField("maxResults"), textField("apiKeyEnv"), textField("baseURL"), choiceField("mode"), choiceField("searchDepth"), choiceField("topic"), booleanField("includeAnswer")].map((spec) => [spec.field, spec]));
227
+ this.specs = new Map([
228
+ choiceField("provider"),
229
+ choiceField("mode"),
230
+ numberField("maxResults"),
231
+ textField("apiKeyEnv"),
232
+ textField("baseURL"),
233
+ choiceField("searchDepth"),
234
+ choiceField("topic"),
235
+ booleanField("includeAnswer"),
236
+ textField("braveApiKeyEnv"),
237
+ textField("braveBaseURL"),
238
+ textField("country"),
239
+ textField("searchLang"),
240
+ choiceField("freshness"),
241
+ textField("proxy"),
242
+ textField("deepseekApiKeyEnv"),
243
+ textField("deepseekBaseURL"),
244
+ textField("model"),
245
+ textField("apiVersion"),
246
+ numberField("maxTokens"),
247
+ numberField("maxUses")
248
+ ].map((spec) => [spec.field, spec]));
154
249
  this.staged = new Map();
155
250
  this.saving = false;
156
251
  this.failed = false;
@@ -182,10 +277,7 @@ window.__ModuleLoader__.load({
182
277
  }
183
278
  field(field) {
184
279
  const staged = this.staged.get(field);
185
- // Write-only credential controls have no section spec; they
186
- // report only their staged draft (mirroring the official
187
- // cards' secret-field branch in CardForm.field).
188
- if (field === API_KEY_FIELD) return {
280
+ if (SECRET_FIELDS.has(field)) return {
189
281
  text: staged?.text ?? "",
190
282
  overridden: false,
191
283
  invalid: false
@@ -206,9 +298,9 @@ window.__ModuleLoader__.load({
206
298
  plan() {
207
299
  const plan = [];
208
300
  for (const [field, staged] of this.staged) {
209
- if (field === API_KEY_FIELD) {
301
+ if (SECRET_FIELDS.has(field)) {
210
302
  const value = staged.text.trim();
211
- if (value !== "") plan.push({ field, run: () => this.writeKey(value) });
303
+ if (value !== "") plan.push({ field, run: () => this.writeKey(field, value) });
212
304
  continue;
213
305
  }
214
306
  const spec = this.specs.get(field);
@@ -269,6 +361,7 @@ window.__ModuleLoader__.load({
269
361
  this.staged.set(field, edit);
270
362
  this.failed = false;
271
363
  this.publish();
364
+ if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv") this.readCredential();
272
365
  }
273
366
  snapshotOf() {
274
367
  return this.scope.getSnapshot();
@@ -289,12 +382,30 @@ window.__ModuleLoader__.load({
289
382
  publish() {
290
383
  for (const listener of this.listeners) listener();
291
384
  }
292
- /** The credential reference the section names, or the provider default. */
293
- refOf() {
385
+ /** Draft-aware engine: staged `provider` wins over the saved section. */
386
+ providerOf() {
387
+ const staged = this.staged.get("provider");
388
+ if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
389
+ const declared = this.sectionValue("provider");
390
+ return declared === "brave" || declared === "deepseek-official" ? declared : "tavily";
391
+ }
392
+ tavilyRef() {
294
393
  const declared = this.sectionValue("apiKeyEnv");
295
- return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_API_KEY_REF;
394
+ return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_TAVILY_KEY_REF;
395
+ }
396
+ braveRef() {
397
+ const declared = this.sectionValue("braveApiKeyEnv");
398
+ return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_BRAVE_KEY_REF;
399
+ }
400
+ deepseekRef() {
401
+ const declared = this.sectionValue("deepseekApiKeyEnv");
402
+ return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_DEEPSEEK_KEY_REF;
403
+ }
404
+ /** The credential reference the currently visible engine uses. */
405
+ refOf() {
406
+ const provider = this.providerOf();
407
+ return provider === "brave" ? this.braveRef() : provider === "deepseek-official" ? this.deepseekRef() : this.tavilyRef();
296
408
  }
297
- /** Ask the credentials domain about the reference the section currently names. */
298
409
  async readCredential() {
299
410
  const ref = this.refOf();
300
411
  if (ref !== this.credential.ref) {
@@ -318,27 +429,26 @@ window.__ModuleLoader__.load({
318
429
  this.credential = next;
319
430
  this.publish();
320
431
  }
321
- /** Re-read after the Host reports a change to the reference this card watches. */
322
432
  refreshCredential(ref) {
323
433
  if (ref !== this.credential.ref) return;
324
434
  this.readCredential();
325
435
  }
326
- /** Write the staged key, then re-read whether the Host now holds one. */
327
- async writeKey(value) {
436
+ async writeKey(field, value) {
437
+ const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : field === DEEPSEEK_API_KEY_FIELD ? this.deepseekRef() : this.tavilyRef();
328
438
  try {
329
- await this.api.credentials.set({ ref: this.refOf(), value });
439
+ await this.api.credentials.set({ ref, value });
330
440
  } catch (_credentialWriteFailure) {}
331
441
  await this.readCredential();
332
- return this.credential.configured;
442
+ return this.credential.ref === ref ? this.credential.configured : true;
333
443
  }
334
444
  };
335
445
 
336
- /** Bridges the `dsh-web-search-plugin` scope and the credentials domain onto the card. */
337
- var TavilyCardController = class {
446
+ /** Bridges the plugin scope and the credentials domain onto the card. */
447
+ var SearchCardController = class {
338
448
  constructor(scope, api) {
339
449
  this.scope = scope;
340
450
  this.api = api;
341
- this.form = new TavilyCardForm(scope, api);
451
+ this.form = new SearchCardForm(scope, api);
342
452
  this.store = this.form.bind(() => this.projection());
343
453
  scope.subscribe(() => {
344
454
  this.form.readCredential();
@@ -347,21 +457,36 @@ window.__ModuleLoader__.load({
347
457
  projection() {
348
458
  return {
349
459
  ...this.form.shell(),
460
+ provider: this.form.field("provider"),
350
461
  mode: this.form.field("mode"),
351
- apiKey: this.form.field(API_KEY_FIELD),
462
+ apiKey: this.form.field(TAVILY_API_KEY_FIELD),
352
463
  apiKeyEnv: this.form.field("apiKeyEnv"),
353
464
  baseURL: this.form.field("baseURL"),
354
465
  maxResults: this.form.field("maxResults"),
355
466
  searchDepth: this.form.field("searchDepth"),
356
467
  includeAnswer: this.form.field("includeAnswer"),
357
468
  topic: this.form.field("topic"),
469
+ braveApiKey: this.form.field(BRAVE_API_KEY_FIELD),
470
+ braveApiKeyEnv: this.form.field("braveApiKeyEnv"),
471
+ braveBaseURL: this.form.field("braveBaseURL"),
472
+ country: this.form.field("country"),
473
+ searchLang: this.form.field("searchLang"),
474
+ freshness: this.form.field("freshness"),
475
+ proxy: this.form.field("proxy"),
476
+ deepseekApiKey: this.form.field(DEEPSEEK_API_KEY_FIELD),
477
+ deepseekApiKeyEnv: this.form.field("deepseekApiKeyEnv"),
478
+ deepseekBaseURL: this.form.field("deepseekBaseURL"),
479
+ model: this.form.field("model"),
480
+ apiVersion: this.form.field("apiVersion"),
481
+ maxTokens: this.form.field("maxTokens"),
482
+ maxUses: this.form.field("maxUses"),
358
483
  apiKeyConfigured: this.form.credential.configured,
359
484
  apiKeyWritable: this.form.credential.writable
360
485
  };
361
486
  }
362
487
  inject() {
363
488
  return {
364
- hooks: { tavilySearchCard: this.store },
489
+ hooks: { pluginSearchCard: this.store },
365
490
  ...this.form.actions()
366
491
  };
367
492
  }
@@ -372,75 +497,106 @@ window.__ModuleLoader__.load({
372
497
  react.createElement("label", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "13px", fontWeight: 500, lineHeight: "1.5" } }, label),
373
498
  children,
374
499
  hint ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: "12px", lineHeight: "1.5" } }, hint) : null);
375
- /** Text/number input. */
376
500
  const textInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "text", ...props.numeric ? { inputMode: "numeric" } : {}, ...props });
377
- /** Password input for the write-only credential. */
378
501
  const secretInput = (props) => react.createElement("input", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, type: "password", autoComplete: "off", ...props });
379
- /** Choice select. */
380
502
  const selectInput = (props) => react.createElement("select", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", height: "34px", font: "inherit", color: "var(--dsw-alias-label-primary)", borderRadius: "8px", padding: "0 12px", fontSize: "13px", lineHeight: "1.5" }, ...props });
381
- /** Checkbox for booleans. */
382
503
  const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
383
504
 
384
- /** Render one plugin card: header disclosure plus the staged form. */
385
- function TavilyCard(props) {
386
- const { t } = props;
387
- const state = props.useTavilySearchCard((snapshot) => snapshot);
388
- const [open, setOpen] = react.useState(false);
505
+ /** Render the provider selector plus the backend-specific staged form. */
506
+ function WebSearchCard(props) {
507
+ const { t, state, actions } = props;
389
508
  if (!state.available) return null;
390
509
  const disabled = !state.writable;
391
510
  const blocked = !state.dirty || state.invalid || state.saving;
392
- return react.createElement("li", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", listStyle: "none" } },
393
- react.createElement("button", { type: "button", "aria-expanded": open, onClick: () => setOpen(!open), style: { appearance: "none", width: "100%", font: "inherit", color: "inherit", textAlign: "left", cursor: "pointer", background: "0 0", border: "0", borderRadius: "12px", alignItems: "center", gap: "12px", padding: "14px 16px", display: "flex" } },
394
- react.createElement("span", { style: { flexDirection: "column", flex: 1, gap: "4px", minWidth: 0, display: "flex" } },
395
- react.createElement("span", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "15px", fontWeight: 600, lineHeight: "1.4" } }, t("nav")),
396
- react.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "13px", lineHeight: "1.5" } }, t("description"))),
397
- state.dirty ? react.createElement("span", { style: { whiteSpace: "nowrap", background: "var(--dsw-alias-bg-module-platform)", color: "var(--dsw-alias-label-secondary)", borderRadius: "999px", flex: "none", padding: "1px 8px", fontSize: "11px", fontWeight: 500, lineHeight: "17px" } }, t("unsaved")) : null,
398
- react.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", flex: "none" } }, open ? "▾" : "▸")),
399
- open ? react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", margin: "0 16px", paddingBottom: "8px" } },
400
- !state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "12px 0 0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
401
- fieldRow(t("mode"), null, selectInput({ id: "plugin-config-web-search-mode", value: state.mode.text, disabled, onChange: (e) => props.edit("mode", e.target.value), children: [
402
- react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
403
- react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
404
- ] })),
405
- state.mode.text === "keyed" ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "plugin-config-web-search-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => props.edit("apiKey", e.target.value) })) : null,
406
- state.mode.text === "keyed" ? fieldRow(t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "plugin-config-web-search-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => props.edit("apiKeyEnv", e.target.value), onReset: () => props.resetField("apiKeyEnv") })) : null,
407
- fieldRow(t("baseURL"), t("baseURLHint"), textInput({ id: "plugin-config-web-search-base", value: state.baseURL.text, disabled, onChange: (e) => props.edit("baseURL", e.target.value), onReset: () => props.resetField("baseURL") })),
408
- fieldRow(t("searchDepth"), null, selectInput({ id: "plugin-config-web-search-depth", value: state.searchDepth.text, disabled, onChange: (e) => props.edit("searchDepth", e.target.value), children: [
409
- react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
410
- react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
411
- ] })),
412
- fieldRow(t("topic"), null, selectInput({ id: "plugin-config-web-search-topic", value: state.topic.text, disabled, onChange: (e) => props.edit("topic", e.target.value), children: [
413
- react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
414
- react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
415
- ] })),
416
- fieldRow(t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "plugin-config-web-search-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => props.edit("includeAnswer", e.target.checked ? "true" : "") })),
417
- fieldRow(t("maxResults"), t("maxResultsHint"), textInput({ id: "plugin-config-web-search-max", numeric: true, value: state.maxResults.text, disabled, onChange: (e) => props.edit("maxResults", e.target.value), onReset: () => props.resetField("maxResults") })),
418
- react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: "8px", padding: "12px 0 4px", display: "flex" } },
419
- state.failed ? react.createElement("p", { role: "status", style: { minWidth: 0, color: "var(--dsw-alias-label-error)", flex: 1, margin: 0, fontSize: "12px", lineHeight: "1.5" } }, t("saveFailed")) : null,
420
- react.createElement("button", { type: "button", disabled: !state.dirty || state.saving, onClick: props.discard, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-secondary)", background: "0 0" } }, t("discard")),
421
- react.createElement("button", { type: "button", disabled: blocked, onClick: props.save, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-on-brand)", background: "var(--dsw-alias-bg-brand-solid)" } }, t(state.saving ? "saving" : "save")))) : null);
511
+ const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
512
+ const brave = provider === "brave";
513
+ const deepseek = provider === "deepseek-official";
514
+ const keyed = state.mode.text === "keyed";
515
+ return react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px" } },
516
+ !state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
517
+ fieldRow(t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
518
+ react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
519
+ react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
520
+ react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
521
+ ] })),
522
+ deepseek ? null : brave ? null : fieldRow(t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
523
+ react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
524
+ react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
525
+ ] })),
526
+ // API key + credential reference for the visible backend.
527
+ deepseek ? fieldRow(t("apiKey"), t("apiKeyHint"), 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) })) : null,
528
+ deepseek ? fieldRow(t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value), onReset: () => actions.resetField("deepseekApiKeyEnv") })) : null,
529
+ brave ? fieldRow(t("apiKey"), t("apiKeyHint"), 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) })) : null,
530
+ brave ? fieldRow(t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value), onReset: () => actions.resetField("braveApiKeyEnv") })) : null,
531
+ (!brave && !deepseek && keyed) ? fieldRow(t("apiKey"), t("apiKeyHint"), 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) })) : null,
532
+ (!brave && !deepseek && keyed) ? fieldRow(t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value), onReset: () => actions.resetField("apiKeyEnv") })) : null,
533
+ // Endpoint.
534
+ deepseek ? fieldRow(t("deepseekBaseURL"), t("deepseekBaseURLHint"), textInput({ id: "ws-deepseek-base", value: state.deepseekBaseURL.text, disabled, onChange: (e) => actions.edit("deepseekBaseURL", e.target.value), onReset: () => actions.resetField("deepseekBaseURL") })) : null,
535
+ brave ? fieldRow(t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value), onReset: () => actions.resetField("braveBaseURL") })) : null,
536
+ (!brave && !deepseek) ? fieldRow(t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value), onReset: () => actions.resetField("baseURL") })) : null,
537
+ // DeepSeek options.
538
+ deepseek ? fieldRow(t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value), onReset: () => actions.resetField("model") })) : null,
539
+ deepseek ? fieldRow(t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value), onReset: () => actions.resetField("apiVersion") })) : null,
540
+ deepseek ? fieldRow(t("maxTokens"), t("maxTokensHint"), textInput({ id: "ws-maxtokens", numeric: true, value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), onReset: () => actions.resetField("maxTokens") })) : null,
541
+ deepseek ? fieldRow(t("maxUses"), t("maxUsesHint"), textInput({ id: "ws-maxuses", numeric: true, value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), onReset: () => actions.resetField("maxUses") })) : null,
542
+ // Tavily options.
543
+ (!brave && !deepseek) ? fieldRow(t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
544
+ react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
545
+ react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
546
+ ] })) : null,
547
+ (!brave && !deepseek) ? fieldRow(t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
548
+ react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
549
+ react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
550
+ ] })) : null,
551
+ (!brave && !deepseek) ? fieldRow(t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })) : null,
552
+ // Brave options.
553
+ brave ? fieldRow(t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, onChange: (e) => actions.edit("country", e.target.value), onReset: () => actions.resetField("country") })) : null,
554
+ brave ? fieldRow(t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, onChange: (e) => actions.edit("searchLang", e.target.value), onReset: () => actions.resetField("searchLang") })) : null,
555
+ brave ? fieldRow(t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
556
+ react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
557
+ react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
558
+ react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
559
+ react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
560
+ react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
561
+ ] })) : null,
562
+ brave ? fieldRow(t("proxy"), t("proxyHint"), textInput({ id: "ws-proxy", value: state.proxy.text, disabled, onChange: (e) => actions.edit("proxy", e.target.value), onReset: () => actions.resetField("proxy") })) : null,
563
+ fieldRow(t("maxResults"), t("maxResultsHint"), textInput({ id: "ws-max", numeric: true, value: state.maxResults.text, disabled, onChange: (e) => actions.edit("maxResults", e.target.value), onReset: () => actions.resetField("maxResults") })),
564
+ react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", justifyContent: "flex-end", alignItems: "center", gap: "8px", padding: "12px 0 4px", display: "flex" } },
565
+ state.failed ? react.createElement("p", { role: "status", style: { minWidth: 0, color: "var(--dsw-alias-label-error)", flex: 1, margin: 0, fontSize: "12px", lineHeight: "1.5" } }, t("saveFailed")) : null,
566
+ react.createElement("button", { type: "button", disabled: !state.dirty || state.saving, onClick: actions.discard, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid var(--dsw-alias-border-l2)", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-secondary)", background: "0 0" } }, t("discard")),
567
+ react.createElement("button", { type: "button", disabled: blocked, onClick: actions.save, style: { appearance: "none", font: "inherit", cursor: "pointer", border: "1px solid transparent", borderRadius: "8px", padding: "5px 14px", fontSize: "13px", lineHeight: "1.5", color: "var(--dsw-alias-label-on-brand)", background: "var(--dsw-alias-bg-brand-solid)" } }, t(state.saving ? "saving" : "save"))));
568
+ }
569
+
570
+ /** Section component: the "Web search" page body, driven by useSyncExternalStore. */
571
+ function makeSection(ctx, controller) {
572
+ return function WebSearchSection() {
573
+ const state = react.useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot);
574
+ const t = ctx.locale.bind(NS);
575
+ if (!state.available) return react.createElement("p", { style: { padding: "16px" } }, t("readOnly"));
576
+ return react.createElement("div", { style: { padding: "16px" } },
577
+ react.createElement("div", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", padding: "4px 16px" } },
578
+ react.createElement(WebSearchCard, { t: t, state: state, actions: controller.form.actions() })));
579
+ };
422
580
  }
423
581
 
424
- /** Mount the Tavily card into the plugin configuration surface. */
582
+ /** Mount the top-level "Web search" settings section. */
425
583
  function apply(ctx) {
426
584
  const { api } = ctx.get("connection");
427
585
  const t = ctx.locale.bind(NS);
428
586
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-web-search-plugin: card dictionaries");
429
- const controller = new TavilyCardController(ctx.settingsScope.bind({ namespace: NS }), api);
587
+ const controller = new SearchCardController(ctx.settingsScope.bind({ namespace: NS }), api);
430
588
  ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
431
589
  controller.form.refreshCredential(ref);
432
590
  }), "dsh-web-search-plugin: credential invalidations");
433
- ctx.slots.inject("settings.plugin.item", function* () {
434
- yield ctx.slots.register({
435
- name: "settings.plugin.item",
436
- key: "dsh-web-search-plugin",
437
- locale: NS,
438
- inject: () => controller.inject()
439
- }, TavilyCard);
440
- });
591
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
592
+ name: "settings.section",
593
+ id: "web-search",
594
+ order: 10,
595
+ label: () => t("nav"),
596
+ locale: NS
597
+ }, makeSection(ctx, controller)));
441
598
  }
442
599
 
443
- /** Cordis service names this browser plugin's apply reads (fiber inject). */
444
600
  const inject = ["slots", "locale", "connection", "remote", "settingsScope"];
445
601
 
446
602
  exports.apply = apply;
@@ -448,4 +604,3 @@ window.__ModuleLoader__.load({
448
604
  return module.exports;
449
605
  }
450
606
  });
451
- //# sourceMappingURL=client.js.map