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/CHANGELOG.md +39 -0
- package/README.md +70 -26
- package/cordis.patch.yml +8 -2
- package/lib/brave.js +1 -0
- package/lib/client.js +632 -90
- package/lib/deepseek.js +148 -0
- package/lib/index.js +138 -18
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Browser half of `dsh-web-search-plugin`: a
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
|
|
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).
|
|
103
|
+
countryHint: "ISO 2-letter code (e.g. cn).",
|
|
72
104
|
searchLang: "Search language",
|
|
73
|
-
searchLangHint: "e.g. zh-hans or en.
|
|
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
|
-
|
|
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
|
|
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
|
|
195
|
+
countryHint: "ISO 两位码(如 cn)。",
|
|
126
196
|
searchLang: "搜索语言",
|
|
127
|
-
searchLangHint: "如 zh-hans、en
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
|
448
|
-
const fieldRow = (label, hint, children) => react.createElement("div", {
|
|
449
|
-
react.createElement("
|
|
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
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
const
|
|
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
|
|
465
|
-
const 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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
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 :
|
|
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
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
] }))
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
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
|
-
/**
|
|
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.
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
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"];
|