dsh-web-search-plugin 0.1.2 → 0.2.1

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,9 +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. The card switches the
5
- * Tavily / Brave backend and writes each engine's credential through the
6
- * credentials domain (`TAVILY_API_KEY` / `BRAVE_API_KEY` by default).
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).
7
7
  *
8
8
  * This bundle is a self-contained hand-written module in the exact wire format
9
9
  * the client module system expects: one `window.__ModuleLoader__.load` call
@@ -29,19 +29,52 @@ window.__ModuleLoader__.load({
29
29
  const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
30
30
  /** Credential reference Brave resolves when the section names none. */
31
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";
32
34
  /** Form field the Tavily write-only credential control stages under. */
33
35
  const TAVILY_API_KEY_FIELD = "apiKey";
34
36
  /** Form field the Brave write-only credential control stages under. */
35
37
  const BRAVE_API_KEY_FIELD = "braveApiKey";
36
- const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD]);
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]);
41
+ const DRAFT_KEY = "dsh-web-search-plugin:settings-draft";
42
+
43
+ function readDraft() {
44
+ try {
45
+ const raw = sessionStorage.getItem(DRAFT_KEY);
46
+ if (!raw) return null;
47
+ const parsed = JSON.parse(raw);
48
+ if (parsed === null || typeof parsed !== "object" || parsed.staged === null || typeof parsed.staged !== "object") return null;
49
+ return parsed;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+ function writeDraft(form) {
55
+ try {
56
+ const staged = {};
57
+ for (const [field, edit] of form.staged) {
58
+ if (SECRET_FIELDS.has(field)) continue;
59
+ staged[field] = { text: String(edit?.text ?? ""), clear: edit?.clear === true };
60
+ }
61
+ if (Object.keys(staged).length === 0 && form.failed !== true) {
62
+ sessionStorage.removeItem(DRAFT_KEY);
63
+ return;
64
+ }
65
+ sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ staged, failed: form.failed === true }));
66
+ } catch {
67
+ /* private mode / quota */
68
+ }
69
+ }
37
70
 
38
71
  /** Locale bundles for the card. */
39
72
  const en = {
40
73
  nav: "Web search",
41
- description: "Tavily (keyless by default) or Brave Search. Switching engines here is enough — web.searchProvider stays dsh-web-search.",
42
74
  provider: "Search engine",
43
75
  providerTavily: "Tavily",
44
76
  providerBrave: "Brave Search",
77
+ providerDeepseek: "DeepSeek (official)",
45
78
  mode: "Auth mode",
46
79
  modeKeyless: "Keyless (free, rate-limited)",
47
80
  modeKeyed: "API key",
@@ -58,7 +91,6 @@ window.__ModuleLoader__.load({
58
91
  braveBaseURL: "Endpoint",
59
92
  braveBaseURLHint: "Leave blank to use the Brave Search API default.",
60
93
  maxResults: "Max results",
61
- maxResultsHint: "Sources returned per search (1-20).",
62
94
  searchDepth: "Search depth",
63
95
  searchDepthBasic: "Basic (faster)",
64
96
  searchDepthAdvanced: "Advanced (slower, deeper)",
@@ -68,9 +100,9 @@ window.__ModuleLoader__.load({
68
100
  topicGeneral: "General",
69
101
  topicNews: "News",
70
102
  country: "Country",
71
- countryHint: "ISO 2-letter code (e.g. cn). Leave blank for Brave's default.",
103
+ countryHint: "ISO 2-letter code (e.g. cn).",
72
104
  searchLang: "Search language",
73
- searchLangHint: "e.g. zh-hans or en. Leave blank for Brave's default.",
105
+ searchLangHint: "e.g. zh-hans or en.",
74
106
  freshness: "Freshness",
75
107
  freshnessAny: "Any time",
76
108
  freshnessDay: "Past day",
@@ -78,24 +110,63 @@ window.__ModuleLoader__.load({
78
110
  freshnessMonth: "Past month",
79
111
  freshnessYear: "Past year",
80
112
  proxy: "Proxy",
81
- proxyHint: "HTTP(S) proxy URL. Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
113
+ proxyPlaceholder: "Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
114
+ deepseekApiKeyEnv: "Credential reference",
115
+ deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
116
+ deepseekBaseURL: "Endpoint",
117
+ deepseekBaseURLHint: "Leave blank to use the DeepSeek Anthropic-compatible API default.",
118
+ model: "Model",
119
+ modelHint: "Anthropic-format model name for the search request.",
120
+ apiVersion: "API version",
121
+ apiVersionHint: "anthropic-version header value.",
122
+ maxTokens: "Max tokens",
123
+ maxTokensHint: "Upper bound on generated tokens for the search request.",
124
+ maxUses: "Max uses",
125
+ maxUsesHint: "Maximum web_search server-tool uses per request.",
82
126
  expand: "Show settings",
83
127
  collapse: "Hide settings",
84
128
  save: "Save",
85
129
  saving: "Saving…",
86
130
  discard: "Discard",
87
131
  unsaved: "Unsaved",
132
+ overridden: "Overridden",
133
+ reset: "Restore default",
88
134
  saveFailed: "The deployment did not accept these values; they were left for you to correct.",
135
+ savedToast: "✓ Settings saved and applied",
136
+ nothingToSave: "No changes to save",
137
+ invalidToast: "Fix the highlighted fields before saving.",
89
138
  readOnly: "This deployment stores settings read-only.",
90
- invalidNumber: "Enter a number, or leave blank to use the default."
139
+ invalidNumber: "Enter a whole number of at least 1, or leave blank for the default.",
140
+ quotaTitle: "Quota",
141
+ quotaPlan: "Plan {plan}",
142
+ quotaUsed: "Used {used} / {limit}",
143
+ quotaUsedOnly: "Used {used}",
144
+ quotaRemaining: "{remaining} left",
145
+ quotaUnlimited: "No published limit",
146
+ quotaResets: "Resets {time}",
147
+ quotaCycle: "Resets with the billing cycle",
148
+ quotaBurst: "Burst {remaining}/{limit}",
149
+ quotaBraveCapacity: "Capacity",
150
+ quotaBraveRps: "{limit} requests per second",
151
+ quotaBraveRpsWindow: "Per-second window",
152
+ braveDefaultPlaceholder: "Leave blank for Brave's default.",
153
+ quotaEmpty: "Usage appears after the next search.",
154
+ quotaKeyedHint: "Keyed Tavily: local deductions vs periodic /usage, whichever is higher.",
155
+ quotaBraveUnlimited: "No monthly request cap",
156
+ quotaRefresh: "Refresh usage",
157
+ quotaRefreshing: "Refreshing…",
158
+ quotaError: "Usage unavailable: {error}",
159
+ quotaWarn: "Remaining quota is low.",
160
+ quotaDanger: "Remaining quota is critical.",
161
+ quotaPaygo: "Pay-as-you-go {used}/{limit}"
91
162
  };
92
163
  /** Simplified Chinese copy. */
93
164
  const zh = {
94
165
  nav: "网页搜索",
95
- description: "Tavily(默认 keyless)或 Brave Search。在此切换引擎即可,不必改 web.searchProvider。",
96
166
  provider: "搜索引擎",
97
167
  providerTavily: "Tavily",
98
168
  providerBrave: "Brave Search",
169
+ providerDeepseek: "DeepSeek(官方)",
99
170
  mode: "认证模式",
100
171
  modeKeyless: "Keyless(免费、限流)",
101
172
  modeKeyed: "API key",
@@ -112,7 +183,6 @@ window.__ModuleLoader__.load({
112
183
  braveBaseURL: "接口地址",
113
184
  braveBaseURLHint: "留空则使用 Brave Search API 默认地址。",
114
185
  maxResults: "结果数量",
115
- maxResultsHint: "每次搜索返回的源数量(1-20)。",
116
186
  searchDepth: "搜索深度",
117
187
  searchDepthBasic: "Basic(较快)",
118
188
  searchDepthAdvanced: "Advanced(较慢、更深)",
@@ -122,9 +192,9 @@ window.__ModuleLoader__.load({
122
192
  topicGeneral: "通用",
123
193
  topicNews: "新闻",
124
194
  country: "国家/地区",
125
- countryHint: "ISO 两位码(如 cn)。留空使用 Brave 默认。",
195
+ countryHint: "ISO 两位码(如 cn)。",
126
196
  searchLang: "搜索语言",
127
- searchLangHint: "如 zh-hans、en。留空使用 Brave 默认。",
197
+ searchLangHint: "如 zh-hans、en。",
128
198
  freshness: "时效",
129
199
  freshnessAny: "不限",
130
200
  freshnessDay: "过去一天",
@@ -132,28 +202,72 @@ window.__ModuleLoader__.load({
132
202
  freshnessMonth: "过去一月",
133
203
  freshnessYear: "过去一年",
134
204
  proxy: "代理",
135
- proxyHint: "HTTP(S) 代理 URL。留空则使用 HTTPS_PROXY / HTTP_PROXY。",
205
+ proxyPlaceholder: "留空使用 HTTPS_PROXY / HTTP_PROXY环境变量",
206
+ deepseekApiKeyEnv: "凭据引用名",
207
+ deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
208
+ deepseekBaseURL: "接口地址",
209
+ deepseekBaseURLHint: "留空则使用 DeepSeek Anthropic 兼容 API 默认地址。",
210
+ model: "模型",
211
+ modelHint: "搜索请求使用的 Anthropic 格式模型名。",
212
+ apiVersion: "API 版本",
213
+ apiVersionHint: "anthropic-version 请求头值。",
214
+ maxTokens: "最大 token",
215
+ maxTokensHint: "搜索请求生成 token 的上限。",
216
+ maxUses: "最大调用次数",
217
+ maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
136
218
  expand: "展开设置",
137
219
  collapse: "收起设置",
138
220
  save: "保存",
139
221
  saving: "保存中…",
140
222
  discard: "放弃修改",
141
223
  unsaved: "未保存",
224
+ overridden: "已覆盖",
225
+ reset: "恢复默认",
142
226
  saveFailed: "本部署没有接受这些值,已保留供你修改。",
227
+ savedToast: "✓ 设置已保存并立即生效",
228
+ nothingToSave: "没有需要保存的更改",
229
+ invalidToast: "请先修正标红的字段再保存。",
143
230
  readOnly: "本部署的设置为只读。",
144
- invalidNumber: "请填数字;留空表示使用默认值。"
231
+ invalidNumber: "请输入不小于 1 的整数;留空表示使用默认值。",
232
+ quotaTitle: "额度",
233
+ quotaPlan: "计划 {plan}",
234
+ quotaUsed: "已用 {used} / {limit}",
235
+ quotaUsedOnly: "已用 {used}",
236
+ quotaRemaining: "剩余 {remaining}",
237
+ quotaUnlimited: "未公布限额",
238
+ quotaResets: "{time} 重置",
239
+ quotaCycle: "按计费周期重置",
240
+ quotaBurst: "突发 {remaining}/{limit}",
241
+ quotaBraveCapacity: "容量",
242
+ quotaBraveRps: "{limit} 次/秒",
243
+ quotaBraveRpsWindow: "每秒窗口",
244
+ braveDefaultPlaceholder: "留空使用 Brave 默认。",
245
+ quotaEmpty: "下次搜索后显示用量。",
246
+ quotaKeyedHint: "Tavily keyed:本地扣费与定时 /usage 取较高值。",
247
+ quotaBraveUnlimited: "月请求不限额",
248
+ quotaRefresh: "刷新额度",
249
+ quotaRefreshing: "刷新中…",
250
+ quotaError: "额度不可用:{error}",
251
+ quotaWarn: "剩余额度偏低。",
252
+ quotaDanger: "剩余额度告急。",
253
+ quotaPaygo: "按量 {used}/{limit}"
145
254
  };
146
255
 
147
- /** A whole-number field spec: empty draft clears, any other invalid draft blocks save. */
148
- function numberField(field) {
256
+ const MAX_RESULTS_CAP = 20;
257
+ /** A whole-number field spec: empty draft clears; non-digits block save; max is clamped. */
258
+ function numberField(field, bounds) {
149
259
  return {
150
260
  field,
151
261
  format: (value) => typeof value === "number" ? String(value) : "",
152
262
  parse: (text) => {
153
263
  const trimmed = text.trim();
154
264
  if (trimmed === "") return { kind: "clear" };
155
- const parsed = Number(trimmed);
156
- return Number.isFinite(parsed) ? { kind: "set", value: parsed } : void 0;
265
+ if (!/^[0-9]+$/u.test(trimmed)) return void 0;
266
+ let parsed = Number(trimmed);
267
+ if (!Number.isInteger(parsed) || parsed < 1) return void 0;
268
+ if (bounds?.min != null && parsed < bounds.min) parsed = bounds.min;
269
+ if (bounds?.max != null && parsed > bounds.max) parsed = bounds.max;
270
+ return { kind: "set", value: parsed };
157
271
  }
158
272
  };
159
273
  }
@@ -197,7 +311,7 @@ window.__ModuleLoader__.load({
197
311
  this.specs = new Map([
198
312
  choiceField("provider"),
199
313
  choiceField("mode"),
200
- numberField("maxResults"),
314
+ numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }),
201
315
  textField("apiKeyEnv"),
202
316
  textField("baseURL"),
203
317
  choiceField("searchDepth"),
@@ -208,13 +322,29 @@ window.__ModuleLoader__.load({
208
322
  textField("country"),
209
323
  textField("searchLang"),
210
324
  choiceField("freshness"),
211
- textField("proxy")
325
+ textField("proxy"),
326
+ textField("deepseekApiKeyEnv"),
327
+ textField("deepseekBaseURL"),
328
+ textField("model"),
329
+ textField("apiVersion"),
330
+ numberField("maxTokens", { min: 1 }),
331
+ numberField("maxUses", { min: 1 })
212
332
  ].map((spec) => [spec.field, spec]));
213
333
  this.staged = new Map();
214
334
  this.saving = false;
215
335
  this.failed = false;
216
336
  this.credential = { ref: "", configured: false, writable: true };
337
+ this.toastKey = null;
338
+ this.toastTimer = 0;
217
339
  this.listeners = new Set();
340
+ const draft = readDraft();
341
+ if (draft?.staged) {
342
+ for (const [field, edit] of Object.entries(draft.staged)) {
343
+ if (!this.specs.has(field) || edit === null || typeof edit !== "object") continue;
344
+ this.staged.set(field, { text: String(edit.text ?? ""), clear: edit.clear === true });
345
+ }
346
+ if (draft.failed === true) this.failed = true;
347
+ }
218
348
  scope.subscribe(() => {
219
349
  this.publish();
220
350
  });
@@ -236,7 +366,8 @@ window.__ModuleLoader__.load({
236
366
  dirty: plan.length > 0,
237
367
  invalid: plan.some((item) => item.run === void 0),
238
368
  saving: this.saving,
239
- failed: this.failed
369
+ failed: this.failed,
370
+ toastKey: this.toastKey
240
371
  };
241
372
  }
242
373
  field(field) {
@@ -280,19 +411,41 @@ window.__ModuleLoader__.load({
280
411
  }
281
412
  return plan;
282
413
  }
414
+ showToast(key) {
415
+ this.toastKey = key;
416
+ if (this.toastTimer) clearTimeout(this.toastTimer);
417
+ this.toastTimer = setTimeout(() => {
418
+ this.toastKey = null;
419
+ this.toastTimer = 0;
420
+ this.publish();
421
+ }, 2500);
422
+ this.publish();
423
+ }
283
424
  async save() {
284
425
  const plan = this.plan();
285
426
  const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
286
- if (plan.length === 0 || this.saving || writes.length !== plan.length) return;
427
+ if (this.saving) return;
428
+ if (plan.length === 0) {
429
+ this.showToast("nothing");
430
+ return;
431
+ }
432
+ if (writes.length !== plan.length) {
433
+ this.showToast("invalid");
434
+ return;
435
+ }
287
436
  this.saving = true;
288
437
  this.failed = false;
289
438
  this.publish();
290
439
  let landed = true;
291
- for (const write of writes) landed = await write() && landed;
440
+ try {
441
+ for (const write of writes) landed = await write() && landed;
442
+ } catch (_saveFailure) {
443
+ landed = false;
444
+ }
292
445
  if (landed) this.staged.clear();
293
446
  this.saving = false;
294
447
  this.failed = !landed;
295
- this.publish();
448
+ this.showToast(landed ? "saved" : "failed");
296
449
  }
297
450
  actions() {
298
451
  return {
@@ -314,18 +467,26 @@ window.__ModuleLoader__.load({
314
467
  };
315
468
  }
316
469
  async clear(field) {
317
- await this.scope.unset(field);
318
- return !this.stored(field);
470
+ try {
471
+ await this.scope.unset(field);
472
+ return true;
473
+ } catch (_clearFailure) {
474
+ return false;
475
+ }
319
476
  }
320
477
  async store(field, value) {
321
- await this.scope.set(field, value);
322
- return this.userLayer()?.[field] === value;
478
+ try {
479
+ await this.scope.set(field, value);
480
+ return true;
481
+ } catch (_storeFailure) {
482
+ return false;
483
+ }
323
484
  }
324
485
  stage(field, edit) {
325
486
  this.staged.set(field, edit);
326
487
  this.failed = false;
327
488
  this.publish();
328
- if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv") this.readCredential();
489
+ if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv") this.readCredential();
329
490
  }
330
491
  snapshotOf() {
331
492
  return this.scope.getSnapshot();
@@ -344,6 +505,7 @@ window.__ModuleLoader__.load({
344
505
  return user !== void 0 && Object.hasOwn(user, field);
345
506
  }
346
507
  publish() {
508
+ writeDraft(this);
347
509
  for (const listener of this.listeners) listener();
348
510
  }
349
511
  /** Draft-aware engine: staged `provider` wins over the saved section. */
@@ -351,7 +513,7 @@ window.__ModuleLoader__.load({
351
513
  const staged = this.staged.get("provider");
352
514
  if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
353
515
  const declared = this.sectionValue("provider");
354
- return declared === "brave" ? "brave" : "tavily";
516
+ return declared === "brave" || declared === "deepseek-official" ? declared : "tavily";
355
517
  }
356
518
  tavilyRef() {
357
519
  const declared = this.sectionValue("apiKeyEnv");
@@ -361,9 +523,14 @@ window.__ModuleLoader__.load({
361
523
  const declared = this.sectionValue("braveApiKeyEnv");
362
524
  return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_BRAVE_KEY_REF;
363
525
  }
526
+ deepseekRef() {
527
+ const declared = this.sectionValue("deepseekApiKeyEnv");
528
+ return declared !== void 0 && declared.length > 0 ? declared : DEFAULT_DEEPSEEK_KEY_REF;
529
+ }
364
530
  /** The credential reference the currently visible engine uses. */
365
531
  refOf() {
366
- return this.providerOf() === "brave" ? this.braveRef() : this.tavilyRef();
532
+ const provider = this.providerOf();
533
+ return provider === "brave" ? this.braveRef() : provider === "deepseek-official" ? this.deepseekRef() : this.tavilyRef();
367
534
  }
368
535
  async readCredential() {
369
536
  const ref = this.refOf();
@@ -393,7 +560,7 @@ window.__ModuleLoader__.load({
393
560
  this.readCredential();
394
561
  }
395
562
  async writeKey(field, value) {
396
- const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : this.tavilyRef();
563
+ const ref = field === BRAVE_API_KEY_FIELD ? this.braveRef() : field === DEEPSEEK_API_KEY_FIELD ? this.deepseekRef() : this.tavilyRef();
397
564
  try {
398
565
  await this.api.credentials.set({ ref, value });
399
566
  } catch (_credentialWriteFailure) {}
@@ -432,8 +599,16 @@ window.__ModuleLoader__.load({
432
599
  searchLang: this.form.field("searchLang"),
433
600
  freshness: this.form.field("freshness"),
434
601
  proxy: this.form.field("proxy"),
602
+ deepseekApiKey: this.form.field(DEEPSEEK_API_KEY_FIELD),
603
+ deepseekApiKeyEnv: this.form.field("deepseekApiKeyEnv"),
604
+ deepseekBaseURL: this.form.field("deepseekBaseURL"),
605
+ model: this.form.field("model"),
606
+ apiVersion: this.form.field("apiVersion"),
607
+ maxTokens: this.form.field("maxTokens"),
608
+ maxUses: this.form.field("maxUses"),
435
609
  apiKeyConfigured: this.form.credential.configured,
436
- apiKeyWritable: this.form.credential.writable
610
+ apiKeyWritable: this.form.credential.writable,
611
+ toastKey: this.form.toastKey
437
612
  };
438
613
  }
439
614
  inject() {
@@ -444,73 +619,439 @@ window.__ModuleLoader__.load({
444
619
  }
445
620
  };
446
621
 
447
- /** Row style: one labelled field with label, hint, and staged input. */
448
- const fieldRow = (label, hint, children) => react.createElement("div", { style: { display: "flex", flexDirection: "column", gap: "4px", padding: "12px 0", borderTop: "1px solid var(--dsw-alias-border-l2)" } },
449
- react.createElement("label", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "13px", fontWeight: 500, lineHeight: "1.5" } }, label),
622
+ /** Row style: one labelled field with optional override badge and restore-default. */
623
+ const fieldRow = (label, hint, children, meta) => react.createElement("div", { className: "dshws_field" },
624
+ react.createElement("div", { className: "dshws_field_head" },
625
+ react.createElement("label", { className: "dshws_field_label" }, label),
626
+ meta?.overridden ? react.createElement("span", { className: "dshws_field_badges" },
627
+ react.createElement("span", { className: "dshws_badge" }, meta.t("overridden")),
628
+ react.createElement("button", { type: "button", className: "dshws_reset", disabled: meta.disabled === true, onClick: meta.onReset }, meta.t("reset"))) : null),
450
629
  children,
451
- hint ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: 0, fontSize: "12px", lineHeight: "1.5" } }, hint) : null);
452
- 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 });
453
- 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 });
454
- 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 });
630
+ meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t("invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
631
+ const pair = (left, right) => {
632
+ if (!left && !right) return null;
633
+ if (!left) return right;
634
+ if (!right) return left;
635
+ return react.createElement("div", { className: "dshws_pair" }, left, right);
636
+ };
637
+ const controlStyle = { 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", width: "100%", boxSizing: "border-box" };
638
+ const textInput = (props) => react.createElement("input", { style: controlStyle, type: "text", ...props });
639
+ const numberInput = (props) => react.createElement("input", {
640
+ style: controlStyle,
641
+ type: "text",
642
+ inputMode: "numeric",
643
+ pattern: "[0-9]*",
644
+ autoComplete: "off",
645
+ ...props,
646
+ onChange: (event) => {
647
+ props.onChange?.({ target: { value: String(event.target.value ?? "").replace(/\D/g, "") } });
648
+ }
649
+ });
650
+ const secretInput = (props) => react.createElement("input", { style: controlStyle, type: "password", autoComplete: "off", ...props });
651
+ const selectInput = (props) => react.createElement("select", { style: controlStyle, ...props });
455
652
  const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
456
653
 
457
- /** Render one plugin card: engine switch plus the engine-specific form. */
458
- function SearchCard(props) {
459
- const { t } = props;
460
- const state = props.usePluginSearchCard((snapshot) => snapshot);
461
- const [open, setOpen] = react.useState(false);
654
+ const CSS_ID = "dsh-web-search-plugin/styles.css#maxresults";
655
+ const GLOBE_MASK = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14zM2.1 8.75h2.55c.12 1.58.48 3 1 4.12A6.02 6.02 0 0 1 2.1 8.75zm8.25 4.12c.52-1.12.88-2.54 1-4.12H13.9a6.02 6.02 0 0 1-3.55 4.12zM13.9 7.25h-2.55c-.12-1.58-.48-3-1-4.12A6.02 6.02 0 0 1 13.9 7.25zM5.65 3.13c-.52 1.12-.88 2.54-1 4.12H2.1a6.02 6.02 0 0 1 3.55-4.12zM8 2.2c.82 1.12 1.36 2.82 1.5 4.95H6.5C6.64 5.02 7.18 3.32 8 2.2zm0 11.6c-.82-1.12-1.36-2.82-1.5-4.95h3C9.36 10.98 8.82 12.68 8 13.8z'/%3E%3C/svg%3E\") 50%/contain no-repeat";
656
+ if (typeof document !== "undefined") {
657
+ for (const old of document.querySelectorAll('style[data-plugin="dsh-web-search-plugin"]')) old.remove();
658
+ const tag = document.createElement("style");
659
+ tag.dataset.plugin = "dsh-web-search-plugin";
660
+ tag.dataset.pluginCss = CSS_ID;
661
+ tag.textContent = [
662
+ "@keyframes dshws-toast-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}",
663
+ ".dshws_quota{border:1px solid var(--dsw-alias-border-l3,rgba(128,128,128,0.12));background:var(--dsw-alias-bg-layer-2,rgba(128,128,128,0.04));border-radius:8px;padding:10px 12px;display:flex;flex-direction:column;gap:6px;margin:4px 0 8px}",
664
+ ".dshws_quota_head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
665
+ ".dshws_quota_title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
666
+ ".dshws_quota_pct{font-variant-numeric:tabular-nums;font-size:12px;font-weight:600;color:var(--dsw-alias-label-primary);background:transparent;border:none;padding:0;font:inherit;font-weight:600;cursor:pointer}",
667
+ ".dshws_quota_pct:disabled{cursor:wait;opacity:.7}",
668
+ ".dshws_quota_track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,0.14));overflow:hidden}",
669
+ ".dshws_quota_fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#10b981);transition:width .2s ease}",
670
+ ".dshws_quota_fill_warning{background:var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warning-primary,#f59e0b))}",
671
+ ".dshws_quota_fill_danger{background:var(--dsw-alias-state-error-primary,#ef4444)}",
672
+ ".dshws_quota_meta{display:flex;justify-content:space-between;gap:8px;flex-wrap:wrap;font-size:11px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
673
+ ".dshws_quota_hint{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.45;margin:0}",
674
+ ".dshws_quota_alert{font-size:11.5px;margin:0;line-height:1.45}",
675
+ ".dshws_quota_alert_warning{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
676
+ ".dshws_quota_alert_danger{color:var(--dsw-alias-state-error-primary,#ef4444)}",
677
+ ".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5;display:inline-flex;align-items:center;justify-content:center;transition:filter .15s ease,opacity .15s ease}",
678
+ ".dshws_btn:disabled{cursor:not-allowed;opacity:.45}",
679
+ ".dshws_btn_primary{border:1px solid transparent;color:var(--dsw-alias-label-on-brand,#fff);background:var(--dsw-alias-bg-brand-solid,var(--dsw-alias-brand-primary,#3b82f6));font-weight:600}",
680
+ ".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.08)}",
681
+ ".dshws_btn_outline{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent}",
682
+ ".dshws_toast{position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--dsw-alias-state-success-primary,#10b981);color:#fff;padding:8px 18px;border-radius:999px;box-shadow:var(--dsw-shadow-lv3,0 8px 24px rgba(0,0,0,.3));font-size:12.5px;font-weight:500;z-index:100000;animation:dshws-toast-in .2s ease-out}",
683
+ ".dshws_toast_error{background:var(--dsw-alias-state-error-primary,#ef4444)}",
684
+ ".dshws_toast_info{background:var(--dsw-alias-label-secondary,#6b7280)}",
685
+ ".dshws_card_header{display:flex;align-items:flex-start;gap:12px;padding:6px 0 2px}",
686
+ ".dshws_card_headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}",
687
+ ".dshws_card_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
688
+ ".dshws_card_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
689
+ ".dshws_pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
690
+ ".dshws_form{display:flex;flex-direction:column;gap:0}",
691
+ ".dshws_field{display:flex;flex-direction:column;gap:4px;padding:10px 0;border-top:1px solid var(--dsw-alias-border-l2);min-width:0}",
692
+ ".dshws_field_head{display:flex;align-items:center;gap:8px}",
693
+ ".dshws_field_label{min-width:0;flex:1;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
694
+ ".dshws_field_badges{display:inline-flex;align-items:center;gap:8px;flex:none}",
695
+ ".dshws_field_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.45}",
696
+ ".dshws_field_error{color:var(--dsw-alias-state-error-primary,#ef4444)!important;margin:0;font-size:12px;line-height:1.45;font-weight:500}",
697
+ ".dshws_pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);column-gap:12px;border-top:1px solid var(--dsw-alias-border-l2)}",
698
+ ".dshws_pair>.dshws_field{border-top:none;padding:10px 0}",
699
+ ".dshws_badge{white-space:nowrap;background:var(--dsw-alias-bg-module-platform,rgba(128,128,128,0.12));color:var(--dsw-alias-label-secondary);border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}",
700
+ ".dshws_reset{appearance:none;font:inherit;color:var(--dsw-alias-label-secondary);cursor:pointer;background:transparent;border:none;padding:0;font-size:12px;line-height:1.5}",
701
+ ".dshws_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
702
+ ".dshws_reset:disabled{cursor:default;opacity:.45}",
703
+ ".dshws_input_invalid{border-color:var(--dsw-alias-state-error-primary,#ef4444)!important}",
704
+ "[data-dshws-nav]>[class*='_navIcon']{display:none}",
705
+ "[data-dshws-nav]:before{content:'';background:currentColor;flex:none;width:16px;height:16px;-webkit-mask:" + GLOBE_MASK + ";mask:" + GLOBE_MASK + "}"
706
+ ].join("\n");
707
+ document.head.appendChild(tag);
708
+ }
709
+
710
+ function interpolate(template, vars) {
711
+ return String(template ?? "").replace(/\{(\w+)\}/g, (_, key) => vars[key] == null ? "" : String(vars[key]));
712
+ }
713
+ function tFill(t, key, vars) {
714
+ return interpolate(t(key), vars);
715
+ }
716
+ function formatResetTime(ms) {
717
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
718
+ const date = new Date(ms);
719
+ return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
720
+ }
721
+ function usedPercent(used, limit) {
722
+ if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(used)) return 0;
723
+ return Math.max(0, Math.min(100, (100 * used) / limit));
724
+ }
725
+ function remainingLevel(used, limit) {
726
+ if (!Number.isFinite(limit) || limit <= 0) return "unknown";
727
+ const remainingPct = Math.max(0, (100 * (limit - used)) / limit);
728
+ if (remainingPct <= 10) return "danger";
729
+ if (remainingPct <= 20) return "warning";
730
+ return "success";
731
+ }
732
+
733
+ const USAGE_POLL_MS = 15000;
734
+ function createUsageClientStore() {
735
+ let snap = { status: "idle", payload: null, refreshing: false };
736
+ const listeners = new Set();
737
+ const publish = () => {
738
+ for (const listener of listeners) listener();
739
+ };
740
+ const load = async (force) => {
741
+ if (force) {
742
+ snap = { ...snap, refreshing: true };
743
+ publish();
744
+ }
745
+ try {
746
+ const response = await fetch(force ? "/dsh-web-search/usage?force=1" : "/dsh-web-search/usage", { cache: "no-store" });
747
+ const payload = await response.json();
748
+ snap = { status: "ok", payload, refreshing: false };
749
+ } catch (error) {
750
+ snap = { status: "error", payload: snap.payload, refreshing: false, error: String(error?.message ?? error) };
751
+ }
752
+ publish();
753
+ return snap;
754
+ };
755
+ return {
756
+ subscribe(listener) {
757
+ listeners.add(listener);
758
+ return () => listeners.delete(listener);
759
+ },
760
+ getSnapshot() {
761
+ return snap;
762
+ },
763
+ refresh: () => load(true),
764
+ start() {
765
+ void load(true);
766
+ const timer = setInterval(() => {
767
+ void load(false);
768
+ }, USAGE_POLL_MS);
769
+ return () => clearInterval(timer);
770
+ }
771
+ };
772
+ }
773
+ const usageStore = createUsageClientStore();
774
+
775
+ function QuotaBar(props) {
776
+ const { name, used, limit, remainingText, resetText, level, onRefresh, refreshing, t, extra } = props;
777
+ const pct = usedPercent(used, limit);
778
+ const hasLimit = Number.isFinite(limit) && limit > 0;
779
+ const fillClass = "dshws_quota_fill" + (level === "danger" ? " dshws_quota_fill_danger" : level === "warning" ? " dshws_quota_fill_warning" : "");
780
+ return react.createElement("div", { className: "dshws_quota" },
781
+ react.createElement("div", { className: "dshws_quota_head" },
782
+ react.createElement("span", { className: "dshws_quota_title" }, name),
783
+ react.createElement("button", {
784
+ type: "button",
785
+ className: "dshws_quota_pct",
786
+ disabled: refreshing,
787
+ title: refreshing ? t("quotaRefreshing") : t("quotaRefresh"),
788
+ onClick: onRefresh
789
+ }, hasLimit ? Math.round(pct) + "%" : (Number.isFinite(used) && used > 0 ? String(used) : "—"))),
790
+ react.createElement("div", { className: "dshws_quota_track" },
791
+ react.createElement("div", { className: fillClass, style: { width: (hasLimit ? pct : 0) + "%" } })),
792
+ react.createElement("div", { className: "dshws_quota_meta" },
793
+ react.createElement("span", null, remainingText),
794
+ react.createElement("span", null, resetText)),
795
+ level === "warning" || level === "danger"
796
+ ? react.createElement("p", { className: "dshws_quota_alert dshws_quota_alert_" + level }, t(level === "danger" ? "quotaDanger" : "quotaWarn"))
797
+ : null,
798
+ extra);
799
+ }
800
+
801
+ function QuotaCard(props) {
802
+ const { t, provider, keyed } = props;
803
+ const usage = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
804
+ const showTavily = provider === "tavily" && keyed;
805
+ const showBrave = provider === "brave";
806
+ if (!showTavily && !showBrave) return null;
807
+ const payload = usage.payload;
808
+ const refreshing = usage.refreshing === true;
809
+ const onRefresh = (event) => {
810
+ event.preventDefault();
811
+ void usageStore.refresh();
812
+ };
813
+ if (showTavily) {
814
+ const info = payload?.tavily;
815
+ const error = info?.error || (usage.status === "error" ? usage.error : null);
816
+ if (!info || info.fetchedAt === 0 && info.used === 0 && !error) {
817
+ return react.createElement("div", { className: "dshws_quota" },
818
+ react.createElement("div", { className: "dshws_quota_head" },
819
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
820
+ react.createElement("button", { type: "button", className: "dshws_quota_pct", disabled: refreshing, onClick: onRefresh }, refreshing ? t("quotaRefreshing") : t("quotaRefresh"))),
821
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")),
822
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaKeyedHint")));
823
+ }
824
+ const used = Number(info.used) || 0;
825
+ const limit = info.limit;
826
+ const remaining = info.remaining;
827
+ const level = remainingLevel(used, limit);
828
+ const name = info.plan ? tFill(t, "quotaPlan", { plan: info.plan }) : t("quotaTitle");
829
+ const remainingText = Number.isFinite(limit) && limit > 0
830
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
831
+ : used > 0
832
+ ? tFill(t, "quotaUsedOnly", { used }) + " · " + t("quotaUnlimited")
833
+ : t("quotaUnlimited");
834
+ return react.createElement(QuotaBar, {
835
+ name,
836
+ used,
837
+ limit,
838
+ remainingText,
839
+ resetText: t("quotaCycle"),
840
+ level,
841
+ onRefresh,
842
+ refreshing,
843
+ t,
844
+ extra: [
845
+ info.paygoUsed > 0 ? react.createElement("p", { className: "dshws_quota_hint", key: "paygo" }, tFill(t, "quotaPaygo", { used: info.paygoUsed, limit: info.paygoLimit ?? "—" })) : null,
846
+ error ? react.createElement("p", { className: "dshws_quota_hint", key: "err" }, tFill(t, "quotaError", { error })) : react.createElement("p", { className: "dshws_quota_hint", key: "hint" }, t("quotaKeyedHint"))
847
+ ]
848
+ });
849
+ }
850
+ const brave = payload?.brave;
851
+ const monthly = brave?.monthly;
852
+ const burst = brave?.burst;
853
+ if (!monthly && !burst) {
854
+ return react.createElement("div", { className: "dshws_quota" },
855
+ react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
856
+ react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")));
857
+ }
858
+ const monthlyCapped = Number.isFinite(monthly?.limit) && monthly.limit > 0;
859
+ const primary = monthlyCapped ? monthly : (burst ?? monthly);
860
+ const used = Number(primary.used) || 0;
861
+ const limit = primary.limit;
862
+ const remaining = primary.remaining;
863
+ const level = remainingLevel(used, limit);
864
+ return react.createElement(QuotaBar, {
865
+ name: monthlyCapped ? t("quotaTitle") : t("quotaBraveCapacity"),
866
+ used,
867
+ limit,
868
+ remainingText: monthlyCapped
869
+ ? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
870
+ : tFill(t, "quotaBraveRps", { limit: limit ?? 0 }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? 0 }),
871
+ resetText: monthlyCapped
872
+ ? tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) })
873
+ : t("quotaBraveUnlimited") + (monthly?.resetAt ? " · " + tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) }) : ""),
874
+ level,
875
+ onRefresh,
876
+ refreshing,
877
+ t,
878
+ extra: monthlyCapped && burst
879
+ ? react.createElement("p", { className: "dshws_quota_hint", key: "burst" }, tFill(t, "quotaBurst", { remaining: burst.remaining, limit: burst.limit }))
880
+ : null
881
+ });
882
+ }
883
+
884
+ function toastCopy(t, key) {
885
+ if (key === "saved") return t("savedToast");
886
+ if (key === "nothing") return t("nothingToSave");
887
+ if (key === "invalid") return t("invalidToast");
888
+ if (key === "failed") return t("saveFailed");
889
+ return "";
890
+ }
891
+
892
+ /** Render the provider selector plus the backend-specific staged form. */
893
+ function WebSearchCard(props) {
894
+ const { t, state, actions } = props;
462
895
  if (!state.available) return null;
463
896
  const disabled = !state.writable;
464
- const blocked = !state.dirty || state.invalid || state.saving;
465
- const brave = state.provider.text === "brave";
897
+ const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
898
+ const brave = provider === "brave";
899
+ const deepseek = provider === "deepseek-official";
466
900
  const keyed = state.mode.text === "keyed";
467
- return react.createElement("li", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", listStyle: "none" } },
468
- 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" } },
469
- react.createElement("span", { style: { flexDirection: "column", flex: 1, gap: "4px", minWidth: 0, display: "flex" } },
470
- react.createElement("span", { style: { color: "var(--dsw-alias-label-primary)", fontSize: "15px", fontWeight: 600, lineHeight: "1.4" } }, t("nav")),
471
- react.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", fontSize: "13px", lineHeight: "1.5" } }, t("description"))),
472
- 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,
473
- react.createElement("span", { style: { color: "var(--dsw-alias-label-tertiary)", flex: "none" } }, open ? "▾" : "▸")),
474
- open ? react.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2)", margin: "0 16px", paddingBottom: "8px" } },
475
- !state.writable ? react.createElement("p", { style: { color: "var(--dsw-alias-label-tertiary)", margin: "12px 0 0", fontSize: "12px", lineHeight: "1.5" } }, t("readOnly")) : null,
476
- fieldRow(t("provider"), null, selectInput({ id: "plugin-config-web-search-provider", value: brave ? "brave" : "tavily", disabled, onChange: (e) => props.edit("provider", e.target.value), children: [
901
+ const toastText = toastCopy(t, state.toastKey);
902
+ const toastTone = state.toastKey === "failed" || state.toastKey === "invalid" ? "error" : state.toastKey === "nothing" ? "info" : "success";
903
+ const row = (field, label, hint, children) => fieldRow(label, hint, children, {
904
+ t,
905
+ overridden: state[field]?.overridden === true,
906
+ invalid: state[field]?.invalid === true,
907
+ disabled,
908
+ onReset: () => actions.resetField(field)
909
+ });
910
+ const maxResultsWrite = numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }).parse(String(state.maxResults.text ?? "").trim());
911
+ const maxResultsValue = maxResultsWrite?.kind === "set" ? String(maxResultsWrite.value) : "8";
912
+ const maxResultsRow = row("maxResults", t("maxResults"), null, selectInput({
913
+ id: "ws-max",
914
+ value: maxResultsValue,
915
+ disabled,
916
+ className: state.maxResults.invalid ? "dshws_input_invalid" : void 0,
917
+ onChange: (e) => actions.edit("maxResults", e.target.value),
918
+ children: Array.from({ length: MAX_RESULTS_CAP }, (_, index) => {
919
+ const n = String(index + 1);
920
+ return react.createElement("option", { value: n, key: n }, n);
921
+ })
922
+ }));
923
+ return react.createElement("div", { className: "dshws_form" },
924
+ react.createElement("div", { className: "dshws_card_header" },
925
+ react.createElement("div", { className: "dshws_card_headText" },
926
+ react.createElement("span", { className: "dshws_card_name" }, t("nav"))),
927
+ state.dirty || state.failed ? react.createElement("span", { className: "dshws_pending" }, t("unsaved")) : null),
928
+ !state.writable ? react.createElement("p", { className: "dshws_field_hint", style: { padding: "8px 0 0" } }, t("readOnly")) : null,
929
+ pair(
930
+ row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
477
931
  react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
478
- react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave"))
932
+ react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
933
+ react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek"))
479
934
  ] })),
480
- brave ? null : 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: [
935
+ deepseek || brave ? null : row("mode", t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
481
936
  react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
482
937
  react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
483
- ] })),
484
- (!brave && keyed) || brave ? fieldRow(t("apiKey"), t("apiKeyHint"), secretInput({ id: "plugin-config-web-search-key", value: brave ? state.braveApiKey.text : state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => props.edit(brave ? BRAVE_API_KEY_FIELD : TAVILY_API_KEY_FIELD, e.target.value) })) : null,
485
- (!brave && keyed) || brave ? fieldRow(brave ? t("braveApiKeyEnv") : t("apiKeyEnv"), brave ? t("braveApiKeyEnvHint") : t("apiKeyEnvHint"), textInput({ id: "plugin-config-web-search-keyref", value: brave ? state.braveApiKeyEnv.text : state.apiKeyEnv.text, disabled, onChange: (e) => props.edit(brave ? "braveApiKeyEnv" : "apiKeyEnv", e.target.value), onReset: () => props.resetField(brave ? "braveApiKeyEnv" : "apiKeyEnv") })) : null,
486
- brave ? fieldRow(t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "plugin-config-web-search-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => props.edit("braveBaseURL", e.target.value), onReset: () => props.resetField("braveBaseURL") })) : 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") })),
487
- brave ? null : 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: [
938
+ ] }))
939
+ ),
940
+ react.createElement(QuotaCard, { t, provider, keyed }),
941
+ deepseek ? pair(
942
+ 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) })),
943
+ row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
944
+ ) : null,
945
+ brave ? pair(
946
+ 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) })),
947
+ row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
948
+ ) : null,
949
+ (!brave && !deepseek && keyed) ? pair(
950
+ 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) })),
951
+ row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
952
+ ) : null,
953
+ 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,
954
+ brave ? row("braveBaseURL", t("braveBaseURL"), t("braveBaseURLHint"), textInput({ id: "ws-brave-base", value: state.braveBaseURL.text, disabled, onChange: (e) => actions.edit("braveBaseURL", e.target.value) })) : null,
955
+ (!brave && !deepseek) ? row("baseURL", t("baseURL"), t("baseURLHint"), textInput({ id: "ws-tavily-base", value: state.baseURL.text, disabled, onChange: (e) => actions.edit("baseURL", e.target.value) })) : null,
956
+ deepseek ? pair(
957
+ row("model", t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value) })),
958
+ row("apiVersion", t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value) }))
959
+ ) : null,
960
+ deepseek ? pair(
961
+ row("maxTokens", t("maxTokens"), t("maxTokensHint"), numberInput({ id: "ws-maxtokens", value: state.maxTokens.text, disabled, onChange: (e) => actions.edit("maxTokens", e.target.value), className: state.maxTokens.invalid ? "dshws_input_invalid" : void 0 })),
962
+ row("maxUses", t("maxUses"), t("maxUsesHint"), numberInput({ id: "ws-maxuses", value: state.maxUses.text, disabled, onChange: (e) => actions.edit("maxUses", e.target.value), className: state.maxUses.invalid ? "dshws_input_invalid" : void 0 }))
963
+ ) : null,
964
+ (!brave && !deepseek) ? pair(
965
+ row("searchDepth", t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
488
966
  react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
489
967
  react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
490
968
  ] })),
491
- brave ? null : 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: [
969
+ row("topic", t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
492
970
  react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
493
971
  react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
494
- ] })),
495
- brave ? null : 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" : "") })),
496
- brave ? fieldRow(t("country"), t("countryHint"), textInput({ id: "plugin-config-web-search-country", value: state.country.text, disabled, onChange: (e) => props.edit("country", e.target.value), onReset: () => props.resetField("country") })) : null,
497
- brave ? fieldRow(t("searchLang"), t("searchLangHint"), textInput({ id: "plugin-config-web-search-lang", value: state.searchLang.text, disabled, onChange: (e) => props.edit("searchLang", e.target.value), onReset: () => props.resetField("searchLang") })) : null,
498
- brave ? fieldRow(t("freshness"), null, selectInput({ id: "plugin-config-web-search-freshness", value: state.freshness.text, disabled, onChange: (e) => props.edit("freshness", e.target.value), children: [
972
+ ] }))
973
+ ) : null,
974
+ (!brave && !deepseek) ? pair(
975
+ row("includeAnswer", t("includeAnswer"), t("includeAnswerHint"), checkboxInput({ id: "ws-answer", checked: state.includeAnswer.text === "true", disabled, onChange: (e) => actions.edit("includeAnswer", e.target.checked ? "true" : "") })),
976
+ maxResultsRow
977
+ ) : null,
978
+ brave ? pair(
979
+ row("country", t("country"), t("countryHint"), textInput({ id: "ws-country", value: state.country.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("country", e.target.value) })),
980
+ row("searchLang", t("searchLang"), t("searchLangHint"), textInput({ id: "ws-lang", value: state.searchLang.text, disabled, placeholder: t("braveDefaultPlaceholder"), onChange: (e) => actions.edit("searchLang", e.target.value) }))
981
+ ) : null,
982
+ brave ? pair(
983
+ row("freshness", t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
499
984
  react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
500
985
  react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
501
986
  react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
502
987
  react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
503
988
  react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
504
- ] })) : null,
505
- brave ? fieldRow(t("proxy"), t("proxyHint"), textInput({ id: "plugin-config-web-search-proxy", value: state.proxy.text, disabled, onChange: (e) => props.edit("proxy", e.target.value), onReset: () => props.resetField("proxy") })) : null,
506
- 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") })),
507
- 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" } },
508
- 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,
509
- 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")),
510
- 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);
989
+ ] })),
990
+ maxResultsRow
991
+ ) : null,
992
+ 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,
993
+ deepseek ? maxResultsRow : null,
994
+ 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" } },
995
+ react.createElement("button", {
996
+ type: "button",
997
+ className: "dshws_btn dshws_btn_outline",
998
+ disabled: (!state.dirty && !state.failed) || state.saving,
999
+ onClick: (event) => {
1000
+ event.preventDefault();
1001
+ actions.discard();
1002
+ }
1003
+ }, t("discard")),
1004
+ react.createElement("button", {
1005
+ type: "button",
1006
+ className: "dshws_btn dshws_btn_primary",
1007
+ disabled: state.saving || !state.writable,
1008
+ onClick: (event) => {
1009
+ event.preventDefault();
1010
+ void actions.save();
1011
+ }
1012
+ }, t(state.saving ? "saving" : "save"))),
1013
+ toastText ? react.createElement("div", { className: "dshws_toast" + (toastTone === "error" ? " dshws_toast_error" : toastTone === "info" ? " dshws_toast_info" : ""), role: "status" }, toastText) : null);
511
1014
  }
512
1015
 
513
- /** Mount the card into the plugin configuration surface. */
1016
+ /** Section component: the "Web search" page body, driven by useSyncExternalStore. */
1017
+ function makeSection(ctx, controller) {
1018
+ return function WebSearchSection() {
1019
+ const state = react.useSyncExternalStore(controller.store.subscribe, controller.store.getSnapshot);
1020
+ const t = ctx.locale.bind(NS);
1021
+ if (!state.available) return react.createElement("p", { style: { padding: "16px" } }, t("readOnly"));
1022
+ return react.createElement("div", { style: { padding: "16px" } },
1023
+ react.createElement("div", { style: { border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-3)", borderRadius: "12px", padding: "4px 16px" } },
1024
+ react.createElement(WebSearchCard, { t: t, state: state, actions: controller.form.actions() })));
1025
+ };
1026
+ }
1027
+
1028
+ function syncSettingsNavIcon() {
1029
+ if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return;
1030
+ const lists = document.querySelectorAll("[class*='_navList']");
1031
+ for (const list of lists) {
1032
+ const cells = list.querySelectorAll(":scope > [class*='_navCell']");
1033
+ for (const cell of cells) {
1034
+ const text = String(cell.textContent ?? "").replace(/\s+/g, " ").trim();
1035
+ if (text === "网页搜索" || text === "Web search") cell.setAttribute("data-dshws-nav", "web-search");
1036
+ else cell.removeAttribute("data-dshws-nav");
1037
+ }
1038
+ }
1039
+ }
1040
+ function startSettingsNavIconSync() {
1041
+ if (typeof document === "undefined") return () => {};
1042
+ const run = () => {
1043
+ try { syncSettingsNavIcon(); } catch { /* settings sheet not mounted yet */ }
1044
+ };
1045
+ run();
1046
+ if (typeof MutationObserver !== "function") return () => {};
1047
+ const root = document.body || document.documentElement;
1048
+ if (!root) return () => {};
1049
+ const observer = new MutationObserver(run);
1050
+ observer.observe(root, { childList: true, subtree: true, characterData: true });
1051
+ return () => observer.disconnect();
1052
+ }
1053
+
1054
+ /** Mount the top-level "Web search" settings section. */
514
1055
  function apply(ctx) {
515
1056
  const { api } = ctx.get("connection");
516
1057
  const t = ctx.locale.bind(NS);
@@ -519,14 +1060,15 @@ window.__ModuleLoader__.load({
519
1060
  ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
520
1061
  controller.form.refreshCredential(ref);
521
1062
  }), "dsh-web-search-plugin: credential invalidations");
522
- ctx.slots.inject("settings.plugin.item", function* () {
523
- yield ctx.slots.register({
524
- name: "settings.plugin.item",
525
- key: "dsh-web-search-plugin",
526
- locale: NS,
527
- inject: () => controller.inject()
528
- }, SearchCard);
529
- });
1063
+ ctx.effect(() => usageStore.start(), "dsh-web-search-plugin: usage poll");
1064
+ ctx.effect(() => startSettingsNavIconSync(), "dsh-web-search-plugin: settings nav icon");
1065
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
1066
+ name: "settings.section",
1067
+ id: "web-search",
1068
+ order: 10,
1069
+ label: () => t("nav"),
1070
+ locale: NS
1071
+ }, makeSection(ctx, controller)));
530
1072
  }
531
1073
 
532
1074
  const inject = ["slots", "locale", "connection", "remote", "settingsScope"];