dsh-web-search-plugin 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +118 -47
- package/cordis.patch.yml +1 -1
- package/lib/brave.js +1 -0
- package/lib/client.js +657 -96
- package/lib/index.js +195 -12
- package/lib/providers.js +212 -0
- package/lib/rest.js +325 -0
- package/lib/shared.js +1 -1
- package/lib/tavily.js +10 -2
- package/lib/usage.js +352 -0
- package/package.json +3 -3
package/lib/client.js
CHANGED
|
@@ -25,6 +25,25 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
|
|
26
26
|
/** Settings namespace of this plugin. */
|
|
27
27
|
const NS = "dsh-web-search-plugin";
|
|
28
|
+
/** Built-in REST providers (mirrors `lib/providers.js`; browser bundle is self-contained). */
|
|
29
|
+
const BUILTIN_REST_PROVIDERS = [
|
|
30
|
+
{ id: "serper", name: "Serper", officialUrl: "https://serper.dev/", keyRequired: true, keyField: "serperApiKey", keyEnvField: "serperApiKeyEnv", baseURLField: "serperBaseURL" },
|
|
31
|
+
{ id: "serpapi", name: "SerpApi", officialUrl: "https://serpapi.com/", keyRequired: true, keyField: "serpapiApiKey", keyEnvField: "serpapiApiKeyEnv", baseURLField: "serpapiBaseURL" },
|
|
32
|
+
{ id: "exa", name: "Exa", officialUrl: "https://dashboard.exa.ai/", keyRequired: true, keyField: "exaApiKey", keyEnvField: "exaApiKeyEnv", baseURLField: "exaBaseURL" },
|
|
33
|
+
{ id: "searxng", name: "SearXNG", officialUrl: "https://docs.searxng.org/", keyRequired: false, keyField: "", keyEnvField: "", baseURLField: "searxngBaseURL" },
|
|
34
|
+
{ id: "scavio", name: "Scavio", officialUrl: "https://scavio.dev/", keyRequired: true, keyField: "scavioApiKey", keyEnvField: "scavioApiKeyEnv", baseURLField: "scavioBaseURL" },
|
|
35
|
+
{ id: "firecrawl", name: "Firecrawl", officialUrl: "https://firecrawl.dev/", keyRequired: true, keyField: "firecrawlApiKey", keyEnvField: "firecrawlApiKeyEnv", baseURLField: "firecrawlBaseURL" }
|
|
36
|
+
];
|
|
37
|
+
/** Find a built-in REST provider by id. */
|
|
38
|
+
function builtinRest(id) {
|
|
39
|
+
for (const entry of BUILTIN_REST_PROVIDERS) if (entry.id === id) return entry;
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
/** Find a built-in REST provider by its literal key field name. */
|
|
43
|
+
function builtinRestByKeyField(field) {
|
|
44
|
+
for (const entry of BUILTIN_REST_PROVIDERS) if (entry.keyField === field) return entry;
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
28
47
|
/** Credential reference Tavily keyed mode resolves when the section names none. */
|
|
29
48
|
const DEFAULT_TAVILY_KEY_REF = "TAVILY_API_KEY";
|
|
30
49
|
/** Credential reference Brave resolves when the section names none. */
|
|
@@ -37,12 +56,42 @@ window.__ModuleLoader__.load({
|
|
|
37
56
|
const BRAVE_API_KEY_FIELD = "braveApiKey";
|
|
38
57
|
/** Form field the DeepSeek write-only credential control stages under. */
|
|
39
58
|
const DEEPSEEK_API_KEY_FIELD = "deepseekApiKey";
|
|
40
|
-
|
|
59
|
+
/** Form fields carrying literal API keys (write-only, staged under the credentials domain). */
|
|
60
|
+
const REST_KEY_FIELDS = BUILTIN_REST_PROVIDERS.map((entry) => entry.keyField).filter((field) => field !== "");
|
|
61
|
+
const SECRET_FIELDS = new Set([TAVILY_API_KEY_FIELD, BRAVE_API_KEY_FIELD, DEEPSEEK_API_KEY_FIELD, ...REST_KEY_FIELDS]);
|
|
62
|
+
const DRAFT_KEY = "dsh-web-search-plugin:settings-draft";
|
|
63
|
+
|
|
64
|
+
function readDraft() {
|
|
65
|
+
try {
|
|
66
|
+
const raw = sessionStorage.getItem(DRAFT_KEY);
|
|
67
|
+
if (!raw) return null;
|
|
68
|
+
const parsed = JSON.parse(raw);
|
|
69
|
+
if (parsed === null || typeof parsed !== "object" || parsed.staged === null || typeof parsed.staged !== "object") return null;
|
|
70
|
+
return parsed;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
function writeDraft(form) {
|
|
76
|
+
try {
|
|
77
|
+
const staged = {};
|
|
78
|
+
for (const [field, edit] of form.staged) {
|
|
79
|
+
if (SECRET_FIELDS.has(field)) continue;
|
|
80
|
+
staged[field] = { text: String(edit?.text ?? ""), clear: edit?.clear === true };
|
|
81
|
+
}
|
|
82
|
+
if (Object.keys(staged).length === 0 && form.failed !== true) {
|
|
83
|
+
sessionStorage.removeItem(DRAFT_KEY);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
sessionStorage.setItem(DRAFT_KEY, JSON.stringify({ staged, failed: form.failed === true }));
|
|
87
|
+
} catch {
|
|
88
|
+
/* private mode / quota */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
41
91
|
|
|
42
92
|
/** Locale bundles for the card. */
|
|
43
93
|
const en = {
|
|
44
94
|
nav: "Web search",
|
|
45
|
-
description: "Tavily (keyless by default) or Brave Search. Switching engines here is enough — web.searchProvider stays dsh-web-search.",
|
|
46
95
|
provider: "Search engine",
|
|
47
96
|
providerTavily: "Tavily",
|
|
48
97
|
providerBrave: "Brave Search",
|
|
@@ -53,7 +102,8 @@ window.__ModuleLoader__.load({
|
|
|
53
102
|
apiKey: "API key",
|
|
54
103
|
apiKeyHint: "Stored outside the settings file. Leave blank to keep the current key.",
|
|
55
104
|
apiKeySet: "A key is configured.",
|
|
56
|
-
apiKeyUnset: "No key
|
|
105
|
+
apiKeyUnset: "No key set",
|
|
106
|
+
apiKeyUnavailableHint: "Search is unavailable until a key is configured.",
|
|
57
107
|
apiKeyEnv: "Credential reference",
|
|
58
108
|
apiKeyEnvHint: "Name of the credential/env var resolved for keyed Tavily search.",
|
|
59
109
|
braveApiKeyEnv: "Credential reference",
|
|
@@ -63,7 +113,6 @@ window.__ModuleLoader__.load({
|
|
|
63
113
|
braveBaseURL: "Endpoint",
|
|
64
114
|
braveBaseURLHint: "Leave blank to use the Brave Search API default.",
|
|
65
115
|
maxResults: "Max results",
|
|
66
|
-
maxResultsHint: "Sources returned per search (1-20).",
|
|
67
116
|
searchDepth: "Search depth",
|
|
68
117
|
searchDepthBasic: "Basic (faster)",
|
|
69
118
|
searchDepthAdvanced: "Advanced (slower, deeper)",
|
|
@@ -73,9 +122,9 @@ window.__ModuleLoader__.load({
|
|
|
73
122
|
topicGeneral: "General",
|
|
74
123
|
topicNews: "News",
|
|
75
124
|
country: "Country",
|
|
76
|
-
countryHint: "ISO 2-letter code (e.g. cn).
|
|
125
|
+
countryHint: "ISO 2-letter code (e.g. cn).",
|
|
77
126
|
searchLang: "Search language",
|
|
78
|
-
searchLangHint: "e.g. zh-hans or en.
|
|
127
|
+
searchLangHint: "e.g. zh-hans or en.",
|
|
79
128
|
freshness: "Freshness",
|
|
80
129
|
freshnessAny: "Any time",
|
|
81
130
|
freshnessDay: "Past day",
|
|
@@ -83,7 +132,7 @@ window.__ModuleLoader__.load({
|
|
|
83
132
|
freshnessMonth: "Past month",
|
|
84
133
|
freshnessYear: "Past year",
|
|
85
134
|
proxy: "Proxy",
|
|
86
|
-
|
|
135
|
+
proxyPlaceholder: "Leave blank to use HTTPS_PROXY / HTTP_PROXY.",
|
|
87
136
|
deepseekApiKeyEnv: "Credential reference",
|
|
88
137
|
deepseekApiKeyEnvHint: "Name of the credential/env var resolved for DeepSeek search.",
|
|
89
138
|
deepseekBaseURL: "Endpoint",
|
|
@@ -98,18 +147,47 @@ window.__ModuleLoader__.load({
|
|
|
98
147
|
maxUsesHint: "Maximum web_search server-tool uses per request.",
|
|
99
148
|
expand: "Show settings",
|
|
100
149
|
collapse: "Hide settings",
|
|
150
|
+
getKey: "Get API key",
|
|
151
|
+
ddgDisabled: "DuckDuckGo — use via SearXNG",
|
|
152
|
+
ddgHint: "DuckDuckGo has no official search API. Pick SearXNG and point it at an instance that includes DuckDuckGo (most public ones do), or self-host one.",
|
|
101
153
|
save: "Save",
|
|
102
154
|
saving: "Saving…",
|
|
103
155
|
discard: "Discard",
|
|
104
156
|
unsaved: "Unsaved",
|
|
157
|
+
overridden: "Overridden",
|
|
158
|
+
reset: "Restore default",
|
|
105
159
|
saveFailed: "The deployment did not accept these values; they were left for you to correct.",
|
|
160
|
+
savedToast: "✓ Settings saved and applied",
|
|
161
|
+
nothingToSave: "No changes to save",
|
|
162
|
+
invalidToast: "Fix the highlighted fields before saving.",
|
|
106
163
|
readOnly: "This deployment stores settings read-only.",
|
|
107
|
-
invalidNumber: "Enter a number, or leave blank
|
|
164
|
+
invalidNumber: "Enter a whole number of at least 1, or leave blank for the default.",
|
|
165
|
+
quotaTitle: "Quota",
|
|
166
|
+
quotaPlan: "Plan {plan}",
|
|
167
|
+
quotaUsed: "Used {used} / {limit}",
|
|
168
|
+
quotaUsedOnly: "Used {used}",
|
|
169
|
+
quotaRemaining: "{remaining} left",
|
|
170
|
+
quotaUnlimited: "No published limit",
|
|
171
|
+
quotaResets: "Resets {time}",
|
|
172
|
+
quotaCycle: "Resets with the billing cycle",
|
|
173
|
+
quotaBurst: "Burst {remaining}/{limit}",
|
|
174
|
+
quotaBraveCapacity: "Capacity",
|
|
175
|
+
quotaBraveRps: "{limit} requests per second",
|
|
176
|
+
quotaBraveRpsWindow: "Per-second window",
|
|
177
|
+
braveDefaultPlaceholder: "Leave blank for Brave's default.",
|
|
178
|
+
quotaEmpty: "Usage appears after the next search.",
|
|
179
|
+
quotaKeyedHint: "Keyed Tavily: local deductions vs periodic /usage, whichever is higher.",
|
|
180
|
+
quotaBraveUnlimited: "No monthly request cap",
|
|
181
|
+
quotaRefresh: "Refresh usage",
|
|
182
|
+
quotaRefreshing: "Refreshing…",
|
|
183
|
+
quotaError: "Usage unavailable: {error}",
|
|
184
|
+
quotaWarn: "Remaining quota is low.",
|
|
185
|
+
quotaDanger: "Remaining quota is critical.",
|
|
186
|
+
quotaPaygo: "Pay-as-you-go {used}/{limit}"
|
|
108
187
|
};
|
|
109
188
|
/** Simplified Chinese copy. */
|
|
110
189
|
const zh = {
|
|
111
190
|
nav: "网页搜索",
|
|
112
|
-
description: "Tavily(默认 keyless)或 Brave Search。在此切换引擎即可,不必改 web.searchProvider。",
|
|
113
191
|
provider: "搜索引擎",
|
|
114
192
|
providerTavily: "Tavily",
|
|
115
193
|
providerBrave: "Brave Search",
|
|
@@ -120,7 +198,8 @@ window.__ModuleLoader__.load({
|
|
|
120
198
|
apiKey: "API Key",
|
|
121
199
|
apiKeyHint: "不写入设置文件。留空表示保持当前密钥。",
|
|
122
200
|
apiKeySet: "已配置密钥。",
|
|
123
|
-
apiKeyUnset: "
|
|
201
|
+
apiKeyUnset: "未配置密钥",
|
|
202
|
+
apiKeyUnavailableHint: "未配置密钥前搜索不可用。",
|
|
124
203
|
apiKeyEnv: "凭据引用名",
|
|
125
204
|
apiKeyEnvHint: "keyed 模式解析的凭据/环境变量名。",
|
|
126
205
|
braveApiKeyEnv: "凭据引用名",
|
|
@@ -130,7 +209,6 @@ window.__ModuleLoader__.load({
|
|
|
130
209
|
braveBaseURL: "接口地址",
|
|
131
210
|
braveBaseURLHint: "留空则使用 Brave Search API 默认地址。",
|
|
132
211
|
maxResults: "结果数量",
|
|
133
|
-
maxResultsHint: "每次搜索返回的源数量(1-20)。",
|
|
134
212
|
searchDepth: "搜索深度",
|
|
135
213
|
searchDepthBasic: "Basic(较快)",
|
|
136
214
|
searchDepthAdvanced: "Advanced(较慢、更深)",
|
|
@@ -140,9 +218,9 @@ window.__ModuleLoader__.load({
|
|
|
140
218
|
topicGeneral: "通用",
|
|
141
219
|
topicNews: "新闻",
|
|
142
220
|
country: "国家/地区",
|
|
143
|
-
countryHint: "ISO 两位码(如 cn
|
|
221
|
+
countryHint: "ISO 两位码(如 cn)。",
|
|
144
222
|
searchLang: "搜索语言",
|
|
145
|
-
searchLangHint: "如 zh-hans、en
|
|
223
|
+
searchLangHint: "如 zh-hans、en。",
|
|
146
224
|
freshness: "时效",
|
|
147
225
|
freshnessAny: "不限",
|
|
148
226
|
freshnessDay: "过去一天",
|
|
@@ -150,7 +228,7 @@ window.__ModuleLoader__.load({
|
|
|
150
228
|
freshnessMonth: "过去一月",
|
|
151
229
|
freshnessYear: "过去一年",
|
|
152
230
|
proxy: "代理",
|
|
153
|
-
|
|
231
|
+
proxyPlaceholder: "留空使用 HTTPS_PROXY / HTTP_PROXY环境变量",
|
|
154
232
|
deepseekApiKeyEnv: "凭据引用名",
|
|
155
233
|
deepseekApiKeyEnvHint: "DeepSeek 搜索解析的凭据/环境变量名。",
|
|
156
234
|
deepseekBaseURL: "接口地址",
|
|
@@ -165,25 +243,60 @@ window.__ModuleLoader__.load({
|
|
|
165
243
|
maxUsesHint: "每次请求 web_search 服务端工具的最大调用次数。",
|
|
166
244
|
expand: "展开设置",
|
|
167
245
|
collapse: "收起设置",
|
|
246
|
+
getKey: "获取 API key",
|
|
247
|
+
ddgDisabled: "DuckDuckGo — 请经 SearXNG 使用",
|
|
248
|
+
ddgHint: "DuckDuckGo 没有官方搜索 API。请选择 SearXNG,并填入一个聚合了 DuckDuckGo 的实例地址(多数公共实例都有),或自建一个。",
|
|
168
249
|
save: "保存",
|
|
169
250
|
saving: "保存中…",
|
|
170
251
|
discard: "放弃修改",
|
|
171
252
|
unsaved: "未保存",
|
|
253
|
+
overridden: "已覆盖",
|
|
254
|
+
reset: "恢复默认",
|
|
172
255
|
saveFailed: "本部署没有接受这些值,已保留供你修改。",
|
|
256
|
+
savedToast: "✓ 设置已保存并立即生效",
|
|
257
|
+
nothingToSave: "没有需要保存的更改",
|
|
258
|
+
invalidToast: "请先修正标红的字段再保存。",
|
|
173
259
|
readOnly: "本部署的设置为只读。",
|
|
174
|
-
invalidNumber: "
|
|
260
|
+
invalidNumber: "请输入不小于 1 的整数;留空表示使用默认值。",
|
|
261
|
+
quotaTitle: "额度",
|
|
262
|
+
quotaPlan: "计划 {plan}",
|
|
263
|
+
quotaUsed: "已用 {used} / {limit}",
|
|
264
|
+
quotaUsedOnly: "已用 {used}",
|
|
265
|
+
quotaRemaining: "剩余 {remaining}",
|
|
266
|
+
quotaUnlimited: "未公布限额",
|
|
267
|
+
quotaResets: "{time} 重置",
|
|
268
|
+
quotaCycle: "按计费周期重置",
|
|
269
|
+
quotaBurst: "突发 {remaining}/{limit}",
|
|
270
|
+
quotaBraveCapacity: "容量",
|
|
271
|
+
quotaBraveRps: "{limit} 次/秒",
|
|
272
|
+
quotaBraveRpsWindow: "每秒窗口",
|
|
273
|
+
braveDefaultPlaceholder: "留空使用 Brave 默认。",
|
|
274
|
+
quotaEmpty: "下次搜索后显示用量。",
|
|
275
|
+
quotaKeyedHint: "Tavily keyed:本地扣费与定时 /usage 取较高值。",
|
|
276
|
+
quotaBraveUnlimited: "月请求不限额",
|
|
277
|
+
quotaRefresh: "刷新额度",
|
|
278
|
+
quotaRefreshing: "刷新中…",
|
|
279
|
+
quotaError: "额度不可用:{error}",
|
|
280
|
+
quotaWarn: "剩余额度偏低。",
|
|
281
|
+
quotaDanger: "剩余额度告急。",
|
|
282
|
+
quotaPaygo: "按量 {used}/{limit}"
|
|
175
283
|
};
|
|
176
284
|
|
|
177
|
-
|
|
178
|
-
|
|
285
|
+
const MAX_RESULTS_CAP = 20;
|
|
286
|
+
/** A whole-number field spec: empty draft clears; non-digits block save; max is clamped. */
|
|
287
|
+
function numberField(field, bounds) {
|
|
179
288
|
return {
|
|
180
289
|
field,
|
|
181
290
|
format: (value) => typeof value === "number" ? String(value) : "",
|
|
182
291
|
parse: (text) => {
|
|
183
292
|
const trimmed = text.trim();
|
|
184
293
|
if (trimmed === "") return { kind: "clear" };
|
|
185
|
-
|
|
186
|
-
|
|
294
|
+
if (!/^[0-9]+$/u.test(trimmed)) return void 0;
|
|
295
|
+
let parsed = Number(trimmed);
|
|
296
|
+
if (!Number.isInteger(parsed) || parsed < 1) return void 0;
|
|
297
|
+
if (bounds?.min != null && parsed < bounds.min) parsed = bounds.min;
|
|
298
|
+
if (bounds?.max != null && parsed > bounds.max) parsed = bounds.max;
|
|
299
|
+
return { kind: "set", value: parsed };
|
|
187
300
|
}
|
|
188
301
|
};
|
|
189
302
|
}
|
|
@@ -227,7 +340,7 @@ window.__ModuleLoader__.load({
|
|
|
227
340
|
this.specs = new Map([
|
|
228
341
|
choiceField("provider"),
|
|
229
342
|
choiceField("mode"),
|
|
230
|
-
numberField("maxResults"),
|
|
343
|
+
numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }),
|
|
231
344
|
textField("apiKeyEnv"),
|
|
232
345
|
textField("baseURL"),
|
|
233
346
|
choiceField("searchDepth"),
|
|
@@ -243,14 +356,31 @@ window.__ModuleLoader__.load({
|
|
|
243
356
|
textField("deepseekBaseURL"),
|
|
244
357
|
textField("model"),
|
|
245
358
|
textField("apiVersion"),
|
|
246
|
-
numberField("maxTokens"),
|
|
247
|
-
numberField("maxUses")
|
|
359
|
+
numberField("maxTokens", { min: 1 }),
|
|
360
|
+
numberField("maxUses", { min: 1 }),
|
|
361
|
+
textField("serperApiKeyEnv"),
|
|
362
|
+
textField("serperBaseURL"),
|
|
363
|
+
textField("serpapiApiKeyEnv"),
|
|
364
|
+
textField("serpapiBaseURL"),
|
|
365
|
+
textField("exaApiKeyEnv"),
|
|
366
|
+
textField("exaBaseURL"),
|
|
367
|
+
textField("searxngBaseURL")
|
|
248
368
|
].map((spec) => [spec.field, spec]));
|
|
249
369
|
this.staged = new Map();
|
|
250
370
|
this.saving = false;
|
|
251
371
|
this.failed = false;
|
|
252
372
|
this.credential = { ref: "", configured: false, writable: true };
|
|
373
|
+
this.toastKey = null;
|
|
374
|
+
this.toastTimer = 0;
|
|
253
375
|
this.listeners = new Set();
|
|
376
|
+
const draft = readDraft();
|
|
377
|
+
if (draft?.staged) {
|
|
378
|
+
for (const [field, edit] of Object.entries(draft.staged)) {
|
|
379
|
+
if (!this.specs.has(field) || edit === null || typeof edit !== "object") continue;
|
|
380
|
+
this.staged.set(field, { text: String(edit.text ?? ""), clear: edit.clear === true });
|
|
381
|
+
}
|
|
382
|
+
if (draft.failed === true) this.failed = true;
|
|
383
|
+
}
|
|
254
384
|
scope.subscribe(() => {
|
|
255
385
|
this.publish();
|
|
256
386
|
});
|
|
@@ -272,7 +402,8 @@ window.__ModuleLoader__.load({
|
|
|
272
402
|
dirty: plan.length > 0,
|
|
273
403
|
invalid: plan.some((item) => item.run === void 0),
|
|
274
404
|
saving: this.saving,
|
|
275
|
-
failed: this.failed
|
|
405
|
+
failed: this.failed,
|
|
406
|
+
toastKey: this.toastKey
|
|
276
407
|
};
|
|
277
408
|
}
|
|
278
409
|
field(field) {
|
|
@@ -316,19 +447,41 @@ window.__ModuleLoader__.load({
|
|
|
316
447
|
}
|
|
317
448
|
return plan;
|
|
318
449
|
}
|
|
450
|
+
showToast(key) {
|
|
451
|
+
this.toastKey = key;
|
|
452
|
+
if (this.toastTimer) clearTimeout(this.toastTimer);
|
|
453
|
+
this.toastTimer = setTimeout(() => {
|
|
454
|
+
this.toastKey = null;
|
|
455
|
+
this.toastTimer = 0;
|
|
456
|
+
this.publish();
|
|
457
|
+
}, 2500);
|
|
458
|
+
this.publish();
|
|
459
|
+
}
|
|
319
460
|
async save() {
|
|
320
461
|
const plan = this.plan();
|
|
321
462
|
const writes = plan.flatMap((item) => item.run === void 0 ? [] : [item.run]);
|
|
322
|
-
if (
|
|
463
|
+
if (this.saving) return;
|
|
464
|
+
if (plan.length === 0) {
|
|
465
|
+
this.showToast("nothing");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
if (writes.length !== plan.length) {
|
|
469
|
+
this.showToast("invalid");
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
323
472
|
this.saving = true;
|
|
324
473
|
this.failed = false;
|
|
325
474
|
this.publish();
|
|
326
475
|
let landed = true;
|
|
327
|
-
|
|
476
|
+
try {
|
|
477
|
+
for (const write of writes) landed = await write() && landed;
|
|
478
|
+
} catch (_saveFailure) {
|
|
479
|
+
landed = false;
|
|
480
|
+
}
|
|
328
481
|
if (landed) this.staged.clear();
|
|
329
482
|
this.saving = false;
|
|
330
483
|
this.failed = !landed;
|
|
331
|
-
this.
|
|
484
|
+
this.showToast(landed ? "saved" : "failed");
|
|
332
485
|
}
|
|
333
486
|
actions() {
|
|
334
487
|
return {
|
|
@@ -350,18 +503,26 @@ window.__ModuleLoader__.load({
|
|
|
350
503
|
};
|
|
351
504
|
}
|
|
352
505
|
async clear(field) {
|
|
353
|
-
|
|
354
|
-
|
|
506
|
+
try {
|
|
507
|
+
await this.scope.unset(field);
|
|
508
|
+
return true;
|
|
509
|
+
} catch (_clearFailure) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
355
512
|
}
|
|
356
513
|
async store(field, value) {
|
|
357
|
-
|
|
358
|
-
|
|
514
|
+
try {
|
|
515
|
+
await this.scope.set(field, value);
|
|
516
|
+
return true;
|
|
517
|
+
} catch (_storeFailure) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
359
520
|
}
|
|
360
521
|
stage(field, edit) {
|
|
361
522
|
this.staged.set(field, edit);
|
|
362
523
|
this.failed = false;
|
|
363
524
|
this.publish();
|
|
364
|
-
if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv") this.readCredential();
|
|
525
|
+
if (field === "provider" || field === "apiKeyEnv" || field === "braveApiKeyEnv" || field === "deepseekApiKeyEnv" || BUILTIN_REST_PROVIDERS.some((entry) => entry.keyEnvField === field)) this.readCredential();
|
|
365
526
|
}
|
|
366
527
|
snapshotOf() {
|
|
367
528
|
return this.scope.getSnapshot();
|
|
@@ -380,6 +541,7 @@ window.__ModuleLoader__.load({
|
|
|
380
541
|
return user !== void 0 && Object.hasOwn(user, field);
|
|
381
542
|
}
|
|
382
543
|
publish() {
|
|
544
|
+
writeDraft(this);
|
|
383
545
|
for (const listener of this.listeners) listener();
|
|
384
546
|
}
|
|
385
547
|
/** Draft-aware engine: staged `provider` wins over the saved section. */
|
|
@@ -387,7 +549,8 @@ window.__ModuleLoader__.load({
|
|
|
387
549
|
const staged = this.staged.get("provider");
|
|
388
550
|
if (staged !== void 0 && !staged.clear && staged.text.trim() !== "") return staged.text.trim();
|
|
389
551
|
const declared = this.sectionValue("provider");
|
|
390
|
-
|
|
552
|
+
if (declared === "tavily" || declared === "brave" || declared === "deepseek-official" || builtinRest(declared) !== void 0) return declared;
|
|
553
|
+
return "deepseek-official";
|
|
391
554
|
}
|
|
392
555
|
tavilyRef() {
|
|
393
556
|
const declared = this.sectionValue("apiKeyEnv");
|
|
@@ -404,7 +567,17 @@ window.__ModuleLoader__.load({
|
|
|
404
567
|
/** The credential reference the currently visible engine uses. */
|
|
405
568
|
refOf() {
|
|
406
569
|
const provider = this.providerOf();
|
|
407
|
-
|
|
570
|
+
if (provider === "brave") return this.braveRef();
|
|
571
|
+
if (provider === "deepseek-official") return this.deepseekRef();
|
|
572
|
+
const rest = builtinRest(provider);
|
|
573
|
+
if (rest !== void 0 && rest.keyRequired) return this.restKeyRef(rest);
|
|
574
|
+
return this.tavilyRef();
|
|
575
|
+
}
|
|
576
|
+
/** Credential reference for a built-in REST provider's key env field. */
|
|
577
|
+
restKeyRef(rest) {
|
|
578
|
+
const declared = this.sectionValue(rest.keyEnvField);
|
|
579
|
+
if (declared !== void 0 && declared.length > 0) return declared;
|
|
580
|
+
return rest.id === "serper" ? "SERPER_API_KEY" : rest.id === "serpapi" ? "SERPAPI_API_KEY" : "EXA_API_KEY";
|
|
408
581
|
}
|
|
409
582
|
async readCredential() {
|
|
410
583
|
const ref = this.refOf();
|
|
@@ -434,7 +607,13 @@ window.__ModuleLoader__.load({
|
|
|
434
607
|
this.readCredential();
|
|
435
608
|
}
|
|
436
609
|
async writeKey(field, value) {
|
|
437
|
-
|
|
610
|
+
let ref;
|
|
611
|
+
if (field === BRAVE_API_KEY_FIELD) ref = this.braveRef();
|
|
612
|
+
else if (field === DEEPSEEK_API_KEY_FIELD) ref = this.deepseekRef();
|
|
613
|
+
else {
|
|
614
|
+
const rest = builtinRestByKeyField(field);
|
|
615
|
+
ref = rest !== void 0 ? this.restKeyRef(rest) : this.tavilyRef();
|
|
616
|
+
}
|
|
438
617
|
try {
|
|
439
618
|
await this.api.credentials.set({ ref, value });
|
|
440
619
|
} catch (_credentialWriteFailure) {}
|
|
@@ -480,8 +659,19 @@ window.__ModuleLoader__.load({
|
|
|
480
659
|
apiVersion: this.form.field("apiVersion"),
|
|
481
660
|
maxTokens: this.form.field("maxTokens"),
|
|
482
661
|
maxUses: this.form.field("maxUses"),
|
|
662
|
+
serperApiKey: this.form.field("serperApiKey"),
|
|
663
|
+
serperApiKeyEnv: this.form.field("serperApiKeyEnv"),
|
|
664
|
+
serperBaseURL: this.form.field("serperBaseURL"),
|
|
665
|
+
serpapiApiKey: this.form.field("serpapiApiKey"),
|
|
666
|
+
serpapiApiKeyEnv: this.form.field("serpapiApiKeyEnv"),
|
|
667
|
+
serpapiBaseURL: this.form.field("serpapiBaseURL"),
|
|
668
|
+
exaApiKey: this.form.field("exaApiKey"),
|
|
669
|
+
exaApiKeyEnv: this.form.field("exaApiKeyEnv"),
|
|
670
|
+
exaBaseURL: this.form.field("exaBaseURL"),
|
|
671
|
+
searxngBaseURL: this.form.field("searxngBaseURL"),
|
|
483
672
|
apiKeyConfigured: this.form.credential.configured,
|
|
484
|
-
apiKeyWritable: this.form.credential.writable
|
|
673
|
+
apiKeyWritable: this.form.credential.writable,
|
|
674
|
+
toastKey: this.form.toastKey
|
|
485
675
|
};
|
|
486
676
|
}
|
|
487
677
|
inject() {
|
|
@@ -492,79 +682,422 @@ window.__ModuleLoader__.load({
|
|
|
492
682
|
}
|
|
493
683
|
};
|
|
494
684
|
|
|
495
|
-
/** Row style: one labelled field with
|
|
496
|
-
const fieldRow = (label, hint, children) => react.createElement("div", {
|
|
497
|
-
react.createElement("
|
|
685
|
+
/** Row style: one labelled field with optional override badge and restore-default. */
|
|
686
|
+
const fieldRow = (label, hint, children, meta) => react.createElement("div", { className: "dshws_field" },
|
|
687
|
+
react.createElement("div", { className: "dshws_field_head" },
|
|
688
|
+
react.createElement("label", { className: "dshws_field_label" }, label),
|
|
689
|
+
meta?.overridden ? react.createElement("span", { className: "dshws_field_badges" },
|
|
690
|
+
react.createElement("span", { className: "dshws_badge" }, meta.t("overridden")),
|
|
691
|
+
react.createElement("button", { type: "button", className: "dshws_reset", disabled: meta.disabled === true, onClick: meta.onReset }, meta.t("reset"))) : null),
|
|
498
692
|
children,
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
693
|
+
meta?.invalid ? react.createElement("p", { className: "dshws_field_error" }, meta.t("invalidNumber")) : hint ? react.createElement("p", { className: "dshws_field_hint" }, hint) : null);
|
|
694
|
+
const pair = (left, right) => {
|
|
695
|
+
if (!left && !right) return null;
|
|
696
|
+
if (!left) return right;
|
|
697
|
+
if (!right) return left;
|
|
698
|
+
return react.createElement("div", { className: "dshws_pair" }, left, right);
|
|
699
|
+
};
|
|
700
|
+
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" };
|
|
701
|
+
const textInput = (props) => react.createElement("input", { style: controlStyle, type: "text", ...props });
|
|
702
|
+
const numberInput = (props) => react.createElement("input", {
|
|
703
|
+
style: controlStyle,
|
|
704
|
+
type: "text",
|
|
705
|
+
inputMode: "numeric",
|
|
706
|
+
pattern: "[0-9]*",
|
|
707
|
+
autoComplete: "off",
|
|
708
|
+
...props,
|
|
709
|
+
onChange: (event) => {
|
|
710
|
+
props.onChange?.({ target: { value: String(event.target.value ?? "").replace(/\D/g, "") } });
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
const secretInput = (props) => react.createElement("input", { style: controlStyle, type: "password", autoComplete: "off", ...props });
|
|
714
|
+
const selectInput = (props) => react.createElement("select", { style: controlStyle, ...props });
|
|
503
715
|
const checkboxInput = (props) => react.createElement("input", { type: "checkbox", style: { width: "16px", height: "16px", accentColor: "var(--dsw-alias-brand-primary)" }, ...props });
|
|
504
716
|
|
|
717
|
+
const CSS_ID = "dsh-web-search-plugin/styles.css#maxresults";
|
|
718
|
+
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";
|
|
719
|
+
if (typeof document !== "undefined") {
|
|
720
|
+
for (const old of document.querySelectorAll('style[data-plugin="dsh-web-search-plugin"]')) old.remove();
|
|
721
|
+
const tag = document.createElement("style");
|
|
722
|
+
tag.dataset.plugin = "dsh-web-search-plugin";
|
|
723
|
+
tag.dataset.pluginCss = CSS_ID;
|
|
724
|
+
tag.textContent = [
|
|
725
|
+
"@keyframes dshws-toast-in{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}",
|
|
726
|
+
".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}",
|
|
727
|
+
".dshws_quota_head{display:flex;align-items:baseline;justify-content:space-between;gap:8px}",
|
|
728
|
+
".dshws_quota_title{font-size:12px;font-weight:600;color:var(--dsw-alias-label-secondary)}",
|
|
729
|
+
".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}",
|
|
730
|
+
".dshws_quota_pct:disabled{cursor:wait;opacity:.7}",
|
|
731
|
+
".dshws_quota_track{height:6px;border-radius:999px;background:var(--dsw-alias-bg-layer-1,rgba(128,128,128,0.14));overflow:hidden}",
|
|
732
|
+
".dshws_quota_fill{height:100%;border-radius:999px;background:var(--dsw-alias-state-success-primary,#10b981);transition:width .2s ease}",
|
|
733
|
+
".dshws_quota_fill_warning{background:var(--dsw-alias-state-warn-primary,var(--dsw-alias-state-warning-primary,#f59e0b))}",
|
|
734
|
+
".dshws_quota_fill_danger{background:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
735
|
+
".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}",
|
|
736
|
+
".dshws_quota_hint{font-size:11px;color:var(--dsw-alias-label-tertiary);line-height:1.45;margin:0}",
|
|
737
|
+
".dshws_quota_alert{font-size:11.5px;margin:0;line-height:1.45}",
|
|
738
|
+
".dshws_quota_alert_warning{color:var(--dsw-alias-state-warn-primary,#f59e0b)}",
|
|
739
|
+
".dshws_quota_alert_danger{color:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
740
|
+
".dshws_btn{appearance:none;font:inherit;cursor:pointer;border-radius:6px;padding:8px 16px;font-size:12.5px;font-weight:500;line-height:1.5;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;transition:filter .15s ease,opacity .15s ease}",
|
|
741
|
+
".dshws_btn:disabled{cursor:not-allowed;opacity:.6}",
|
|
742
|
+
".dshws_btn_primary{background:var(--dsw-alias-button-primary-fill,var(--dsw-alias-brand-primary,#3b82f6));color:var(--dsw-alias-label-primary-foreground,#fff);font-weight:600}",
|
|
743
|
+
".dshws_btn_primary:hover:not(:disabled){filter:brightness(1.12)}",
|
|
744
|
+
".dshws_btn_secondary{background:var(--dsw-alias-button-tool-bar-fill,var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1)));color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)))}",
|
|
745
|
+
".dshws_btn_secondary:hover:not(:disabled){background:var(--dsw-alias-button-tool-bar-hover,var(--dsw-alias-interactive-bg-active,rgba(128,128,128,.16)))}",
|
|
746
|
+
".dshws_btn_outline{background:transparent;border-color:var(--dsw-alias-border-l2,var(--dsw-alias-border-secondary,rgba(128,128,128,.25)));color:var(--dsw-alias-label-secondary)}",
|
|
747
|
+
".dshws_btn_outline:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1,rgba(128,128,128,.45));background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1))}",
|
|
748
|
+
".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}",
|
|
749
|
+
".dshws_toast_error{background:var(--dsw-alias-state-error-primary,#ef4444)}",
|
|
750
|
+
".dshws_toast_info{background:var(--dsw-alias-label-secondary,#6b7280)}",
|
|
751
|
+
".dshws_card_header{display:flex;align-items:flex-start;gap:12px;padding:6px 0 2px}",
|
|
752
|
+
".dshws_card_headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}",
|
|
753
|
+
".dshws_card_name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}",
|
|
754
|
+
".dshws_card_desc{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}",
|
|
755
|
+
".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}",
|
|
756
|
+
".dshws_form{display:flex;flex-direction:column;gap:0}",
|
|
757
|
+
".dshws_field{display:flex;flex-direction:column;gap:4px;padding:10px 0;border-top:1px solid var(--dsw-alias-border-l2);min-width:0}",
|
|
758
|
+
".dshws_field_head{display:flex;align-items:center;gap:8px}",
|
|
759
|
+
".dshws_field_label{min-width:0;flex:1;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:1.5}",
|
|
760
|
+
".dshws_field_badges{display:inline-flex;align-items:center;gap:8px;flex:none}",
|
|
761
|
+
".dshws_field_hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.45}",
|
|
762
|
+
".dshws_field_error{color:var(--dsw-alias-state-error-primary,#ef4444)!important;margin:0;font-size:12px;line-height:1.45;font-weight:500}",
|
|
763
|
+
".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)}",
|
|
764
|
+
".dshws_pair>.dshws_field{border-top:none;padding:10px 0}",
|
|
765
|
+
".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}",
|
|
766
|
+
".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}",
|
|
767
|
+
".dshws_reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}",
|
|
768
|
+
".dshws_reset:disabled{cursor:default;opacity:.45}",
|
|
769
|
+
".dshws_input_invalid{border-color:var(--dsw-alias-state-error-primary,#ef4444)!important}",
|
|
770
|
+
"[data-dshws-nav]>[class*='_navIcon']{display:none}",
|
|
771
|
+
"[data-dshws-nav]:before{content:'';background:currentColor;flex:none;width:16px;height:16px;-webkit-mask:" + GLOBE_MASK + ";mask:" + GLOBE_MASK + "}"
|
|
772
|
+
].join("\n");
|
|
773
|
+
document.head.appendChild(tag);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function interpolate(template, vars) {
|
|
777
|
+
return String(template ?? "").replace(/\{(\w+)\}/g, (_, key) => vars[key] == null ? "" : String(vars[key]));
|
|
778
|
+
}
|
|
779
|
+
function tFill(t, key, vars) {
|
|
780
|
+
return interpolate(t(key), vars);
|
|
781
|
+
}
|
|
782
|
+
function formatResetTime(ms) {
|
|
783
|
+
if (!Number.isFinite(ms) || ms <= 0) return "—";
|
|
784
|
+
const date = new Date(ms);
|
|
785
|
+
return Number.isNaN(date.getTime()) ? "—" : date.toLocaleString();
|
|
786
|
+
}
|
|
787
|
+
function usedPercent(used, limit) {
|
|
788
|
+
if (!Number.isFinite(limit) || limit <= 0 || !Number.isFinite(used)) return 0;
|
|
789
|
+
return Math.max(0, Math.min(100, (100 * used) / limit));
|
|
790
|
+
}
|
|
791
|
+
function remainingLevel(used, limit) {
|
|
792
|
+
if (!Number.isFinite(limit) || limit <= 0) return "unknown";
|
|
793
|
+
const remainingPct = Math.max(0, (100 * (limit - used)) / limit);
|
|
794
|
+
if (remainingPct <= 10) return "danger";
|
|
795
|
+
if (remainingPct <= 20) return "warning";
|
|
796
|
+
return "success";
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const USAGE_POLL_MS = 15000;
|
|
800
|
+
function createUsageClientStore() {
|
|
801
|
+
let snap = { status: "idle", payload: null, refreshing: false };
|
|
802
|
+
const listeners = new Set();
|
|
803
|
+
const publish = () => {
|
|
804
|
+
for (const listener of listeners) listener();
|
|
805
|
+
};
|
|
806
|
+
const load = async (force) => {
|
|
807
|
+
if (force) {
|
|
808
|
+
snap = { ...snap, refreshing: true };
|
|
809
|
+
publish();
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
const response = await fetch(force ? "/dsh-web-search/usage?force=1" : "/dsh-web-search/usage", { cache: "no-store" });
|
|
813
|
+
const payload = await response.json();
|
|
814
|
+
snap = { status: "ok", payload, refreshing: false };
|
|
815
|
+
} catch (error) {
|
|
816
|
+
snap = { status: "error", payload: snap.payload, refreshing: false, error: String(error?.message ?? error) };
|
|
817
|
+
}
|
|
818
|
+
publish();
|
|
819
|
+
return snap;
|
|
820
|
+
};
|
|
821
|
+
return {
|
|
822
|
+
subscribe(listener) {
|
|
823
|
+
listeners.add(listener);
|
|
824
|
+
return () => listeners.delete(listener);
|
|
825
|
+
},
|
|
826
|
+
getSnapshot() {
|
|
827
|
+
return snap;
|
|
828
|
+
},
|
|
829
|
+
refresh: () => load(true),
|
|
830
|
+
start() {
|
|
831
|
+
void load(true);
|
|
832
|
+
const timer = setInterval(() => {
|
|
833
|
+
void load(false);
|
|
834
|
+
}, USAGE_POLL_MS);
|
|
835
|
+
return () => clearInterval(timer);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const usageStore = createUsageClientStore();
|
|
840
|
+
|
|
841
|
+
function QuotaBar(props) {
|
|
842
|
+
const { name, used, limit, remainingText, resetText, level, onRefresh, refreshing, t, extra } = props;
|
|
843
|
+
const pct = usedPercent(used, limit);
|
|
844
|
+
const hasLimit = Number.isFinite(limit) && limit > 0;
|
|
845
|
+
const fillClass = "dshws_quota_fill" + (level === "danger" ? " dshws_quota_fill_danger" : level === "warning" ? " dshws_quota_fill_warning" : "");
|
|
846
|
+
return react.createElement("div", { className: "dshws_quota" },
|
|
847
|
+
react.createElement("div", { className: "dshws_quota_head" },
|
|
848
|
+
react.createElement("span", { className: "dshws_quota_title" }, name),
|
|
849
|
+
react.createElement("button", {
|
|
850
|
+
type: "button",
|
|
851
|
+
className: "dshws_quota_pct",
|
|
852
|
+
disabled: refreshing,
|
|
853
|
+
title: refreshing ? t("quotaRefreshing") : t("quotaRefresh"),
|
|
854
|
+
onClick: onRefresh
|
|
855
|
+
}, hasLimit ? Math.round(pct) + "%" : (Number.isFinite(used) && used > 0 ? String(used) : "—"))),
|
|
856
|
+
react.createElement("div", { className: "dshws_quota_track" },
|
|
857
|
+
react.createElement("div", { className: fillClass, style: { width: (hasLimit ? pct : 0) + "%" } })),
|
|
858
|
+
react.createElement("div", { className: "dshws_quota_meta" },
|
|
859
|
+
react.createElement("span", null, remainingText),
|
|
860
|
+
react.createElement("span", null, resetText)),
|
|
861
|
+
level === "warning" || level === "danger"
|
|
862
|
+
? react.createElement("p", { className: "dshws_quota_alert dshws_quota_alert_" + level }, t(level === "danger" ? "quotaDanger" : "quotaWarn"))
|
|
863
|
+
: null,
|
|
864
|
+
extra);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function QuotaCard(props) {
|
|
868
|
+
const { t, provider, keyed } = props;
|
|
869
|
+
const usage = react.useSyncExternalStore(usageStore.subscribe, usageStore.getSnapshot, usageStore.getSnapshot);
|
|
870
|
+
const showTavily = provider === "tavily" && keyed;
|
|
871
|
+
const showBrave = provider === "brave";
|
|
872
|
+
if (!showTavily && !showBrave) return null;
|
|
873
|
+
const payload = usage.payload;
|
|
874
|
+
const refreshing = usage.refreshing === true;
|
|
875
|
+
const onRefresh = (event) => {
|
|
876
|
+
event.preventDefault();
|
|
877
|
+
void usageStore.refresh();
|
|
878
|
+
};
|
|
879
|
+
if (showTavily) {
|
|
880
|
+
const info = payload?.tavily;
|
|
881
|
+
const error = info?.error || (usage.status === "error" ? usage.error : null);
|
|
882
|
+
if (!info || info.fetchedAt === 0 && info.used === 0 && !error) {
|
|
883
|
+
return react.createElement("div", { className: "dshws_quota" },
|
|
884
|
+
react.createElement("div", { className: "dshws_quota_head" },
|
|
885
|
+
react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
|
|
886
|
+
react.createElement("button", { type: "button", className: "dshws_quota_pct", disabled: refreshing, onClick: onRefresh }, refreshing ? t("quotaRefreshing") : t("quotaRefresh"))),
|
|
887
|
+
react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")),
|
|
888
|
+
react.createElement("p", { className: "dshws_quota_hint" }, t("quotaKeyedHint")));
|
|
889
|
+
}
|
|
890
|
+
const used = Number(info.used) || 0;
|
|
891
|
+
const limit = info.limit;
|
|
892
|
+
const remaining = info.remaining;
|
|
893
|
+
const level = remainingLevel(used, limit);
|
|
894
|
+
const name = info.plan ? tFill(t, "quotaPlan", { plan: info.plan }) : t("quotaTitle");
|
|
895
|
+
const remainingText = Number.isFinite(limit) && limit > 0
|
|
896
|
+
? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
|
|
897
|
+
: used > 0
|
|
898
|
+
? tFill(t, "quotaUsedOnly", { used }) + " · " + t("quotaUnlimited")
|
|
899
|
+
: t("quotaUnlimited");
|
|
900
|
+
return react.createElement(QuotaBar, {
|
|
901
|
+
name,
|
|
902
|
+
used,
|
|
903
|
+
limit,
|
|
904
|
+
remainingText,
|
|
905
|
+
resetText: t("quotaCycle"),
|
|
906
|
+
level,
|
|
907
|
+
onRefresh,
|
|
908
|
+
refreshing,
|
|
909
|
+
t,
|
|
910
|
+
extra: [
|
|
911
|
+
info.paygoUsed > 0 ? react.createElement("p", { className: "dshws_quota_hint", key: "paygo" }, tFill(t, "quotaPaygo", { used: info.paygoUsed, limit: info.paygoLimit ?? "—" })) : null,
|
|
912
|
+
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"))
|
|
913
|
+
]
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
const brave = payload?.brave;
|
|
917
|
+
const monthly = brave?.monthly;
|
|
918
|
+
const burst = brave?.burst;
|
|
919
|
+
if (!monthly && !burst) {
|
|
920
|
+
return react.createElement("div", { className: "dshws_quota" },
|
|
921
|
+
react.createElement("span", { className: "dshws_quota_title" }, t("quotaTitle")),
|
|
922
|
+
react.createElement("p", { className: "dshws_quota_hint" }, t("quotaEmpty")));
|
|
923
|
+
}
|
|
924
|
+
const monthlyCapped = Number.isFinite(monthly?.limit) && monthly.limit > 0;
|
|
925
|
+
const primary = monthlyCapped ? monthly : (burst ?? monthly);
|
|
926
|
+
const used = Number(primary.used) || 0;
|
|
927
|
+
const limit = primary.limit;
|
|
928
|
+
const remaining = primary.remaining;
|
|
929
|
+
const level = remainingLevel(used, limit);
|
|
930
|
+
return react.createElement(QuotaBar, {
|
|
931
|
+
name: monthlyCapped ? t("quotaTitle") : t("quotaBraveCapacity"),
|
|
932
|
+
used,
|
|
933
|
+
limit,
|
|
934
|
+
remainingText: monthlyCapped
|
|
935
|
+
? tFill(t, "quotaUsed", { used, limit }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? Math.max(0, limit - used) })
|
|
936
|
+
: tFill(t, "quotaBraveRps", { limit: limit ?? 0 }) + " · " + tFill(t, "quotaRemaining", { remaining: remaining ?? 0 }),
|
|
937
|
+
resetText: monthlyCapped
|
|
938
|
+
? tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) })
|
|
939
|
+
: t("quotaBraveUnlimited") + (monthly?.resetAt ? " · " + tFill(t, "quotaResets", { time: formatResetTime(monthly.resetAt) }) : ""),
|
|
940
|
+
level,
|
|
941
|
+
onRefresh,
|
|
942
|
+
refreshing,
|
|
943
|
+
t,
|
|
944
|
+
extra: monthlyCapped && burst
|
|
945
|
+
? react.createElement("p", { className: "dshws_quota_hint", key: "burst" }, tFill(t, "quotaBurst", { remaining: burst.remaining, limit: burst.limit }))
|
|
946
|
+
: null
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function toastCopy(t, key) {
|
|
951
|
+
if (key === "saved") return t("savedToast");
|
|
952
|
+
if (key === "nothing") return t("nothingToSave");
|
|
953
|
+
if (key === "invalid") return t("invalidToast");
|
|
954
|
+
if (key === "failed") return t("saveFailed");
|
|
955
|
+
return "";
|
|
956
|
+
}
|
|
957
|
+
|
|
505
958
|
/** Render the provider selector plus the backend-specific staged form. */
|
|
506
959
|
function WebSearchCard(props) {
|
|
507
960
|
const { t, state, actions } = props;
|
|
508
961
|
if (!state.available) return null;
|
|
509
962
|
const disabled = !state.writable;
|
|
510
|
-
const
|
|
511
|
-
const provider = state.provider.text === "brave" ? "brave" : state.provider.text === "deepseek-official" ? "deepseek-official" : "tavily";
|
|
963
|
+
const provider = state.provider.text === "tavily" || state.provider.text === "brave" || state.provider.text === "deepseek-official" || builtinRest(state.provider.text) !== void 0 ? state.provider.text : "deepseek-official";
|
|
512
964
|
const brave = provider === "brave";
|
|
513
965
|
const deepseek = provider === "deepseek-official";
|
|
966
|
+
const tavily = provider === "tavily";
|
|
967
|
+
const restMeta = builtinRest(provider);
|
|
514
968
|
const keyed = state.mode.text === "keyed";
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
]
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
969
|
+
const keyHint = state.apiKeyConfigured ? t("apiKeyHint") : t("apiKeyHint") + " " + t("apiKeyUnavailableHint");
|
|
970
|
+
const toastText = toastCopy(t, state.toastKey);
|
|
971
|
+
const toastTone = state.toastKey === "failed" || state.toastKey === "invalid" ? "error" : state.toastKey === "nothing" ? "info" : "success";
|
|
972
|
+
const row = (field, label, hint, children) => fieldRow(label, hint, children, {
|
|
973
|
+
t,
|
|
974
|
+
overridden: state[field]?.overridden === true,
|
|
975
|
+
invalid: state[field]?.invalid === true,
|
|
976
|
+
disabled,
|
|
977
|
+
onReset: () => actions.resetField(field)
|
|
978
|
+
});
|
|
979
|
+
const maxResultsWrite = numberField("maxResults", { min: 1, max: MAX_RESULTS_CAP }).parse(String(state.maxResults.text ?? "").trim());
|
|
980
|
+
const maxResultsValue = maxResultsWrite?.kind === "set" ? String(maxResultsWrite.value) : "8";
|
|
981
|
+
const maxResultsRow = row("maxResults", t("maxResults"), null, selectInput({
|
|
982
|
+
id: "ws-max",
|
|
983
|
+
value: maxResultsValue,
|
|
984
|
+
disabled,
|
|
985
|
+
className: state.maxResults.invalid ? "dshws_input_invalid" : void 0,
|
|
986
|
+
onChange: (e) => actions.edit("maxResults", e.target.value),
|
|
987
|
+
children: Array.from({ length: MAX_RESULTS_CAP }, (_, index) => {
|
|
988
|
+
const n = String(index + 1);
|
|
989
|
+
return react.createElement("option", { value: n, key: n }, n);
|
|
990
|
+
})
|
|
991
|
+
}));
|
|
992
|
+
return react.createElement("div", { className: "dshws_form" },
|
|
993
|
+
react.createElement("div", { className: "dshws_card_header" },
|
|
994
|
+
react.createElement("div", { className: "dshws_card_headText" },
|
|
995
|
+
react.createElement("span", { className: "dshws_card_name" }, t("nav"))),
|
|
996
|
+
state.dirty || state.failed ? react.createElement("span", { className: "dshws_pending" }, t("unsaved")) : null),
|
|
997
|
+
!state.writable ? react.createElement("p", { className: "dshws_field_hint", style: { padding: "8px 0 0" } }, t("readOnly")) : null,
|
|
998
|
+
pair(
|
|
999
|
+
row("provider", t("provider"), null, selectInput({ id: "ws-provider", value: provider, disabled, onChange: (e) => actions.edit("provider", e.target.value), children: [
|
|
1000
|
+
react.createElement("option", { value: "tavily", key: "tavily" }, t("providerTavily")),
|
|
1001
|
+
react.createElement("option", { value: "brave", key: "brave" }, t("providerBrave")),
|
|
1002
|
+
react.createElement("option", { value: "deepseek-official", key: "deepseek-official" }, t("providerDeepseek")),
|
|
1003
|
+
...BUILTIN_REST_PROVIDERS.map((entry) => react.createElement("option", { value: entry.id, key: entry.id }, entry.name)),
|
|
1004
|
+
react.createElement("option", { value: "__ddg__", key: "ddg", disabled: true }, t("ddgDisabled"))
|
|
1005
|
+
] }),
|
|
1006
|
+
react.createElement("p", { className: "dshws_field_hint", style: { padding: "4px 0 0" } }, t("ddgHint"))),
|
|
1007
|
+
tavily ? row("mode", t("mode"), null, selectInput({ id: "ws-mode", value: state.mode.text, disabled, onChange: (e) => actions.edit("mode", e.target.value), children: [
|
|
1008
|
+
react.createElement("option", { value: "keyless", key: "keyless" }, t("modeKeyless")),
|
|
1009
|
+
react.createElement("option", { value: "keyed", key: "keyed" }, t("modeKeyed"))
|
|
1010
|
+
] })) : null
|
|
1011
|
+
),
|
|
1012
|
+
react.createElement(QuotaCard, { t, provider, keyed }),
|
|
1013
|
+
deepseek ? pair(
|
|
1014
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-deepseek-key", value: state.deepseekApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(DEEPSEEK_API_KEY_FIELD, e.target.value) })),
|
|
1015
|
+
row("deepseekApiKeyEnv", t("deepseekApiKeyEnv"), t("deepseekApiKeyEnvHint"), textInput({ id: "ws-deepseek-keyref", value: state.deepseekApiKeyEnv.text, disabled, onChange: (e) => actions.edit("deepseekApiKeyEnv", e.target.value) }))
|
|
1016
|
+
) : null,
|
|
1017
|
+
brave ? pair(
|
|
1018
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-brave-key", value: state.braveApiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(BRAVE_API_KEY_FIELD, e.target.value) })),
|
|
1019
|
+
row("braveApiKeyEnv", t("braveApiKeyEnv"), t("braveApiKeyEnvHint"), textInput({ id: "ws-brave-keyref", value: state.braveApiKeyEnv.text, disabled, onChange: (e) => actions.edit("braveApiKeyEnv", e.target.value) }))
|
|
1020
|
+
) : null,
|
|
1021
|
+
(tavily && keyed) ? pair(
|
|
1022
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-tavily-key", value: state.apiKey.text, disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(TAVILY_API_KEY_FIELD, e.target.value) })),
|
|
1023
|
+
row("apiKeyEnv", t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-tavily-keyref", value: state.apiKeyEnv.text, disabled, onChange: (e) => actions.edit("apiKeyEnv", e.target.value) }))
|
|
1024
|
+
) : null,
|
|
1025
|
+
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,
|
|
1026
|
+
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,
|
|
1027
|
+
tavily ? 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,
|
|
1028
|
+
deepseek ? pair(
|
|
1029
|
+
row("model", t("model"), t("modelHint"), textInput({ id: "ws-model", value: state.model.text, disabled, onChange: (e) => actions.edit("model", e.target.value) })),
|
|
1030
|
+
row("apiVersion", t("apiVersion"), t("apiVersionHint"), textInput({ id: "ws-apiversion", value: state.apiVersion.text, disabled, onChange: (e) => actions.edit("apiVersion", e.target.value) }))
|
|
1031
|
+
) : null,
|
|
1032
|
+
deepseek ? pair(
|
|
1033
|
+
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 })),
|
|
1034
|
+
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 }))
|
|
1035
|
+
) : null,
|
|
1036
|
+
tavily ? pair(
|
|
1037
|
+
row("searchDepth", t("searchDepth"), null, selectInput({ id: "ws-depth", value: state.searchDepth.text, disabled, onChange: (e) => actions.edit("searchDepth", e.target.value), children: [
|
|
1038
|
+
react.createElement("option", { value: "basic", key: "basic" }, t("searchDepthBasic")),
|
|
1039
|
+
react.createElement("option", { value: "advanced", key: "advanced" }, t("searchDepthAdvanced"))
|
|
1040
|
+
] })),
|
|
1041
|
+
row("topic", t("topic"), null, selectInput({ id: "ws-topic", value: state.topic.text, disabled, onChange: (e) => actions.edit("topic", e.target.value), children: [
|
|
1042
|
+
react.createElement("option", { value: "general", key: "general" }, t("topicGeneral")),
|
|
1043
|
+
react.createElement("option", { value: "news", key: "news" }, t("topicNews"))
|
|
1044
|
+
] }))
|
|
1045
|
+
) : null,
|
|
1046
|
+
tavily ? pair(
|
|
1047
|
+
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" : "") })),
|
|
1048
|
+
maxResultsRow
|
|
1049
|
+
) : null,
|
|
1050
|
+
brave ? pair(
|
|
1051
|
+
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) })),
|
|
1052
|
+
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) }))
|
|
1053
|
+
) : null,
|
|
1054
|
+
brave ? pair(
|
|
1055
|
+
row("freshness", t("freshness"), null, selectInput({ id: "ws-freshness", value: state.freshness.text, disabled, onChange: (e) => actions.edit("freshness", e.target.value), children: [
|
|
1056
|
+
react.createElement("option", { value: "", key: "any" }, t("freshnessAny")),
|
|
1057
|
+
react.createElement("option", { value: "pd", key: "pd" }, t("freshnessDay")),
|
|
1058
|
+
react.createElement("option", { value: "pw", key: "pw" }, t("freshnessWeek")),
|
|
1059
|
+
react.createElement("option", { value: "pm", key: "pm" }, t("freshnessMonth")),
|
|
1060
|
+
react.createElement("option", { value: "py", key: "py" }, t("freshnessYear"))
|
|
1061
|
+
] })),
|
|
1062
|
+
maxResultsRow
|
|
1063
|
+
) : null,
|
|
1064
|
+
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,
|
|
1065
|
+
deepseek ? maxResultsRow : null,
|
|
1066
|
+
restMeta ? react.createElement("div", { className: "dshws_form" },
|
|
1067
|
+
restMeta.keyRequired && restMeta.officialUrl ? react.createElement("a", {
|
|
1068
|
+
href: restMeta.officialUrl,
|
|
1069
|
+
target: "_blank",
|
|
1070
|
+
rel: "noopener noreferrer",
|
|
1071
|
+
className: "dshws_field_hint",
|
|
1072
|
+
style: { color: "var(--dsw-alias-brand-primary,#3b82f6)", textDecoration: "none", padding: "8px 0", display: "inline-block" }
|
|
1073
|
+
}, t("getKey") + " ↗") : null,
|
|
1074
|
+
restMeta.keyRequired ? pair(
|
|
1075
|
+
fieldRow(t("apiKey"), keyHint, secretInput({ id: "ws-" + restMeta.id + "-key", value: state[restMeta.keyField]?.text ?? "", disabled: !state.apiKeyWritable, placeholder: state.apiKeyConfigured ? t("apiKeySet") : t("apiKeyUnset"), onChange: (e) => actions.edit(restMeta.keyField, e.target.value) })),
|
|
1076
|
+
row(restMeta.keyEnvField, t("apiKeyEnv"), t("apiKeyEnvHint"), textInput({ id: "ws-" + restMeta.id + "-keyref", value: state[restMeta.keyEnvField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.keyEnvField, e.target.value) }))
|
|
1077
|
+
) : null,
|
|
1078
|
+
row(restMeta.baseURLField, t("baseURL"), t("baseURLHint"), textInput({ id: "ws-" + restMeta.id + "-base", value: state[restMeta.baseURLField]?.text ?? "", disabled, onChange: (e) => actions.edit(restMeta.baseURLField, e.target.value) })),
|
|
1079
|
+
maxResultsRow
|
|
1080
|
+
) : null,
|
|
564
1081
|
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
|
-
|
|
566
|
-
|
|
567
|
-
|
|
1082
|
+
react.createElement("button", {
|
|
1083
|
+
type: "button",
|
|
1084
|
+
className: "dshws_btn dshws_btn_secondary",
|
|
1085
|
+
disabled: (!state.dirty && !state.failed) || state.saving,
|
|
1086
|
+
onClick: (event) => {
|
|
1087
|
+
event.preventDefault();
|
|
1088
|
+
actions.discard();
|
|
1089
|
+
}
|
|
1090
|
+
}, t("discard")),
|
|
1091
|
+
react.createElement("button", {
|
|
1092
|
+
type: "button",
|
|
1093
|
+
className: "dshws_btn dshws_btn_primary",
|
|
1094
|
+
disabled: state.saving || !state.writable,
|
|
1095
|
+
onClick: (event) => {
|
|
1096
|
+
event.preventDefault();
|
|
1097
|
+
void actions.save();
|
|
1098
|
+
}
|
|
1099
|
+
}, t(state.saving ? "saving" : "save"))),
|
|
1100
|
+
toastText ? react.createElement("div", { className: "dshws_toast" + (toastTone === "error" ? " dshws_toast_error" : toastTone === "info" ? " dshws_toast_info" : ""), role: "status" }, toastText) : null);
|
|
568
1101
|
}
|
|
569
1102
|
|
|
570
1103
|
/** Section component: the "Web search" page body, driven by useSyncExternalStore. */
|
|
@@ -579,6 +1112,32 @@ window.__ModuleLoader__.load({
|
|
|
579
1112
|
};
|
|
580
1113
|
}
|
|
581
1114
|
|
|
1115
|
+
function syncSettingsNavIcon() {
|
|
1116
|
+
if (typeof document === "undefined" || typeof document.querySelectorAll !== "function") return;
|
|
1117
|
+
const lists = document.querySelectorAll("[class*='_navList']");
|
|
1118
|
+
for (const list of lists) {
|
|
1119
|
+
const cells = list.querySelectorAll(":scope > [class*='_navCell']");
|
|
1120
|
+
for (const cell of cells) {
|
|
1121
|
+
const text = String(cell.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
1122
|
+
if (text === "网页搜索" || text === "Web search") cell.setAttribute("data-dshws-nav", "web-search");
|
|
1123
|
+
else cell.removeAttribute("data-dshws-nav");
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
function startSettingsNavIconSync() {
|
|
1128
|
+
if (typeof document === "undefined") return () => {};
|
|
1129
|
+
const run = () => {
|
|
1130
|
+
try { syncSettingsNavIcon(); } catch { /* settings sheet not mounted yet */ }
|
|
1131
|
+
};
|
|
1132
|
+
run();
|
|
1133
|
+
if (typeof MutationObserver !== "function") return () => {};
|
|
1134
|
+
const root = document.body || document.documentElement;
|
|
1135
|
+
if (!root) return () => {};
|
|
1136
|
+
const observer = new MutationObserver(run);
|
|
1137
|
+
observer.observe(root, { childList: true, subtree: true, characterData: true });
|
|
1138
|
+
return () => observer.disconnect();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
582
1141
|
/** Mount the top-level "Web search" settings section. */
|
|
583
1142
|
function apply(ctx) {
|
|
584
1143
|
const { api } = ctx.get("connection");
|
|
@@ -588,6 +1147,8 @@ window.__ModuleLoader__.load({
|
|
|
588
1147
|
ctx.effect(() => ctx.remote.$on("credentials/updated", (ref) => {
|
|
589
1148
|
controller.form.refreshCredential(ref);
|
|
590
1149
|
}), "dsh-web-search-plugin: credential invalidations");
|
|
1150
|
+
ctx.effect(() => usageStore.start(), "dsh-web-search-plugin: usage poll");
|
|
1151
|
+
ctx.effect(() => startSettingsNavIconSync(), "dsh-web-search-plugin: settings nav icon");
|
|
591
1152
|
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
592
1153
|
name: "settings.section",
|
|
593
1154
|
id: "web-search",
|