@darlingc/dsh-freesearch 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js ADDED
@@ -0,0 +1,805 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-free-search",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+
7
+ let react = require("react");
8
+ let react_jsx_runtime = require("react/jsx-runtime");
9
+
10
+ //#region css
11
+ const css = [
12
+ ".dshfs-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:8px;min-width:0;list-style:none;transition:border-color .16s,background .16s;overflow:hidden;margin-bottom:8px}",
13
+ ".dshfs-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}",
14
+ ".dshfs-header{width:100%;color:inherit;cursor:pointer;text-align:left;font:inherit;background:0 0;border:0;align-items:center;gap:8px;padding:10px 14px;display:flex}",
15
+ ".dshfs-header:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
16
+ ".dshfs-headText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex;overflow:hidden}",
17
+ ".dshfs-name{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-weight:600;overflow:hidden}",
18
+ ".dshfs-description{color:var(--dsw-alias-label-tertiary);white-space:nowrap;text-overflow:ellipsis;font-size:12px;overflow:hidden}",
19
+ ".dshfs-pending{color:var(--dsw-alias-state-warn-primary);white-space:nowrap;flex:none;font-size:12px}",
20
+ ".dshfs-chevron{color:var(--dsw-alias-label-tertiary);flex:none;font-size:13px;transition:transform .12s}",
21
+ ".dshfs-chevronOpen{transform:rotate(180deg)}",
22
+ ".dshfs-body{flex-direction:column;gap:14px;padding:0 14px 14px;display:flex}",
23
+ ".dshfs-footer{justify-content:space-between;align-items:center;gap:8px;display:flex;flex-wrap:wrap}",
24
+ ".dshfs-footerLeft{display:flex;align-items:center;gap:8px;flex-wrap:wrap;min-width:0}",
25
+ ".dshfs-footerRight{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
26
+ ".dshfs-failed{color:var(--dsw-alias-state-error-primary);font-size:12px}",
27
+ ".dshfs-testOk{color:#7ddb9c;font-size:12px;line-height:1.5}",
28
+ ".dshfs-resultRow{display:flex;flex-direction:column;align-items:flex-start;gap:4px;min-width:0;margin-top:2px}",
29
+ ".dshfs-field{flex-direction:column;gap:4px;min-width:0;display:flex}",
30
+ ".dshfs-label{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}",
31
+ ".dshfs-select{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
32
+ ".dshfs-select:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
33
+ ".dshfs-input{border:1px solid var(--dsw-alias-border-l2);font:inherit;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-primary);background:var(--dsw-specific-input-major);border-radius:6px;padding:6px 8px;font-size:13px;transition:border-color .13s,box-shadow .13s;width:100%}",
34
+ ".dshfs-ttl{width:88px}",
35
+ ".dshfs-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
36
+ ".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
37
+ ".dshfs-input:disabled{opacity:.6;cursor:default}",
38
+ ".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
39
+ ".dshfs-platforms{display:flex;gap:10px;flex-wrap:wrap}",
40
+ ".dshfs-platform{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
41
+ ".dshfs-platform input{accent-color:var(--dsw-alias-state-business-primary)}",
42
+ ".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
43
+ ".dshfs-link:hover{text-decoration:underline}",
44
+ ".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
45
+ ".dshfs-save{border:1px solid var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}",
46
+ ".dshfs-save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}",
47
+ ".dshfs-save:disabled{opacity:.5;cursor:default}",
48
+ ".dshfs-upgrade{border:1px solid rgba(80,200,120,.4);background:rgba(80,200,120,.15);color:#7ddb9c}",
49
+ ".dshfs-upgrade:hover:not(:disabled){background:rgba(80,200,120,.28)}",
50
+ ".dshfs-upgrade:disabled{opacity:.5;cursor:default}",
51
+ ".dshfs-badge{background:var(--dsw-alias-interactive-bg-hover-accent);color:var(--dsw-alias-state-business-primary);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
52
+ ".dshfs-badgeFree{background:rgba(80,200,120,.15);color:#7ddb9c;border:1px solid rgba(80,200,120,.3);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
53
+ ".dshfs-badgeKey{background:rgba(240,170,80,.15);color:#f0b060;border:1px solid rgba(240,170,80,.3);white-space:nowrap;border-radius:999px;flex:none;padding:1px 6px;font-size:11px}",
54
+ ".dshfs-langToggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;flex:none;padding:2px 8px;font-size:11px;border-radius:6px}",
55
+ ".dshfs-update{color:var(--dsw-alias-state-warn-primary);flex:none;font-size:11px;white-space:nowrap;border:1px solid rgba(240,170,80,.3);background:rgba(240,170,80,.12);border-radius:999px;padding:1px 6px}",
56
+ ".dshfs-updateOk{color:#7ddb9c;flex:none;font-size:11px;white-space:nowrap;border:1px solid rgba(80,200,120,.3);background:rgba(80,200,120,.12);border-radius:999px;padding:1px 6px}",
57
+ ".dshfs-updateLine{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
58
+ ".dshfs-version{color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}",
59
+ ].join("");
60
+ const tagId = "dsh-free-search/card.css";
61
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
62
+ const tag = document.createElement("style");
63
+ tag.dataset.plugin = "dsh-free-search";
64
+ tag.dataset.pluginCss = tagId;
65
+ tag.textContent = css;
66
+ document.head.appendChild(tag);
67
+ }
68
+ //#endregion
69
+
70
+ const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
71
+ const NS = "free-search";
72
+ const I18N = {
73
+ zh: {
74
+ description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
75
+ unsaved: "未保存",
76
+ searchEngine: "搜索引擎",
77
+ visit: "访问官网 →",
78
+ getKey: "获取 API Key →",
79
+ engineHint: "Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
80
+ apiKeys: "API 密钥(可选)",
81
+ exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
82
+ tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
83
+ keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
84
+ perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
85
+ deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
86
+ geminiPh: (c) => c ? "Gemini API 密钥(已配置)" : "Gemini API 密钥(免费层,联网搜索)",
87
+ keysHint: "密钥保存在 settings.yaml(界面脱敏显示)。为某引擎填写密钥后即可使用。",
88
+ platformSearch: "平台搜索(platform_search 工具)",
89
+ platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
90
+ cacheTtl: "结果缓存时长(分钟)",
91
+ cacheTtlHint: "0 关闭缓存,最长 5 分钟。缩短可加快时效,延长可防限流、省额度。",
92
+ unavailable: "设置不可用 —— free-search 桥接未暴露。",
93
+ saveFailed: "保存失败",
94
+ testing: "测试中…",
95
+ testEngine: "测试引擎",
96
+ useBing: "恢复 Bing 默认",
97
+ discard: "撤销",
98
+ saving: "保存中…",
99
+ save: "保存",
100
+ testOk: (r) => `✓ ${r.count} 条结果(引擎: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · 例如 "${r.sample.slice(0, 40)}"` : ""}`,
101
+ testFail: (e) => `✗ ${e}`,
102
+ toggleLang: "EN",
103
+ checkUpdate: "检查更新",
104
+ checkingUpdate: "检查中…",
105
+ updateAvailable: (c, l) => `发现新版本 v${l}(当前 v${c})`,
106
+ updateLatest: (c) => `已是最新版本 v${c}`,
107
+ updateCheckFailed: "检查更新失败(无法访问 npm registry)",
108
+ updateView: "查看 →",
109
+ upgrade: "升级",
110
+ upgrading: "升级中…",
111
+ upgradeLinkMode: "(本地开发模式,升级请用 git pull)",
112
+ upgradeDone: (l) => `升级到 v${l} 完成,重启 dsh 后生效`,
113
+ upgradeFailed: (m) => `升级失败:${m}`,
114
+ },
115
+ en: {
116
+ description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
117
+ unsaved: "unsaved",
118
+ searchEngine: "Search engine",
119
+ visit: "Visit website →",
120
+ getKey: "Get API Key →",
121
+ engineHint: "Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
122
+ apiKeys: "API keys (optional)",
123
+ exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
124
+ tavilyPh: (c) => c ? "Tavily API key (configured)" : "Tavily API key (optional, free without)",
125
+ keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
126
+ perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
127
+ deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
128
+ geminiPh: (c) => c ? "Gemini API key (configured)" : "Gemini API key (free tier, web search)",
129
+ keysHint: "Keys are stored in settings.yaml (redacted in the UI). Enter a key for an engine to use it.",
130
+ platformSearch: "Platform search (platform_search tool)",
131
+ platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
132
+ cacheTtl: "Result cache TTL (minutes)",
133
+ cacheTtlHint: "0 disables caching, max 5 minutes. Lower = fresher results, higher = less rate-limiting / fewer credits used.",
134
+ unavailable: "Settings unavailable — the free-search bridge is not exposed.",
135
+ saveFailed: "save failed",
136
+ testing: "Testing…",
137
+ testEngine: "Test engine",
138
+ useBing: "Use Bing default",
139
+ discard: "Discard",
140
+ saving: "Saving…",
141
+ save: "Save",
142
+ testOk: (r) => `✓ ${r.count} results (engine: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · e.g. "${r.sample.slice(0, 40)}"` : ""}`,
143
+ testFail: (e) => `✗ ${e}`,
144
+ toggleLang: "中文",
145
+ checkUpdate: "Check update",
146
+ checkingUpdate: "Checking…",
147
+ updateAvailable: (c, l) => `New version v${l} available (current v${c})`,
148
+ updateLatest: (c) => `You're on the latest version v${c}`,
149
+ updateCheckFailed: "Update check failed (cannot reach npm registry)",
150
+ updateView: "View →",
151
+ upgrade: "Upgrade",
152
+ upgrading: "Upgrading…",
153
+ upgradeLinkMode: "(local dev install - use git pull to update)",
154
+ upgradeDone: (l) => `Upgraded to v${l} - restart dsh to apply`,
155
+ upgradeFailed: (m) => `Upgrade failed: ${m}`,
156
+ },
157
+ };
158
+ const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
159
+ // 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
160
+ const PLUGIN_VERSION = "0.5.0";
161
+ const ENGINES = [
162
+ { id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
163
+ { id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
164
+ { id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
165
+ { id: "anysearch", label: "AnySearch · AI", badge: "FREE", link: "https://anysearch.com" },
166
+ { id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
167
+ { id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
168
+ { id: "tavily", label: "Tavily", badge: "FREE", link: "https://app.tavily.com/home" },
169
+ { id: "keenable", label: "Keenable", badge: "FREE", link: "https://keenable.ai/login" },
170
+ { id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
171
+ { id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
172
+ { id: "gemini", label: "Gemini · Google Search", badge: "API KEY", link: "https://aistudio.google.com/apikey" },
173
+ ];
174
+
175
+ async function bridgeDescribe() {
176
+ const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
177
+ method: "POST",
178
+ headers: { "content-type": "application/json" },
179
+ body: "{}",
180
+ });
181
+ return response.json();
182
+ }
183
+
184
+ async function bridgeMutate(payload) {
185
+ const response = await fetch(`${BRIDGE_PREFIX}/mutate`, {
186
+ method: "POST",
187
+ headers: { "content-type": "application/json" },
188
+ body: JSON.stringify(payload),
189
+ });
190
+ return response.json();
191
+ }
192
+
193
+ async function bridgeRawSearch(payload) {
194
+ const response = await fetch(`${BRIDGE_PREFIX}/raw-search`, {
195
+ method: "POST",
196
+ headers: { "content-type": "application/json" },
197
+ body: JSON.stringify(payload),
198
+ });
199
+ return response.json();
200
+ }
201
+
202
+ async function bridgeCheckUpdate() {
203
+ const response = await fetch(`${BRIDGE_PREFIX}/check-update`, {
204
+ method: "POST",
205
+ headers: { "content-type": "application/json" },
206
+ body: "{}",
207
+ });
208
+ return response.json();
209
+ }
210
+
211
+ function FreeSearchCard(props) {
212
+ const [open, setOpen] = react.useState(false);
213
+ const [state, setState] = react.useState({ status: "loading" });
214
+ const [provider, setProvider] = react.useState("bing");
215
+ const [exaKey, setExaKey] = react.useState("");
216
+ const [tavilyKey, setTavilyKey] = react.useState("");
217
+ const [keenableKey, setKeenableKey] = react.useState("");
218
+ const [perplexityKey, setPerplexityKey] = react.useState("");
219
+ const [deepseekKey, setDeepseekKey] = react.useState("");
220
+ const [geminiKey, setGeminiKey] = react.useState("");
221
+ const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
222
+ const [cacheTtl, setCacheTtl] = react.useState(5);
223
+ const [keysConfigured, setKeysConfigured] = react.useState({});
224
+ const [lang, setLang] = react.useState("zh");
225
+ const [dirty, setDirty] = react.useState(false);
226
+ const [saving, setSaving] = react.useState(false);
227
+ const [failed, setFailed] = react.useState(false);
228
+ const [testing, setTesting] = react.useState(false);
229
+ const [testResult, setTestResult] = react.useState(null);
230
+ const [checkingUpdate, setCheckingUpdate] = react.useState(false);
231
+ const [upgrading, setUpgrading] = react.useState(false);
232
+ const [updateInfo, setUpdateInfo] = react.useState(null);
233
+
234
+ const load = react.useCallback(async () => {
235
+ try {
236
+ const result = await bridgeDescribe();
237
+ if (result.ok) {
238
+ const view = result.value.namespaces.find((n) => n.ns === NS);
239
+ if (view) {
240
+ const v = view.value ?? {};
241
+ setProvider(v.provider ?? "ddg");
242
+ setLang(v.lang === "en" ? "en" : "zh");
243
+ setExaKey(v.exaApiKey ?? "");
244
+ setTavilyKey(v.tavilyApiKey ?? "");
245
+ setKeenableKey(v.keenableApiKey ?? "");
246
+ setPerplexityKey(v.perplexityApiKey ?? "");
247
+ setDeepseekKey(v.deepseekApiKey ?? "");
248
+ setGeminiKey(v.geminiApiKey ?? "");
249
+ setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
250
+ setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
251
+ // secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
252
+ const configured = {};
253
+ for (const secret of view.secrets ?? []) {
254
+ if (secret.set) {
255
+ const path = secret.path.join(".");
256
+ if (path === "exaApiKey") configured.exa = true;
257
+ if (path === "tavilyApiKey") configured.tavily = true;
258
+ if (path === "keenableApiKey") configured.keenable = true;
259
+ if (path === "perplexityApiKey") configured.perplexity = true;
260
+ if (path === "deepseekApiKey") configured.deepseek = true;
261
+ if (path === "geminiApiKey") configured.gemini = true;
262
+ }
263
+ }
264
+ setKeysConfigured(configured);
265
+ setState({ status: "ready", writable: result.value.writable });
266
+ } else {
267
+ setState({ status: "unavailable" });
268
+ }
269
+ } else {
270
+ setState({ status: "unavailable" });
271
+ }
272
+ } catch {
273
+ setState({ status: "unavailable" });
274
+ }
275
+ }, []);
276
+
277
+ react.useEffect(() => {
278
+ load();
279
+ }, [load]);
280
+
281
+ const select = (value) => {
282
+ setProvider(value);
283
+ setDirty(true);
284
+ setFailed(false);
285
+ };
286
+
287
+ const save = async () => {
288
+ setSaving(true);
289
+ setFailed(false);
290
+ try {
291
+ const ops = [{ op: "set", path: ["provider"], value: provider }];
292
+ ops.push({ op: "set", path: ["lang"], value: lang });
293
+ if (exaKey.trim()) ops.push({ op: "set", path: ["exaApiKey"], value: exaKey.trim() });
294
+ if (tavilyKey.trim()) ops.push({ op: "set", path: ["tavilyApiKey"], value: tavilyKey.trim() });
295
+ if (keenableKey.trim()) ops.push({ op: "set", path: ["keenableApiKey"], value: keenableKey.trim() });
296
+ if (perplexityKey.trim()) ops.push({ op: "set", path: ["perplexityApiKey"], value: perplexityKey.trim() });
297
+ if (deepseekKey.trim()) ops.push({ op: "set", path: ["deepseekApiKey"], value: deepseekKey.trim() });
298
+ if (geminiKey.trim()) ops.push({ op: "set", path: ["geminiApiKey"], value: geminiKey.trim() });
299
+ ops.push({ op: "set", path: ["platforms"], value: platforms });
300
+ ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
301
+ const result = await bridgeMutate({ ns: NS, ops });
302
+ if (result.ok) {
303
+ setDirty(false);
304
+ setProvider(result.value.value.provider ?? provider);
305
+ setFailed(false);
306
+ load();
307
+ } else {
308
+ setFailed(true);
309
+ }
310
+ } catch {
311
+ setFailed(true);
312
+ } finally {
313
+ setSaving(false);
314
+ }
315
+ };
316
+
317
+ const discard = () => {
318
+ load();
319
+ setDirty(false);
320
+ setFailed(false);
321
+ };
322
+
323
+ const runTest = async () => {
324
+ setTesting(true);
325
+ setTestResult(null);
326
+ setFailed(false);
327
+ try {
328
+ const result = await bridgeRawSearch({
329
+ query: "DeepSeek Harness",
330
+ maxResults: 2,
331
+ engine: provider,
332
+ });
333
+ if (result.ok) {
334
+ const sources = result.value.sources ?? [];
335
+ setTestResult({
336
+ ok: true,
337
+ count: sources.length,
338
+ engine: result.value.provider ?? provider,
339
+ content: result.value.content ?? "",
340
+ sample: sources[0]?.title ?? "",
341
+ });
342
+ } else {
343
+ setTestResult({ ok: false, error: result.message ?? "unknown error" });
344
+ }
345
+ } catch {
346
+ setTestResult({ ok: false, error: "request failed" });
347
+ } finally {
348
+ setTesting(false);
349
+ }
350
+ };
351
+
352
+ const runCheckUpdate = async () => {
353
+ setCheckingUpdate(true);
354
+ setUpdateInfo(null);
355
+ setFailed(false);
356
+ try {
357
+ const result = await bridgeCheckUpdate();
358
+ if (result.ok) {
359
+ setUpdateInfo({ ok: true, ...result.value });
360
+ } else {
361
+ setUpdateInfo({ ok: false });
362
+ }
363
+ } catch {
364
+ setUpdateInfo({ ok: false });
365
+ } finally {
366
+ setCheckingUpdate(false);
367
+ }
368
+ };
369
+
370
+ const runUpdate = async () => {
371
+ setUpgrading(true);
372
+ setUpdateInfo(null);
373
+ setFailed(false);
374
+ try {
375
+ const response = await fetch(`${BRIDGE_PREFIX}/update`, {
376
+ method: "POST",
377
+ headers: { "content-type": "application/json" },
378
+ body: "{}",
379
+ });
380
+ const result = await response.json();
381
+ if (result.ok) {
382
+ setUpdateInfo({ ok: true, hasUpdate: false, upgraded: true, message: result.value.message, latest: result.value.latest });
383
+ } else {
384
+ setUpdateInfo({ ok: false, upgradeFailed: result.message ?? "upgrade failed" });
385
+ }
386
+ } catch {
387
+ setUpdateInfo({ ok: false, upgradeFailed: "request failed" });
388
+ } finally {
389
+ setUpgrading(false);
390
+ }
391
+ };
392
+
393
+ if (state.status === "loading") return null;
394
+ const ready = state.status === "ready";
395
+ const t = tt(lang);
396
+ const title = "Free Search";
397
+ const description = t.description;
398
+ const currentEngine = ENGINES.find((e) => e.id === provider) ?? ENGINES[0];
399
+ const badgeClass =
400
+ currentEngine.badge === "FREE" ? "dshfs-badge dshfs-badgeFree" : "dshfs-badge dshfs-badgeKey";
401
+
402
+ const toggleLang = () => {
403
+ setLang((prev) => (prev === "en" ? "zh" : "en"));
404
+ setDirty(true);
405
+ setFailed(false);
406
+ };
407
+
408
+ return react_jsx_runtime.jsx("li", {
409
+ className: open ? "dshfs-card dshfs-cardOpen" : "dshfs-card",
410
+ children: [
411
+ react_jsx_runtime.jsx("button", {
412
+ type: "button",
413
+ className: "dshfs-header",
414
+ "aria-expanded": open,
415
+ onClick: () => setOpen(!open),
416
+ children: [
417
+ react_jsx_runtime.jsx("span", { className: "dshfs-headText", children: [
418
+ react_jsx_runtime.jsx("span", { className: "dshfs-name", children: title }),
419
+ react_jsx_runtime.jsx("span", { className: "dshfs-description", children: description }),
420
+ ] }),
421
+ react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
422
+ dirty ? react_jsx_runtime.jsx("span", { className: "dshfs-pending", children: t.unsaved }) : null,
423
+ react_jsx_runtime.jsx("button", {
424
+ type: "button",
425
+ className: "dshfs-btn dshfs-langToggle",
426
+ onClick: (e) => {
427
+ e.stopPropagation();
428
+ toggleLang();
429
+ },
430
+ children: t.toggleLang,
431
+ }),
432
+ react_jsx_runtime.jsx("span", {
433
+ className: open ? "dshfs-chevron dshfs-chevronOpen" : "dshfs-chevron",
434
+ children: "▾",
435
+ }),
436
+ ],
437
+ }),
438
+ open
439
+ ? react_jsx_runtime.jsx("div", {
440
+ className: "dshfs-body",
441
+ children: [
442
+ react_jsx_runtime.jsx("div", {
443
+ className: "dshfs-field",
444
+ children: [
445
+ react_jsx_runtime.jsx("div", {
446
+ className: "dshfs-label",
447
+ children: [
448
+ t.searchEngine,
449
+ react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
450
+ ],
451
+ }),
452
+ react_jsx_runtime.jsx("select", {
453
+ className: "dshfs-select",
454
+ value: provider,
455
+ disabled: !ready || saving,
456
+ onChange: (e) => select(e.target.value),
457
+ children: ENGINES.map((engine) =>
458
+ react_jsx_runtime.jsx("option", { value: engine.id, children: `${engine.label} (${engine.badge})` }, engine.id)
459
+ ),
460
+ }),
461
+ currentEngine.link
462
+ ? react_jsx_runtime.jsx("a", {
463
+ className: "dshfs-link",
464
+ href: currentEngine.link,
465
+ target: "_blank",
466
+ rel: "noopener noreferrer",
467
+ children:
468
+ currentEngine.badge === "FREE"
469
+ ? t.visit
470
+ : t.getKey,
471
+ })
472
+ : null,
473
+ react_jsx_runtime.jsx("p", {
474
+ className: "dshfs-hint",
475
+ children: t.engineHint,
476
+ }),
477
+ ],
478
+ }),
479
+ react_jsx_runtime.jsx("div", {
480
+ className: "dshfs-field",
481
+ children: [
482
+ react_jsx_runtime.jsx("div", {
483
+ className: "dshfs-label",
484
+ children: t.apiKeys,
485
+ }),
486
+ react_jsx_runtime.jsx("input", {
487
+ className: "dshfs-input",
488
+ type: "password",
489
+ placeholder: t.exaPh(keysConfigured.exa),
490
+ value: exaKey,
491
+ disabled: !ready || saving,
492
+ onChange: (e) => {
493
+ setExaKey(e.target.value);
494
+ setDirty(true);
495
+ setFailed(false);
496
+ },
497
+ }),
498
+ react_jsx_runtime.jsx("input", {
499
+ className: "dshfs-input",
500
+ type: "password",
501
+ placeholder: t.tavilyPh(keysConfigured.tavily),
502
+ value: tavilyKey,
503
+ disabled: !ready || saving,
504
+ onChange: (e) => {
505
+ setTavilyKey(e.target.value);
506
+ setDirty(true);
507
+ setFailed(false);
508
+ },
509
+ }),
510
+ react_jsx_runtime.jsx("input", {
511
+ className: "dshfs-input",
512
+ type: "password",
513
+ placeholder: t.keenablePh(keysConfigured.keenable),
514
+ value: keenableKey,
515
+ disabled: !ready || saving,
516
+ onChange: (e) => {
517
+ setKeenableKey(e.target.value);
518
+ setDirty(true);
519
+ setFailed(false);
520
+ },
521
+ }),
522
+ react_jsx_runtime.jsx("input", {
523
+ className: "dshfs-input",
524
+ type: "password",
525
+ placeholder: t.perplexityPh(keysConfigured.perplexity),
526
+ value: perplexityKey,
527
+ disabled: !ready || saving,
528
+ onChange: (e) => {
529
+ setPerplexityKey(e.target.value);
530
+ setDirty(true);
531
+ setFailed(false);
532
+ },
533
+ }),
534
+ react_jsx_runtime.jsx("input", {
535
+ className: "dshfs-input",
536
+ type: "password",
537
+ placeholder: t.deepseekPh(keysConfigured.deepseek),
538
+ value: deepseekKey,
539
+ disabled: !ready || saving,
540
+ onChange: (e) => {
541
+ setDeepseekKey(e.target.value);
542
+ setDirty(true);
543
+ setFailed(false);
544
+ },
545
+ }),
546
+ react_jsx_runtime.jsx("input", {
547
+ className: "dshfs-input",
548
+ type: "password",
549
+ placeholder: t.geminiPh(keysConfigured.gemini),
550
+ value: geminiKey,
551
+ disabled: !ready || saving,
552
+ onChange: (e) => {
553
+ setGeminiKey(e.target.value);
554
+ setDirty(true);
555
+ setFailed(false);
556
+ },
557
+ }),
558
+ react_jsx_runtime.jsx("p", {
559
+ className: "dshfs-hint",
560
+ children: t.keysHint,
561
+ }),
562
+ ],
563
+ }),
564
+ react_jsx_runtime.jsx("div", {
565
+ className: "dshfs-field",
566
+ children: [
567
+ react_jsx_runtime.jsx("div", {
568
+ className: "dshfs-label",
569
+ children: t.platformSearch,
570
+ }),
571
+ react_jsx_runtime.jsx("div", {
572
+ className: "dshfs-platforms",
573
+ children: [
574
+ ["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
575
+ ["hn", "Hacker News"], ["stackoverflow", "Stack Overflow"], ["wikipedia", "Wikipedia"], ["npm", "npm"],
576
+ ].map(([id, label]) =>
577
+ react_jsx_runtime.jsx("label", {
578
+ className: "dshfs-platform",
579
+ children: [
580
+ react_jsx_runtime.jsx("input", {
581
+ type: "checkbox",
582
+ checked: platforms.includes(id),
583
+ disabled: !ready || saving,
584
+ onChange: (e) => {
585
+ setPlatforms((prev) =>
586
+ e.target.checked ? [...prev, id] : prev.filter((p) => p !== id)
587
+ );
588
+ setDirty(true);
589
+ setFailed(false);
590
+ },
591
+ }),
592
+ label,
593
+ ],
594
+ }, id)
595
+ ),
596
+ }),
597
+ react_jsx_runtime.jsx("p", {
598
+ className: "dshfs-hint",
599
+ children: t.platformHint,
600
+ }),
601
+ ],
602
+ }),
603
+ react_jsx_runtime.jsx("div", {
604
+ className: "dshfs-field",
605
+ children: [
606
+ react_jsx_runtime.jsx("div", {
607
+ className: "dshfs-label",
608
+ children: t.cacheTtl,
609
+ }),
610
+ react_jsx_runtime.jsx("input", {
611
+ className: "dshfs-input dshfs-ttl",
612
+ type: "number",
613
+ min: 0,
614
+ max: 5,
615
+ step: 1,
616
+ value: cacheTtl,
617
+ disabled: !ready || saving,
618
+ onChange: (e) => {
619
+ setCacheTtl(Number(e.target.value));
620
+ setDirty(true);
621
+ setFailed(false);
622
+ },
623
+ }),
624
+ react_jsx_runtime.jsx("p", {
625
+ className: "dshfs-hint",
626
+ children: t.cacheTtlHint,
627
+ }),
628
+ ],
629
+ }),
630
+ react_jsx_runtime.jsx("div", {
631
+ className: "dshfs-resultRow",
632
+ children: [
633
+ failed ? react_jsx_runtime.jsx("span", { className: "dshfs-failed", children: t.saveFailed }) : null,
634
+ testResult
635
+ ? react_jsx_runtime.jsx("span", {
636
+ className: testResult.ok ? "dshfs-testOk" : "dshfs-failed",
637
+ children: testResult.ok
638
+ ? t.testOk(testResult)
639
+ : t.testFail(testResult.error),
640
+ })
641
+ : null,
642
+ ],
643
+ }),
644
+ !ready
645
+ ? react_jsx_runtime.jsx("p", {
646
+ className: "dshfs-hint",
647
+ children: t.unavailable,
648
+ })
649
+ : null,
650
+ react_jsx_runtime.jsx("div", {
651
+ className: "dshfs-footer",
652
+ children: [
653
+ react_jsx_runtime.jsx("div", {
654
+ className: "dshfs-footerLeft",
655
+ children: [
656
+ react_jsx_runtime.jsx("span", { className: "dshfs-version", children: "v" + PLUGIN_VERSION }),
657
+ react_jsx_runtime.jsx("button", {
658
+ className: "dshfs-btn",
659
+ type: "button",
660
+ onClick: runCheckUpdate,
661
+ disabled: checkingUpdate || saving || !ready,
662
+ children: checkingUpdate ? t.checkingUpdate : t.checkUpdate,
663
+ }),
664
+ updateInfo && updateInfo.ok
665
+ ? updateInfo.upgraded
666
+ ? react_jsx_runtime.jsx("span", {
667
+ className: "dshfs-updateOk",
668
+ children: t.upgradeDone(updateInfo.latest),
669
+ })
670
+ : updateInfo.hasUpdate
671
+ ? react_jsx_runtime.jsx("span", {
672
+ className: "dshfs-update",
673
+ children: [
674
+ t.updateAvailable(updateInfo.current, updateInfo.latest),
675
+ " ",
676
+ updateInfo.installable
677
+ ? react_jsx_runtime.jsx("button", {
678
+ className: "dshfs-btn dshfs-upgrade",
679
+ type: "button",
680
+ onClick: runUpdate,
681
+ disabled: upgrading || saving || !ready,
682
+ children: upgrading ? t.upgrading : t.upgrade,
683
+ })
684
+ : react_jsx_runtime.jsx("a", {
685
+ className: "dshfs-link",
686
+ href: updateInfo.updateUrl,
687
+ target: "_blank",
688
+ rel: "noopener noreferrer",
689
+ children: t.updateView,
690
+ }),
691
+ updateInfo.installable ? null : " " + t.upgradeLinkMode,
692
+ ],
693
+ })
694
+ : react_jsx_runtime.jsx("span", {
695
+ className: "dshfs-updateOk",
696
+ children: t.updateLatest(updateInfo.current),
697
+ })
698
+ : updateInfo && !updateInfo.ok
699
+ ? react_jsx_runtime.jsx("span", {
700
+ className: "dshfs-failed",
701
+ children: updateInfo.upgradeFailed ? t.upgradeFailed(updateInfo.upgradeFailed) : t.updateCheckFailed,
702
+ })
703
+ : null,
704
+ ],
705
+ }),
706
+ react_jsx_runtime.jsx("div", {
707
+ className: "dshfs-footerRight",
708
+ children: [
709
+ react_jsx_runtime.jsx("button", {
710
+ className: "dshfs-btn",
711
+ type: "button",
712
+ onClick: runTest,
713
+ disabled: testing || saving || !ready,
714
+ children: testing ? t.testing : t.testEngine,
715
+ }),
716
+ react_jsx_runtime.jsx("button", {
717
+ className: "dshfs-btn",
718
+ type: "button",
719
+ onClick: () => {
720
+ setProvider("bing");
721
+ setDirty(true);
722
+ setFailed(false);
723
+ },
724
+ disabled: saving || !ready || provider === "bing",
725
+ children: t.useBing,
726
+ }),
727
+ react_jsx_runtime.jsx("button", {
728
+ className: "dshfs-btn",
729
+ type: "button",
730
+ onClick: discard,
731
+ disabled: saving || !dirty,
732
+ children: t.discard,
733
+ }),
734
+ react_jsx_runtime.jsx("button", {
735
+ className: "dshfs-btn dshfs-save",
736
+ type: "button",
737
+ onClick: save,
738
+ disabled: saving || !dirty || !ready,
739
+ children: saving ? t.saving : t.save,
740
+ }),
741
+ ],
742
+ }),
743
+ ],
744
+ })
745
+ ],
746
+ })
747
+ : null,
748
+ ],
749
+ });
750
+ }
751
+
752
+ const inject = ["slots", "commandUi"];
753
+
754
+ function apply(ctx) {
755
+ // 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
756
+ // 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
757
+ ctx.slots.inject("settings.plugin.item", () =>
758
+ ctx.slots.register(
759
+ {
760
+ name: "settings.plugin.item",
761
+ key: "free-search",
762
+ id: "dsh-free-search",
763
+ order: 120,
764
+ inject: () => ({}),
765
+ },
766
+ FreeSearchCard
767
+ )
768
+ );
769
+ // /free-search-engine 弹出式命令:输入 "/" 选中后弹出引擎列表,点选即切换。
770
+ // 等效于设置页切换引擎+保存;命令只改 provider 配置,搜索仍走回退链。
771
+ ctx.inject(["commandUi"], (sctx) => {
772
+ const command = sctx.get("commandUi");
773
+ sctx.effect(() => {
774
+ const dispose = command.register({
775
+ name: "free-search-engine",
776
+ description: "切换搜索引擎 / Switch web search engine",
777
+ available: () => true,
778
+ ui: {
779
+ kind: "popupSelect",
780
+ options: async () => {
781
+ const result = await bridgeDescribe();
782
+ const view = result.ok ? result.value.namespaces.find((n) => n.ns === NS) : undefined;
783
+ const current = view?.value?.provider ?? "bing";
784
+ return ENGINES.map((e) => ({
785
+ id: e.id,
786
+ label: `${e.label}${e.badge === "FREE" ? " · 免费" : " · API Key"}`,
787
+ detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "当前") : undefined,
788
+ active: e.id === current,
789
+ }));
790
+ },
791
+ onSelect: async (option) => {
792
+ await bridgeMutate({ ns: NS, ops: [{ op: "set", path: ["provider"], value: option.id }] });
793
+ },
794
+ },
795
+ });
796
+ return dispose;
797
+ }, "free-search: /free-search-engine command");
798
+ });
799
+ }
800
+
801
+ exports.apply = apply;
802
+ exports.inject = inject;
803
+ return module.exports;
804
+ },
805
+ });