dsh-free-search 0.4.23 → 0.4.25
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 +1015 -1012
- package/lib/index.js +2166 -2141
- package/package.json +19 -3
package/lib/client.js
CHANGED
|
@@ -1,1012 +1,1015 @@
|
|
|
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
|
-
// 下拉选项列表配色锁死:不随皮肤变量变化(皮肤只影响 select 框体本身)
|
|
34
|
-
// color-scheme 让浏览器原生下拉按主题渲染;option 显式固定底色/文字色兜底
|
|
35
|
-
".dshfs-select{color-scheme:light dark}",
|
|
36
|
-
".dshfs-select option,.dshfs-select optgroup{background-color:#ffffff;color:#1f2328}",
|
|
37
|
-
"@media (prefers-color-scheme:dark){.dshfs-select{color-scheme:dark}.dshfs-select option,.dshfs-select optgroup{background-color:#1e1f24;color:#e8e8ea}}",
|
|
38
|
-
".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%}",
|
|
39
|
-
".dshfs-ttl{width:88px}",
|
|
40
|
-
".dshfs-fieldRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
41
|
-
".dshfs-keyStorage{width:auto;min-width:180px}",
|
|
42
|
-
".dshfs-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
|
|
43
|
-
".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
44
|
-
".dshfs-input:disabled{opacity:.6;cursor:default}",
|
|
45
|
-
".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
|
|
46
|
-
".dshfs-platforms{display:flex;gap:10px;flex-wrap:wrap}",
|
|
47
|
-
".dshfs-platform{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
|
|
48
|
-
".dshfs-platform input{accent-color:var(--dsw-alias-state-business-primary)}",
|
|
49
|
-
".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
|
|
50
|
-
".dshfs-link:hover{text-decoration:underline}",
|
|
51
|
-
".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
|
|
52
|
-
".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)}",
|
|
53
|
-
".dshfs-save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}",
|
|
54
|
-
".dshfs-save:disabled{opacity:.5;cursor:default}",
|
|
55
|
-
".dshfs-upgrade{border:1px solid rgba(80,200,120,.4);background:rgba(80,200,120,.15);color:#7ddb9c}",
|
|
56
|
-
".dshfs-upgrade:hover:not(:disabled){background:rgba(80,200,120,.28)}",
|
|
57
|
-
".dshfs-upgrade:disabled{opacity:.5;cursor:default}",
|
|
58
|
-
".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}",
|
|
59
|
-
".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}",
|
|
60
|
-
".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}",
|
|
61
|
-
".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}",
|
|
62
|
-
".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}",
|
|
63
|
-
".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}",
|
|
64
|
-
".dshfs-updateLine{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
65
|
-
".dshfs-version{color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
66
|
-
].join("");
|
|
67
|
-
const tagId = "dsh-free-search/card.css";
|
|
68
|
-
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
69
|
-
const tag = document.createElement("style");
|
|
70
|
-
tag.dataset.plugin = "dsh-free-search";
|
|
71
|
-
tag.dataset.pluginCss = tagId;
|
|
72
|
-
tag.textContent = css;
|
|
73
|
-
document.head.appendChild(tag);
|
|
74
|
-
}
|
|
75
|
-
//#endregion
|
|
76
|
-
|
|
77
|
-
const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
|
|
78
|
-
const NS = "free-search";
|
|
79
|
-
const I18N = {
|
|
80
|
-
zh: {
|
|
81
|
-
description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
|
|
82
|
-
unsaved: "未保存",
|
|
83
|
-
searchEngine: "搜索引擎",
|
|
84
|
-
visit: "访问官网 →",
|
|
85
|
-
getKey: "获取 API Key →",
|
|
86
|
-
engineHint: "Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
|
|
87
|
-
apiKeys: "API 密钥(可选)",
|
|
88
|
-
exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
|
|
89
|
-
tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
|
|
90
|
-
keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
|
|
91
|
-
perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
|
|
92
|
-
deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
|
|
93
|
-
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
94
|
-
keyStorage: "Key 存储位置",
|
|
95
|
-
keyStorageCred: "凭据中心(推荐)",
|
|
96
|
-
keyStorageSettings: "设置页(兼容)",
|
|
97
|
-
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
98
|
-
keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
|
|
99
|
-
platformSearch: "平台搜索(platform_search 工具)",
|
|
100
|
-
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
101
|
-
cacheTtl: "结果缓存时长(分钟)",
|
|
102
|
-
cacheTtlHint: "0 关闭缓存,最长 5 分钟。缩短可加快时效,延长可防限流、省额度。",
|
|
103
|
-
unavailable: "设置不可用 —— free-search 桥接未暴露。",
|
|
104
|
-
saveFailed: "保存失败",
|
|
105
|
-
testing: "测试中…",
|
|
106
|
-
testEngine: "测试引擎",
|
|
107
|
-
useBing: "恢复 Bing 默认",
|
|
108
|
-
discard: "撤销",
|
|
109
|
-
saving: "保存中…",
|
|
110
|
-
save: "保存",
|
|
111
|
-
testOk: (r) => `✓ ${r.count} 条结果(引擎: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · 例如 "${r.sample.slice(0, 40)}"` : ""}`,
|
|
112
|
-
testFail: (e) => `✗ ${e}`,
|
|
113
|
-
toggleLang: "EN",
|
|
114
|
-
checkUpdate: "检查更新",
|
|
115
|
-
checkingUpdate: "检查中…",
|
|
116
|
-
updateAvailable: (c, l) => `发现新版本 v${l}(当前 v${c})`,
|
|
117
|
-
updateLatest: (c) => `已是最新版本 v${c}`,
|
|
118
|
-
updateCheckFailed: "检查更新失败(无法访问 npm registry)",
|
|
119
|
-
updateView: "查看 →",
|
|
120
|
-
upgrade: "升级",
|
|
121
|
-
upgrading: "升级中…",
|
|
122
|
-
upgradeLinkMode: "(本地开发模式,升级请用 git pull)",
|
|
123
|
-
upgradeDone: (l) => `升级到 v${l} 完成,重启 dsh 后生效`,
|
|
124
|
-
upgradeFailed: (m) => `升级失败:${m}`,
|
|
125
|
-
safeSearchLabel: "安全搜索过滤 (adlt)",
|
|
126
|
-
safeSearchOff: "关闭 —— 引擎默认(不加参数)",
|
|
127
|
-
safeSearchModerate: "中等 —— Bing 默认",
|
|
128
|
-
safeSearchStrict: "严格",
|
|
129
|
-
safeSearchHint: "作用于 Bing(adlt)、DuckDuckGo HTML / Lite(adlt 等级)。如遇引擎自带过滤可在此调整。",
|
|
130
|
-
bingMarketLabel: "Bing 市场(本地化结果)",
|
|
131
|
-
bingMarketHint: "Bing 的 mkt + Accept-Language 跟随此设置。例如 ru-RU 会让西里尔查询返回俄语结果。",
|
|
132
|
-
marketZhCN: "zh-CN —— 中国大陆(默认)",
|
|
133
|
-
marketZhTW: "zh-TW —— 台湾",
|
|
134
|
-
marketEnUS: "en-US —— 美国",
|
|
135
|
-
marketEnGB: "en-GB —— 英国",
|
|
136
|
-
marketRuRU: "ru-RU —— 俄罗斯",
|
|
137
|
-
marketJaJP: "ja-JP —— 日本",
|
|
138
|
-
marketDeDE: "de-DE —— 德国",
|
|
139
|
-
marketFrFR: "fr-FR —— 法国",
|
|
140
|
-
marketEsES: "es-ES —— 西班牙",
|
|
141
|
-
marketKoKR: "ko-KR —— 韩国",
|
|
142
|
-
},
|
|
143
|
-
en: {
|
|
144
|
-
description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
|
|
145
|
-
unsaved: "unsaved",
|
|
146
|
-
searchEngine: "Search engine",
|
|
147
|
-
visit: "Visit website →",
|
|
148
|
-
getKey: "Get API Key →",
|
|
149
|
-
engineHint: "Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
|
|
150
|
-
apiKeys: "API keys (optional)",
|
|
151
|
-
exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
|
|
152
|
-
tavilyPh: (c) => c ? "Tavily API key (configured)" : "Tavily API key (optional, free without)",
|
|
153
|
-
keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
|
|
154
|
-
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
155
|
-
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
156
|
-
keysHint: "Key resolution: .credentials.yaml credential center > here > environment variables. Recommended: store keys in the credential center (same as official LLM providers, one place for all).",
|
|
157
|
-
keyStorage: "Key storage",
|
|
158
|
-
keyStorageCred: "Credential center (recommended)",
|
|
159
|
-
keyStorageSettings: "Settings page (legacy)",
|
|
160
|
-
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
161
|
-
keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
|
|
162
|
-
platformSearch: "Platform search (platform_search tool)",
|
|
163
|
-
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
164
|
-
cacheTtl: "Result cache TTL (minutes)",
|
|
165
|
-
cacheTtlHint: "0 disables caching, max 5 minutes. Lower = fresher results, higher = less rate-limiting / fewer credits used.",
|
|
166
|
-
unavailable: "Settings unavailable — the free-search bridge is not exposed.",
|
|
167
|
-
saveFailed: "save failed",
|
|
168
|
-
testing: "Testing…",
|
|
169
|
-
testEngine: "Test engine",
|
|
170
|
-
useBing: "Use Bing default",
|
|
171
|
-
discard: "Discard",
|
|
172
|
-
saving: "Saving…",
|
|
173
|
-
save: "Save",
|
|
174
|
-
testOk: (r) => `✓ ${r.count} results (engine: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · e.g. "${r.sample.slice(0, 40)}"` : ""}`,
|
|
175
|
-
testFail: (e) => `✗ ${e}`,
|
|
176
|
-
toggleLang: "中文",
|
|
177
|
-
checkUpdate: "Check update",
|
|
178
|
-
checkingUpdate: "Checking…",
|
|
179
|
-
updateAvailable: (c, l) => `New version v${l} available (current v${c})`,
|
|
180
|
-
updateLatest: (c) => `You're on the latest version v${c}`,
|
|
181
|
-
updateCheckFailed: "Update check failed (cannot reach npm registry)",
|
|
182
|
-
updateView: "View →",
|
|
183
|
-
upgrade: "Upgrade",
|
|
184
|
-
upgrading: "Upgrading…",
|
|
185
|
-
upgradeLinkMode: "(local dev install - use git pull to update)",
|
|
186
|
-
upgradeDone: (l) => `Upgraded to v${l} - restart dsh to apply`,
|
|
187
|
-
upgradeFailed: (m) => `Upgrade failed: ${m}`,
|
|
188
|
-
safeSearchLabel: "Safe search filter (adlt)",
|
|
189
|
-
safeSearchOff: "Off - engine default (no filtering)",
|
|
190
|
-
safeSearchModerate: "Moderate - Bing default",
|
|
191
|
-
safeSearchStrict: "Strict",
|
|
192
|
-
safeSearchHint: "Applies to bing (adlt), ddg, ddg-lite (adlt degree). If you see the engine's own filtering, adjust here.",
|
|
193
|
-
bingMarketLabel: "Bing market (localized results)",
|
|
194
|
-
bingMarketHint: "Bing's mkt + Accept-Language follow this. e.g. ru-RU returns Russian results for Cyrillic queries.",
|
|
195
|
-
marketZhCN: "zh-CN - China (default)",
|
|
196
|
-
marketZhTW: "zh-TW - Taiwan",
|
|
197
|
-
marketEnUS: "en-US - United States",
|
|
198
|
-
marketEnGB: "en-GB - United Kingdom",
|
|
199
|
-
marketRuRU: "ru-RU - Russia",
|
|
200
|
-
marketJaJP: "ja-JP - Japan",
|
|
201
|
-
marketDeDE: "de-DE - Germany",
|
|
202
|
-
marketFrFR: "fr-FR - France",
|
|
203
|
-
marketEsES: "es-ES - Spain",
|
|
204
|
-
marketKoKR: "ko-KR - Korea",
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
208
|
-
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
209
|
-
const PLUGIN_VERSION = "0.4.
|
|
210
|
-
const ENGINES = [
|
|
211
|
-
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
212
|
-
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
213
|
-
{ id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
|
|
214
|
-
{ id: "anysearch", label: "AnySearch · AI", badge: "FREE", link: "https://anysearch.com" },
|
|
215
|
-
{ id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
|
|
216
|
-
{ id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
|
|
217
|
-
{ id: "tavily", label: "Tavily", badge: "FREE", link: "https://app.tavily.com/home" },
|
|
218
|
-
{ id: "keenable", label: "Keenable", badge: "FREE", link: "https://keenable.ai/login" },
|
|
219
|
-
{ id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
|
|
220
|
-
{ id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
|
|
221
|
-
];
|
|
222
|
-
|
|
223
|
-
async function bridgeDescribe() {
|
|
224
|
-
const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
|
|
225
|
-
method: "POST",
|
|
226
|
-
headers: { "content-type": "application/json" },
|
|
227
|
-
body: "{}",
|
|
228
|
-
});
|
|
229
|
-
return response.json();
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function bridgeMutate(payload) {
|
|
233
|
-
const response = await fetch(`${BRIDGE_PREFIX}/mutate`, {
|
|
234
|
-
method: "POST",
|
|
235
|
-
headers: { "content-type": "application/json" },
|
|
236
|
-
body: JSON.stringify(payload),
|
|
237
|
-
});
|
|
238
|
-
return response.json();
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function bridgeRawSearch(payload) {
|
|
242
|
-
const response = await fetch(`${BRIDGE_PREFIX}/raw-search`, {
|
|
243
|
-
method: "POST",
|
|
244
|
-
headers: { "content-type": "application/json" },
|
|
245
|
-
body: JSON.stringify(payload),
|
|
246
|
-
});
|
|
247
|
-
return response.json();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
async function bridgeCheckUpdate() {
|
|
251
|
-
const response = await fetch(`${BRIDGE_PREFIX}/check-update`, {
|
|
252
|
-
method: "POST",
|
|
253
|
-
headers: { "content-type": "application/json" },
|
|
254
|
-
body: "{}",
|
|
255
|
-
});
|
|
256
|
-
return response.json();
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function bridgeCredentialsStatus() {
|
|
260
|
-
const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: { "content-type": "application/json" },
|
|
263
|
-
body: "{}",
|
|
264
|
-
});
|
|
265
|
-
return response.json();
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
async function bridgeCredentialsSet(key, value) {
|
|
269
|
-
const response = await fetch(`${BRIDGE_PREFIX}/credentials-set`, {
|
|
270
|
-
method: "POST",
|
|
271
|
-
headers: { "content-type": "application/json" },
|
|
272
|
-
body: JSON.stringify({ key, value }),
|
|
273
|
-
});
|
|
274
|
-
return response.json();
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
async function bridgeCredentialsUnset(key) {
|
|
278
|
-
const response = await fetch(`${BRIDGE_PREFIX}/credentials-unset`, {
|
|
279
|
-
method: "POST",
|
|
280
|
-
headers: { "content-type": "application/json" },
|
|
281
|
-
body: JSON.stringify({ key }),
|
|
282
|
-
});
|
|
283
|
-
return response.json();
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
function FreeSearchCard(props) {
|
|
287
|
-
// 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
|
|
288
|
-
const isDarkScheme = react.useMemo(() => {
|
|
289
|
-
try {
|
|
290
|
-
const root = document.body || document.documentElement;
|
|
291
|
-
const bg = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base").trim();
|
|
292
|
-
const m = bg.match(/(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/);
|
|
293
|
-
if (m) {
|
|
294
|
-
const l = 0.299 * Number(m[1]) + 0.587 * Number(m[2]) + 0.114 * Number(m[3]);
|
|
295
|
-
return l < 128;
|
|
296
|
-
}
|
|
297
|
-
if (/^#([0-9a-f]{3,8})/i.test(bg)) {
|
|
298
|
-
const hex = bg.slice(1);
|
|
299
|
-
const h = hex.length <= 4 ? hex.replace(/./g, (c) => c + c) : hex;
|
|
300
|
-
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
301
|
-
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
|
302
|
-
}
|
|
303
|
-
} catch {}
|
|
304
|
-
return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
305
|
-
}, []);
|
|
306
|
-
const selectColorScheme = isDarkScheme ? "dark" : "light";
|
|
307
|
-
const [open, setOpen] = react.useState(false);
|
|
308
|
-
const [state, setState] = react.useState({ status: "loading" });
|
|
309
|
-
const [provider, setProvider] = react.useState("bing");
|
|
310
|
-
const [safeSearch, setSafeSearch] = react.useState("off");
|
|
311
|
-
const [bingMarket, setBingMarket] = react.useState("zh-CN");
|
|
312
|
-
const [exaKey, setExaKey] = react.useState("");
|
|
313
|
-
const [tavilyKey, setTavilyKey] = react.useState("");
|
|
314
|
-
const [keenableKey, setKeenableKey] = react.useState("");
|
|
315
|
-
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
316
|
-
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
317
|
-
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
318
|
-
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
319
|
-
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
320
|
-
// key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
|
|
321
|
-
const [keyStorage, setKeyStorage] = react.useState("credentials");
|
|
322
|
-
// 凭据中心里已配置的 key(describe 不返回,需单独查)
|
|
323
|
-
const [credConfigured, setCredConfigured] = react.useState({});
|
|
324
|
-
const [lang, setLang] = react.useState("zh");
|
|
325
|
-
const [dirty, setDirty] = react.useState(false);
|
|
326
|
-
const [saving, setSaving] = react.useState(false);
|
|
327
|
-
const [failed, setFailed] = react.useState(false);
|
|
328
|
-
const [testing, setTesting] = react.useState(false);
|
|
329
|
-
const [testResult, setTestResult] = react.useState(null);
|
|
330
|
-
const [checkingUpdate, setCheckingUpdate] = react.useState(false);
|
|
331
|
-
const [upgrading, setUpgrading] = react.useState(false);
|
|
332
|
-
const [updateInfo, setUpdateInfo] = react.useState(null);
|
|
333
|
-
|
|
334
|
-
const load = react.useCallback(async () => {
|
|
335
|
-
try {
|
|
336
|
-
const result = await bridgeDescribe();
|
|
337
|
-
if (result.ok) {
|
|
338
|
-
const view = result.value.namespaces.find((n) => n.ns === NS);
|
|
339
|
-
if (view) {
|
|
340
|
-
const v = view.value ?? {};
|
|
341
|
-
setProvider(v.provider ?? "ddg");
|
|
342
|
-
setSafeSearch(v.safeSearch === "strict" || v.safeSearch === "moderate" ? v.safeSearch : "off");
|
|
343
|
-
setBingMarket(v.bingMarket === undefined ? "zh-CN" : v.bingMarket);
|
|
344
|
-
setLang(v.lang === "en" ? "en" : "zh");
|
|
345
|
-
setExaKey(v.exaApiKey ?? "");
|
|
346
|
-
setTavilyKey(v.tavilyApiKey ?? "");
|
|
347
|
-
setKeenableKey(v.keenableApiKey ?? "");
|
|
348
|
-
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
349
|
-
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
350
|
-
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
351
|
-
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
352
|
-
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
353
|
-
const configured = {};
|
|
354
|
-
for (const secret of view.secrets ?? []) {
|
|
355
|
-
if (secret.set) {
|
|
356
|
-
const path = secret.path.join(".");
|
|
357
|
-
if (path === "exaApiKey") configured.exa = true;
|
|
358
|
-
if (path === "tavilyApiKey") configured.tavily = true;
|
|
359
|
-
if (path === "keenableApiKey") configured.keenable = true;
|
|
360
|
-
if (path === "perplexityApiKey") configured.perplexity = true;
|
|
361
|
-
if (path === "deepseekApiKey") configured.deepseek = true;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
setKeysConfigured(configured);
|
|
365
|
-
// key 存储位置(默认凭据中心)
|
|
366
|
-
setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
|
|
367
|
-
setState({ status: "ready", writable: result.value.writable });
|
|
368
|
-
// 查询凭据中心里各 key 的配置状态
|
|
369
|
-
try {
|
|
370
|
-
const cred = await bridgeCredentialsStatus();
|
|
371
|
-
if (cred.ok) {
|
|
372
|
-
const cc = {};
|
|
373
|
-
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
|
|
374
|
-
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
375
|
-
if (map[k]) cc[map[k]] = v;
|
|
376
|
-
}
|
|
377
|
-
setCredConfigured(cc);
|
|
378
|
-
}
|
|
379
|
-
} catch {}
|
|
380
|
-
} else {
|
|
381
|
-
setState({ status: "unavailable" });
|
|
382
|
-
}
|
|
383
|
-
} else {
|
|
384
|
-
setState({ status: "unavailable" });
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
setState({ status: "unavailable" });
|
|
388
|
-
}
|
|
389
|
-
}, []);
|
|
390
|
-
|
|
391
|
-
react.useEffect(() => {
|
|
392
|
-
load();
|
|
393
|
-
}, [load]);
|
|
394
|
-
|
|
395
|
-
const select = (value) => {
|
|
396
|
-
setProvider(value);
|
|
397
|
-
setDirty(true);
|
|
398
|
-
setFailed(false);
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
const save = async () => {
|
|
402
|
-
setSaving(true);
|
|
403
|
-
setFailed(false);
|
|
404
|
-
try {
|
|
405
|
-
// key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
|
|
406
|
-
const keyFields = [
|
|
407
|
-
["exaApiKey", exaKey],
|
|
408
|
-
["tavilyApiKey", tavilyKey],
|
|
409
|
-
["keenableApiKey", keenableKey],
|
|
410
|
-
["perplexityApiKey", perplexityKey],
|
|
411
|
-
["deepseekApiKey", deepseekKey],
|
|
412
|
-
];
|
|
413
|
-
let credFailed = false;
|
|
414
|
-
if (keyStorage === "credentials") {
|
|
415
|
-
for (const [field, value] of keyFields) {
|
|
416
|
-
if (!value.trim()) continue;
|
|
417
|
-
const r = await bridgeCredentialsSet(field, value.trim());
|
|
418
|
-
if (!r.ok) credFailed = true;
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
const ops = [{ op: "set", path: ["provider"], value: provider }];
|
|
422
|
-
ops.push({ op: "set", path: ["lang"], value: lang });
|
|
423
|
-
ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
|
|
424
|
-
ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
|
|
425
|
-
ops.push({ op: "set", path: ["bingMarket"], value: bingMarket });
|
|
426
|
-
if (keyStorage !== "credentials") {
|
|
427
|
-
// settings 模式:key 仍写 settings.yaml(旧行为)
|
|
428
|
-
for (const [field, value] of keyFields) {
|
|
429
|
-
if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
433
|
-
ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
|
|
434
|
-
const result = await bridgeMutate({ ns: NS, ops });
|
|
435
|
-
if (result.ok && !credFailed) {
|
|
436
|
-
setDirty(false);
|
|
437
|
-
setProvider(result.value.value.provider ?? provider);
|
|
438
|
-
setFailed(false);
|
|
439
|
-
load();
|
|
440
|
-
} else {
|
|
441
|
-
setFailed(true);
|
|
442
|
-
}
|
|
443
|
-
} catch {
|
|
444
|
-
setFailed(true);
|
|
445
|
-
} finally {
|
|
446
|
-
setSaving(false);
|
|
447
|
-
}
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
const discard = () => {
|
|
451
|
-
load();
|
|
452
|
-
setDirty(false);
|
|
453
|
-
setFailed(false);
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const runTest = async () => {
|
|
457
|
-
setTesting(true);
|
|
458
|
-
setTestResult(null);
|
|
459
|
-
setFailed(false);
|
|
460
|
-
try {
|
|
461
|
-
const result = await bridgeRawSearch({
|
|
462
|
-
query: "DeepSeek Harness",
|
|
463
|
-
maxResults: 2,
|
|
464
|
-
engine: provider,
|
|
465
|
-
});
|
|
466
|
-
if (result.ok) {
|
|
467
|
-
const sources = result.value.sources ?? [];
|
|
468
|
-
setTestResult({
|
|
469
|
-
ok: true,
|
|
470
|
-
count: sources.length,
|
|
471
|
-
engine: result.value.provider ?? provider,
|
|
472
|
-
content: result.value.content ?? "",
|
|
473
|
-
sample: sources[0]?.title ?? "",
|
|
474
|
-
});
|
|
475
|
-
} else {
|
|
476
|
-
setTestResult({ ok: false, error: result.message ?? "unknown error" });
|
|
477
|
-
}
|
|
478
|
-
} catch {
|
|
479
|
-
setTestResult({ ok: false, error: "request failed" });
|
|
480
|
-
} finally {
|
|
481
|
-
setTesting(false);
|
|
482
|
-
}
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const runCheckUpdate = async () => {
|
|
486
|
-
setCheckingUpdate(true);
|
|
487
|
-
setUpdateInfo(null);
|
|
488
|
-
setFailed(false);
|
|
489
|
-
try {
|
|
490
|
-
const result = await bridgeCheckUpdate();
|
|
491
|
-
if (result.ok) {
|
|
492
|
-
setUpdateInfo({ ok: true, ...result.value });
|
|
493
|
-
} else {
|
|
494
|
-
setUpdateInfo({ ok: false });
|
|
495
|
-
}
|
|
496
|
-
} catch {
|
|
497
|
-
setUpdateInfo({ ok: false });
|
|
498
|
-
} finally {
|
|
499
|
-
setCheckingUpdate(false);
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
const runUpdate = async () => {
|
|
504
|
-
setUpgrading(true);
|
|
505
|
-
setUpdateInfo(null);
|
|
506
|
-
setFailed(false);
|
|
507
|
-
try {
|
|
508
|
-
const response = await fetch(`${BRIDGE_PREFIX}/update`, {
|
|
509
|
-
method: "POST",
|
|
510
|
-
headers: { "content-type": "application/json" },
|
|
511
|
-
body: "{}",
|
|
512
|
-
});
|
|
513
|
-
const result = await response.json();
|
|
514
|
-
if (result.ok) {
|
|
515
|
-
setUpdateInfo({ ok: true, hasUpdate: false, upgraded: true, message: result.value.message, latest: result.value.latest });
|
|
516
|
-
} else {
|
|
517
|
-
setUpdateInfo({ ok: false, upgradeFailed: result.message ?? "upgrade failed" });
|
|
518
|
-
}
|
|
519
|
-
} catch {
|
|
520
|
-
setUpdateInfo({ ok: false, upgradeFailed: "request failed" });
|
|
521
|
-
} finally {
|
|
522
|
-
setUpgrading(false);
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
if (state.status === "loading") return null;
|
|
527
|
-
const ready = state.status === "ready";
|
|
528
|
-
const t = tt(lang);
|
|
529
|
-
const title = "Free Search";
|
|
530
|
-
const description = t.description;
|
|
531
|
-
const currentEngine = ENGINES.find((e) => e.id === provider) ?? ENGINES[0];
|
|
532
|
-
const badgeClass =
|
|
533
|
-
currentEngine.badge === "FREE" ? "dshfs-badge dshfs-badgeFree" : "dshfs-badge dshfs-badgeKey";
|
|
534
|
-
|
|
535
|
-
const toggleLang = () => {
|
|
536
|
-
setLang((prev) => (prev === "en" ? "zh" : "en"));
|
|
537
|
-
setDirty(true);
|
|
538
|
-
setFailed(false);
|
|
539
|
-
};
|
|
540
|
-
|
|
541
|
-
return react_jsx_runtime.jsx("li", {
|
|
542
|
-
className: open ? "dshfs-card dshfs-cardOpen" : "dshfs-card",
|
|
543
|
-
children: [
|
|
544
|
-
react_jsx_runtime.jsx("button", {
|
|
545
|
-
type: "button",
|
|
546
|
-
className: "dshfs-header",
|
|
547
|
-
"aria-expanded": open,
|
|
548
|
-
onClick: () => setOpen(!open),
|
|
549
|
-
children: [
|
|
550
|
-
react_jsx_runtime.jsx("span", { className: "dshfs-headText", children: [
|
|
551
|
-
react_jsx_runtime.jsx("span", { className: "dshfs-name", children: title }),
|
|
552
|
-
react_jsx_runtime.jsx("span", { className: "dshfs-description", children: description }),
|
|
553
|
-
] }),
|
|
554
|
-
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
555
|
-
dirty ? react_jsx_runtime.jsx("span", { className: "dshfs-pending", children: t.unsaved }) : null,
|
|
556
|
-
react_jsx_runtime.jsx("button", {
|
|
557
|
-
type: "button",
|
|
558
|
-
className: "dshfs-btn dshfs-langToggle",
|
|
559
|
-
onClick: (e) => {
|
|
560
|
-
e.stopPropagation();
|
|
561
|
-
toggleLang();
|
|
562
|
-
},
|
|
563
|
-
children: t.toggleLang,
|
|
564
|
-
}),
|
|
565
|
-
react_jsx_runtime.jsx("span", {
|
|
566
|
-
className: open ? "dshfs-chevron dshfs-chevronOpen" : "dshfs-chevron",
|
|
567
|
-
children: "▾",
|
|
568
|
-
}),
|
|
569
|
-
],
|
|
570
|
-
}),
|
|
571
|
-
open
|
|
572
|
-
? react_jsx_runtime.jsx("div", {
|
|
573
|
-
className: "dshfs-body",
|
|
574
|
-
children: [
|
|
575
|
-
react_jsx_runtime.jsx("div", {
|
|
576
|
-
className: "dshfs-field",
|
|
577
|
-
children: [
|
|
578
|
-
react_jsx_runtime.jsx("div", {
|
|
579
|
-
className: "dshfs-label",
|
|
580
|
-
children: [
|
|
581
|
-
t.searchEngine,
|
|
582
|
-
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
583
|
-
],
|
|
584
|
-
}),
|
|
585
|
-
react_jsx_runtime.jsx("select", {
|
|
586
|
-
className: "dshfs-select",
|
|
587
|
-
value: provider,
|
|
588
|
-
style: { colorScheme: selectColorScheme },
|
|
589
|
-
disabled: !ready || saving,
|
|
590
|
-
onChange: (e) => select(e.target.value),
|
|
591
|
-
children: ENGINES.map((engine) =>
|
|
592
|
-
react_jsx_runtime.jsx("option", { value: engine.id, children: `${engine.label} (${engine.badge})` }, engine.id)
|
|
593
|
-
),
|
|
594
|
-
}),
|
|
595
|
-
currentEngine.link
|
|
596
|
-
? react_jsx_runtime.jsx("a", {
|
|
597
|
-
className: "dshfs-link",
|
|
598
|
-
href: currentEngine.link,
|
|
599
|
-
target: "_blank",
|
|
600
|
-
rel: "noopener noreferrer",
|
|
601
|
-
children:
|
|
602
|
-
currentEngine.badge === "FREE"
|
|
603
|
-
? t.visit
|
|
604
|
-
: t.getKey,
|
|
605
|
-
})
|
|
606
|
-
: null,
|
|
607
|
-
react_jsx_runtime.jsx("p", {
|
|
608
|
-
className: "dshfs-hint",
|
|
609
|
-
children: t.engineHint,
|
|
610
|
-
}),
|
|
611
|
-
],
|
|
612
|
-
}),
|
|
613
|
-
react_jsx_runtime.jsx("div", {
|
|
614
|
-
className: "dshfs-field",
|
|
615
|
-
children: [
|
|
616
|
-
react_jsx_runtime.jsx("div", {
|
|
617
|
-
className: "dshfs-label",
|
|
618
|
-
children: t.safeSearchLabel,
|
|
619
|
-
}),
|
|
620
|
-
react_jsx_runtime.jsx("select", {
|
|
621
|
-
className: "dshfs-select",
|
|
622
|
-
value: safeSearch,
|
|
623
|
-
style: { colorScheme: selectColorScheme },
|
|
624
|
-
disabled: !ready || saving,
|
|
625
|
-
onChange: (e) => setSafeSearch(e.target.value),
|
|
626
|
-
children: [
|
|
627
|
-
react_jsx_runtime.jsx("option", { value: "off", children: t.safeSearchOff }, "off"),
|
|
628
|
-
react_jsx_runtime.jsx("option", { value: "moderate", children: t.safeSearchModerate }, "moderate"),
|
|
629
|
-
react_jsx_runtime.jsx("option", { value: "strict", children: t.safeSearchStrict }, "strict"),
|
|
630
|
-
],
|
|
631
|
-
}),
|
|
632
|
-
react_jsx_runtime.jsx("p", {
|
|
633
|
-
className: "dshfs-hint",
|
|
634
|
-
children: t.safeSearchHint,
|
|
635
|
-
}),
|
|
636
|
-
],
|
|
637
|
-
}),
|
|
638
|
-
react_jsx_runtime.jsx("div", {
|
|
639
|
-
className: "dshfs-field",
|
|
640
|
-
children: [
|
|
641
|
-
react_jsx_runtime.jsx("div", {
|
|
642
|
-
className: "dshfs-label",
|
|
643
|
-
children: t.bingMarketLabel,
|
|
644
|
-
}),
|
|
645
|
-
react_jsx_runtime.jsx("select", {
|
|
646
|
-
className: "dshfs-select",
|
|
647
|
-
value: bingMarket,
|
|
648
|
-
style: { colorScheme: selectColorScheme },
|
|
649
|
-
disabled: !ready || saving,
|
|
650
|
-
onChange: (e) => setBingMarket(e.target.value),
|
|
651
|
-
children: [
|
|
652
|
-
react_jsx_runtime.jsx("option", { value: "zh-CN", children: t.marketZhCN }, "zh-CN"),
|
|
653
|
-
react_jsx_runtime.jsx("option", { value: "zh-TW", children: t.marketZhTW }, "zh-TW"),
|
|
654
|
-
react_jsx_runtime.jsx("option", { value: "en-US", children: t.marketEnUS }, "en-US"),
|
|
655
|
-
react_jsx_runtime.jsx("option", { value: "en-GB", children: t.marketEnGB }, "en-GB"),
|
|
656
|
-
react_jsx_runtime.jsx("option", { value: "ru-RU", children: t.marketRuRU }, "ru-RU"),
|
|
657
|
-
react_jsx_runtime.jsx("option", { value: "ja-JP", children: t.marketJaJP }, "ja-JP"),
|
|
658
|
-
react_jsx_runtime.jsx("option", { value: "de-DE", children: t.marketDeDE }, "de-DE"),
|
|
659
|
-
react_jsx_runtime.jsx("option", { value: "fr-FR", children: t.marketFrFR }, "fr-FR"),
|
|
660
|
-
react_jsx_runtime.jsx("option", { value: "es-ES", children: t.marketEsES }, "es-ES"),
|
|
661
|
-
react_jsx_runtime.jsx("option", { value: "ko-KR", children: t.marketKoKR }, "ko-KR"),
|
|
662
|
-
],
|
|
663
|
-
}),
|
|
664
|
-
react_jsx_runtime.jsx("p", {
|
|
665
|
-
className: "dshfs-hint",
|
|
666
|
-
children: t.bingMarketHint,
|
|
667
|
-
}),
|
|
668
|
-
],
|
|
669
|
-
}),
|
|
670
|
-
react_jsx_runtime.jsx("div", {
|
|
671
|
-
className: "dshfs-field",
|
|
672
|
-
children: [
|
|
673
|
-
react_jsx_runtime.jsx("div", {
|
|
674
|
-
className: "dshfs-label",
|
|
675
|
-
children: t.apiKeys,
|
|
676
|
-
}),
|
|
677
|
-
react_jsx_runtime.jsx("input", {
|
|
678
|
-
className: "dshfs-input",
|
|
679
|
-
type: "password",
|
|
680
|
-
placeholder: t.exaPh(keysConfigured.exa),
|
|
681
|
-
value: exaKey,
|
|
682
|
-
disabled: !ready || saving,
|
|
683
|
-
onChange: (e) => {
|
|
684
|
-
setExaKey(e.target.value);
|
|
685
|
-
setDirty(true);
|
|
686
|
-
setFailed(false);
|
|
687
|
-
},
|
|
688
|
-
}),
|
|
689
|
-
react_jsx_runtime.jsx("input", {
|
|
690
|
-
className: "dshfs-input",
|
|
691
|
-
type: "password",
|
|
692
|
-
placeholder: t.tavilyPh(keysConfigured.tavily),
|
|
693
|
-
value: tavilyKey,
|
|
694
|
-
disabled: !ready || saving,
|
|
695
|
-
onChange: (e) => {
|
|
696
|
-
setTavilyKey(e.target.value);
|
|
697
|
-
setDirty(true);
|
|
698
|
-
setFailed(false);
|
|
699
|
-
},
|
|
700
|
-
}),
|
|
701
|
-
react_jsx_runtime.jsx("input", {
|
|
702
|
-
className: "dshfs-input",
|
|
703
|
-
type: "password",
|
|
704
|
-
placeholder: t.keenablePh(keysConfigured.keenable),
|
|
705
|
-
value: keenableKey,
|
|
706
|
-
disabled: !ready || saving,
|
|
707
|
-
onChange: (e) => {
|
|
708
|
-
setKeenableKey(e.target.value);
|
|
709
|
-
setDirty(true);
|
|
710
|
-
setFailed(false);
|
|
711
|
-
},
|
|
712
|
-
}),
|
|
713
|
-
react_jsx_runtime.jsx("input", {
|
|
714
|
-
className: "dshfs-input",
|
|
715
|
-
type: "password",
|
|
716
|
-
placeholder: t.perplexityPh(keysConfigured.perplexity),
|
|
717
|
-
value: perplexityKey,
|
|
718
|
-
disabled: !ready || saving,
|
|
719
|
-
onChange: (e) => {
|
|
720
|
-
setPerplexityKey(e.target.value);
|
|
721
|
-
setDirty(true);
|
|
722
|
-
setFailed(false);
|
|
723
|
-
},
|
|
724
|
-
}),
|
|
725
|
-
react_jsx_runtime.jsx("input", {
|
|
726
|
-
className: "dshfs-input",
|
|
727
|
-
type: "password",
|
|
728
|
-
placeholder: t.deepseekPh(keysConfigured.deepseek),
|
|
729
|
-
value: deepseekKey,
|
|
730
|
-
disabled: !ready || saving,
|
|
731
|
-
onChange: (e) => {
|
|
732
|
-
setDeepseekKey(e.target.value);
|
|
733
|
-
setDirty(true);
|
|
734
|
-
setFailed(false);
|
|
735
|
-
},
|
|
736
|
-
}),
|
|
737
|
-
react_jsx_runtime.jsx("p", {
|
|
738
|
-
className: "dshfs-hint",
|
|
739
|
-
children: t.keysHint,
|
|
740
|
-
}),
|
|
741
|
-
react_jsx_runtime.jsx("div", {
|
|
742
|
-
className: "dshfs-fieldRow",
|
|
743
|
-
children: [
|
|
744
|
-
react_jsx_runtime.jsx("label", {
|
|
745
|
-
className: "dshfs-label",
|
|
746
|
-
children: t.keyStorage,
|
|
747
|
-
}),
|
|
748
|
-
react_jsx_runtime.jsx("select", {
|
|
749
|
-
className: "dshfs-select dshfs-keyStorage",
|
|
750
|
-
value: keyStorage,
|
|
751
|
-
style: { colorScheme: selectColorScheme },
|
|
752
|
-
disabled: !ready || saving,
|
|
753
|
-
onChange: (e) => {
|
|
754
|
-
setKeyStorage(e.target.value);
|
|
755
|
-
setDirty(true);
|
|
756
|
-
setFailed(false);
|
|
757
|
-
},
|
|
758
|
-
children: [
|
|
759
|
-
react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
|
|
760
|
-
react_jsx_runtime.jsx("option", { value: "settings", children: t.keyStorageSettings }),
|
|
761
|
-
],
|
|
762
|
-
}),
|
|
763
|
-
],
|
|
764
|
-
}),
|
|
765
|
-
react_jsx_runtime.jsx("p", {
|
|
766
|
-
className: "dshfs-hint",
|
|
767
|
-
children: keyStorage === "credentials" ? t.keyStorageCredHint(credConfigured) : t.keyStorageSettingsHint,
|
|
768
|
-
}),
|
|
769
|
-
],
|
|
770
|
-
}),
|
|
771
|
-
react_jsx_runtime.jsx("div", {
|
|
772
|
-
className: "dshfs-field",
|
|
773
|
-
children: [
|
|
774
|
-
react_jsx_runtime.jsx("div", {
|
|
775
|
-
className: "dshfs-label",
|
|
776
|
-
children: t.platformSearch,
|
|
777
|
-
}),
|
|
778
|
-
react_jsx_runtime.jsx("div", {
|
|
779
|
-
className: "dshfs-platforms",
|
|
780
|
-
children: [
|
|
781
|
-
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
|
|
782
|
-
["hn", "Hacker News"], ["stackoverflow", "Stack Overflow"], ["wikipedia", "Wikipedia"], ["npm", "npm"],
|
|
783
|
-
].map(([id, label]) =>
|
|
784
|
-
react_jsx_runtime.jsx("label", {
|
|
785
|
-
className: "dshfs-platform",
|
|
786
|
-
children: [
|
|
787
|
-
react_jsx_runtime.jsx("input", {
|
|
788
|
-
type: "checkbox",
|
|
789
|
-
checked: platforms.includes(id),
|
|
790
|
-
disabled: !ready || saving,
|
|
791
|
-
onChange: (e) => {
|
|
792
|
-
setPlatforms((prev) =>
|
|
793
|
-
e.target.checked ? [...prev, id] : prev.filter((p) => p !== id)
|
|
794
|
-
);
|
|
795
|
-
setDirty(true);
|
|
796
|
-
setFailed(false);
|
|
797
|
-
},
|
|
798
|
-
}),
|
|
799
|
-
label,
|
|
800
|
-
],
|
|
801
|
-
}, id)
|
|
802
|
-
),
|
|
803
|
-
}),
|
|
804
|
-
react_jsx_runtime.jsx("p", {
|
|
805
|
-
className: "dshfs-hint",
|
|
806
|
-
children: t.platformHint,
|
|
807
|
-
}),
|
|
808
|
-
],
|
|
809
|
-
}),
|
|
810
|
-
react_jsx_runtime.jsx("div", {
|
|
811
|
-
className: "dshfs-field",
|
|
812
|
-
children: [
|
|
813
|
-
react_jsx_runtime.jsx("div", {
|
|
814
|
-
className: "dshfs-label",
|
|
815
|
-
children: t.cacheTtl,
|
|
816
|
-
}),
|
|
817
|
-
react_jsx_runtime.jsx("input", {
|
|
818
|
-
className: "dshfs-input dshfs-ttl",
|
|
819
|
-
type: "number",
|
|
820
|
-
min: 0,
|
|
821
|
-
max: 5,
|
|
822
|
-
step: 1,
|
|
823
|
-
value: cacheTtl,
|
|
824
|
-
disabled: !ready || saving,
|
|
825
|
-
onChange: (e) => {
|
|
826
|
-
setCacheTtl(Number(e.target.value));
|
|
827
|
-
setDirty(true);
|
|
828
|
-
setFailed(false);
|
|
829
|
-
},
|
|
830
|
-
}),
|
|
831
|
-
react_jsx_runtime.jsx("p", {
|
|
832
|
-
className: "dshfs-hint",
|
|
833
|
-
children: t.cacheTtlHint,
|
|
834
|
-
}),
|
|
835
|
-
],
|
|
836
|
-
}),
|
|
837
|
-
react_jsx_runtime.jsx("div", {
|
|
838
|
-
className: "dshfs-resultRow",
|
|
839
|
-
children: [
|
|
840
|
-
failed ? react_jsx_runtime.jsx("span", { className: "dshfs-failed", children: t.saveFailed }) : null,
|
|
841
|
-
testResult
|
|
842
|
-
? react_jsx_runtime.jsx("span", {
|
|
843
|
-
className: testResult.ok ? "dshfs-testOk" : "dshfs-failed",
|
|
844
|
-
children: testResult.ok
|
|
845
|
-
? t.testOk(testResult)
|
|
846
|
-
: t.testFail(testResult.error),
|
|
847
|
-
})
|
|
848
|
-
: null,
|
|
849
|
-
],
|
|
850
|
-
}),
|
|
851
|
-
!ready
|
|
852
|
-
? react_jsx_runtime.jsx("p", {
|
|
853
|
-
className: "dshfs-hint",
|
|
854
|
-
children: t.unavailable,
|
|
855
|
-
})
|
|
856
|
-
: null,
|
|
857
|
-
react_jsx_runtime.jsx("div", {
|
|
858
|
-
className: "dshfs-footer",
|
|
859
|
-
children: [
|
|
860
|
-
react_jsx_runtime.jsx("div", {
|
|
861
|
-
className: "dshfs-footerLeft",
|
|
862
|
-
children: [
|
|
863
|
-
react_jsx_runtime.jsx("span", { className: "dshfs-version", children: "v" + PLUGIN_VERSION }),
|
|
864
|
-
react_jsx_runtime.jsx("button", {
|
|
865
|
-
className: "dshfs-btn",
|
|
866
|
-
type: "button",
|
|
867
|
-
onClick: runCheckUpdate,
|
|
868
|
-
disabled: checkingUpdate || saving || !ready,
|
|
869
|
-
children: checkingUpdate ? t.checkingUpdate : t.checkUpdate,
|
|
870
|
-
}),
|
|
871
|
-
updateInfo && updateInfo.ok
|
|
872
|
-
? updateInfo.upgraded
|
|
873
|
-
? react_jsx_runtime.jsx("span", {
|
|
874
|
-
className: "dshfs-updateOk",
|
|
875
|
-
children: t.upgradeDone(updateInfo.latest),
|
|
876
|
-
})
|
|
877
|
-
: updateInfo.hasUpdate
|
|
878
|
-
? react_jsx_runtime.jsx("span", {
|
|
879
|
-
className: "dshfs-update",
|
|
880
|
-
children: [
|
|
881
|
-
t.updateAvailable(updateInfo.current, updateInfo.latest),
|
|
882
|
-
" ",
|
|
883
|
-
updateInfo.installable
|
|
884
|
-
? react_jsx_runtime.jsx("button", {
|
|
885
|
-
className: "dshfs-btn dshfs-upgrade",
|
|
886
|
-
type: "button",
|
|
887
|
-
onClick: runUpdate,
|
|
888
|
-
disabled: upgrading || saving || !ready,
|
|
889
|
-
children: upgrading ? t.upgrading : t.upgrade,
|
|
890
|
-
})
|
|
891
|
-
: react_jsx_runtime.jsx("a", {
|
|
892
|
-
className: "dshfs-link",
|
|
893
|
-
href: updateInfo.updateUrl,
|
|
894
|
-
target: "_blank",
|
|
895
|
-
rel: "noopener noreferrer",
|
|
896
|
-
children: t.updateView,
|
|
897
|
-
}),
|
|
898
|
-
updateInfo.installable ? null : " " + t.upgradeLinkMode,
|
|
899
|
-
],
|
|
900
|
-
})
|
|
901
|
-
: react_jsx_runtime.jsx("span", {
|
|
902
|
-
className: "dshfs-updateOk",
|
|
903
|
-
children: t.updateLatest(updateInfo.current),
|
|
904
|
-
})
|
|
905
|
-
: updateInfo && !updateInfo.ok
|
|
906
|
-
? react_jsx_runtime.jsx("span", {
|
|
907
|
-
className: "dshfs-failed",
|
|
908
|
-
children: updateInfo.upgradeFailed ? t.upgradeFailed(updateInfo.upgradeFailed) : t.updateCheckFailed,
|
|
909
|
-
})
|
|
910
|
-
: null,
|
|
911
|
-
],
|
|
912
|
-
}),
|
|
913
|
-
react_jsx_runtime.jsx("div", {
|
|
914
|
-
className: "dshfs-footerRight",
|
|
915
|
-
children: [
|
|
916
|
-
react_jsx_runtime.jsx("button", {
|
|
917
|
-
className: "dshfs-btn",
|
|
918
|
-
type: "button",
|
|
919
|
-
onClick: runTest,
|
|
920
|
-
disabled: testing || saving || !ready,
|
|
921
|
-
children: testing ? t.testing : t.testEngine,
|
|
922
|
-
}),
|
|
923
|
-
react_jsx_runtime.jsx("button", {
|
|
924
|
-
className: "dshfs-btn",
|
|
925
|
-
type: "button",
|
|
926
|
-
onClick: () => {
|
|
927
|
-
setProvider("bing");
|
|
928
|
-
setDirty(true);
|
|
929
|
-
setFailed(false);
|
|
930
|
-
},
|
|
931
|
-
disabled: saving || !ready || provider === "bing",
|
|
932
|
-
children: t.useBing,
|
|
933
|
-
}),
|
|
934
|
-
react_jsx_runtime.jsx("button", {
|
|
935
|
-
className: "dshfs-btn",
|
|
936
|
-
type: "button",
|
|
937
|
-
onClick: discard,
|
|
938
|
-
disabled: saving || !dirty,
|
|
939
|
-
children: t.discard,
|
|
940
|
-
}),
|
|
941
|
-
react_jsx_runtime.jsx("button", {
|
|
942
|
-
className: "dshfs-btn dshfs-save",
|
|
943
|
-
type: "button",
|
|
944
|
-
onClick: save,
|
|
945
|
-
disabled: saving || !dirty || !ready,
|
|
946
|
-
children: saving ? t.saving : t.save,
|
|
947
|
-
}),
|
|
948
|
-
],
|
|
949
|
-
}),
|
|
950
|
-
],
|
|
951
|
-
})
|
|
952
|
-
],
|
|
953
|
-
})
|
|
954
|
-
: null,
|
|
955
|
-
],
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
|
|
959
|
-
const inject = ["slots", "commandUi"];
|
|
960
|
-
|
|
961
|
-
function apply(ctx) {
|
|
962
|
-
// 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
|
|
963
|
-
// 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
|
|
964
|
-
ctx.slots.inject("settings.plugin.item", () =>
|
|
965
|
-
ctx.slots.register(
|
|
966
|
-
{
|
|
967
|
-
name: "settings.plugin.item",
|
|
968
|
-
key: "free-search",
|
|
969
|
-
id: "dsh-free-search",
|
|
970
|
-
order: 120,
|
|
971
|
-
inject: () => ({}),
|
|
972
|
-
},
|
|
973
|
-
FreeSearchCard
|
|
974
|
-
)
|
|
975
|
-
);
|
|
976
|
-
// /free-search-engine 弹出式命令:输入 "/" 选中后弹出引擎列表,点选即切换。
|
|
977
|
-
// 等效于设置页切换引擎+保存;命令只改 provider 配置,搜索仍走回退链。
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
},
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
+
// 下拉选项列表配色锁死:不随皮肤变量变化(皮肤只影响 select 框体本身)
|
|
34
|
+
// color-scheme 让浏览器原生下拉按主题渲染;option 显式固定底色/文字色兜底
|
|
35
|
+
".dshfs-select{color-scheme:light dark}",
|
|
36
|
+
".dshfs-select option,.dshfs-select optgroup{background-color:#ffffff;color:#1f2328}",
|
|
37
|
+
"@media (prefers-color-scheme:dark){.dshfs-select{color-scheme:dark}.dshfs-select option,.dshfs-select optgroup{background-color:#1e1f24;color:#e8e8ea}}",
|
|
38
|
+
".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%}",
|
|
39
|
+
".dshfs-ttl{width:88px}",
|
|
40
|
+
".dshfs-fieldRow{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
41
|
+
".dshfs-keyStorage{width:auto;min-width:180px}",
|
|
42
|
+
".dshfs-input:hover:not(:disabled){border-color:var(--dsw-alias-label-dimmed)}",
|
|
43
|
+
".dshfs-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
44
|
+
".dshfs-input:disabled{opacity:.6;cursor:default}",
|
|
45
|
+
".dshfs-hint{color:var(--dsw-alias-label-secondary);margin:0;font-size:12px}",
|
|
46
|
+
".dshfs-platforms{display:flex;gap:10px;flex-wrap:wrap}",
|
|
47
|
+
".dshfs-platform{display:flex;align-items:center;gap:5px;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}",
|
|
48
|
+
".dshfs-platform input{accent-color:var(--dsw-alias-state-business-primary)}",
|
|
49
|
+
".dshfs-link{color:var(--dsw-alias-state-business-primary);font-size:12px;text-decoration:none;align-self:flex-start;padding:2px 0}",
|
|
50
|
+
".dshfs-link:hover{text-decoration:underline}",
|
|
51
|
+
".dshfs-btn{font:inherit;cursor:pointer;border-radius:6px;padding:5px 12px;font-size:13px;transition:background-color .13s,border-color .13s,color .13s}",
|
|
52
|
+
".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)}",
|
|
53
|
+
".dshfs-save:hover:not(:disabled){border-color:var(--dsw-alias-button-info-hover);background:var(--dsw-alias-button-info-hover)}",
|
|
54
|
+
".dshfs-save:disabled{opacity:.5;cursor:default}",
|
|
55
|
+
".dshfs-upgrade{border:1px solid rgba(80,200,120,.4);background:rgba(80,200,120,.15);color:#7ddb9c}",
|
|
56
|
+
".dshfs-upgrade:hover:not(:disabled){background:rgba(80,200,120,.28)}",
|
|
57
|
+
".dshfs-upgrade:disabled{opacity:.5;cursor:default}",
|
|
58
|
+
".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}",
|
|
59
|
+
".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}",
|
|
60
|
+
".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}",
|
|
61
|
+
".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}",
|
|
62
|
+
".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}",
|
|
63
|
+
".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}",
|
|
64
|
+
".dshfs-updateLine{display:flex;align-items:center;gap:8px;flex-wrap:wrap}",
|
|
65
|
+
".dshfs-version{color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
66
|
+
].join("");
|
|
67
|
+
const tagId = "dsh-free-search/card.css";
|
|
68
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
|
|
69
|
+
const tag = document.createElement("style");
|
|
70
|
+
tag.dataset.plugin = "dsh-free-search";
|
|
71
|
+
tag.dataset.pluginCss = tagId;
|
|
72
|
+
tag.textContent = css;
|
|
73
|
+
document.head.appendChild(tag);
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
|
|
77
|
+
const BRIDGE_PREFIX = "/api/dsh-free-search-settings";
|
|
78
|
+
const NS = "free-search";
|
|
79
|
+
const I18N = {
|
|
80
|
+
zh: {
|
|
81
|
+
description: "免费搜索 —— 无需 API key(Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
|
|
82
|
+
unsaved: "未保存",
|
|
83
|
+
searchEngine: "搜索引擎",
|
|
84
|
+
visit: "访问官网 →",
|
|
85
|
+
getKey: "获取 API Key →",
|
|
86
|
+
engineHint: "Bing 是最稳定的免费引擎。DuckDuckGo 在共享 IP 上可能限流。API KEY 引擎需在下方填写凭据。",
|
|
87
|
+
apiKeys: "API 密钥(可选)",
|
|
88
|
+
exaPh: (c) => c ? "Exa API 密钥(已配置)" : "Exa API 密钥(可选,不填也可免费使用)",
|
|
89
|
+
tavilyPh: (c) => c ? "Tavily API 密钥(已配置)" : "Tavily API 密钥(可选,不填也可免费使用)",
|
|
90
|
+
keenablePh: (c) => c ? "Keenable API 密钥(已配置)" : "Keenable API 密钥(可选,不填也可免费使用)",
|
|
91
|
+
perplexityPh: (c) => c ? "Perplexity API 密钥(已配置)" : "Perplexity API 密钥(pplx-...)",
|
|
92
|
+
deepseekPh: (c) => c ? "DeepSeek API 密钥(已配置)" : "DeepSeek API 密钥(sk-...)",
|
|
93
|
+
keysHint: "密钥读取优先级:.credentials.yaml 凭据中心 > 这里 > 环境变量。推荐把 key 写进凭据中心(与官方 LLM 一致,一处管理)。",
|
|
94
|
+
keyStorage: "Key 存储位置",
|
|
95
|
+
keyStorageCred: "凭据中心(推荐)",
|
|
96
|
+
keyStorageSettings: "设置页(兼容)",
|
|
97
|
+
keyStorageCredHint: (c) => `保存后写入 ~/.dsh/.credentials.yaml(最高优先级)。当前已配置:${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "无"}`,
|
|
98
|
+
keyStorageSettingsHint: "保存后写入 settings.yaml(向后兼容路径,优先级低于凭据中心)。",
|
|
99
|
+
platformSearch: "平台搜索(platform_search 工具)",
|
|
100
|
+
platformHint: "为 agent 的 platform_search 工具启用平台。禁用的平台会被跳过。",
|
|
101
|
+
cacheTtl: "结果缓存时长(分钟)",
|
|
102
|
+
cacheTtlHint: "0 关闭缓存,最长 5 分钟。缩短可加快时效,延长可防限流、省额度。",
|
|
103
|
+
unavailable: "设置不可用 —— free-search 桥接未暴露。",
|
|
104
|
+
saveFailed: "保存失败",
|
|
105
|
+
testing: "测试中…",
|
|
106
|
+
testEngine: "测试引擎",
|
|
107
|
+
useBing: "恢复 Bing 默认",
|
|
108
|
+
discard: "撤销",
|
|
109
|
+
saving: "保存中…",
|
|
110
|
+
save: "保存",
|
|
111
|
+
testOk: (r) => `✓ ${r.count} 条结果(引擎: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · 例如 "${r.sample.slice(0, 40)}"` : ""}`,
|
|
112
|
+
testFail: (e) => `✗ ${e}`,
|
|
113
|
+
toggleLang: "EN",
|
|
114
|
+
checkUpdate: "检查更新",
|
|
115
|
+
checkingUpdate: "检查中…",
|
|
116
|
+
updateAvailable: (c, l) => `发现新版本 v${l}(当前 v${c})`,
|
|
117
|
+
updateLatest: (c) => `已是最新版本 v${c}`,
|
|
118
|
+
updateCheckFailed: "检查更新失败(无法访问 npm registry)",
|
|
119
|
+
updateView: "查看 →",
|
|
120
|
+
upgrade: "升级",
|
|
121
|
+
upgrading: "升级中…",
|
|
122
|
+
upgradeLinkMode: "(本地开发模式,升级请用 git pull)",
|
|
123
|
+
upgradeDone: (l) => `升级到 v${l} 完成,重启 dsh 后生效`,
|
|
124
|
+
upgradeFailed: (m) => `升级失败:${m}`,
|
|
125
|
+
safeSearchLabel: "安全搜索过滤 (adlt)",
|
|
126
|
+
safeSearchOff: "关闭 —— 引擎默认(不加参数)",
|
|
127
|
+
safeSearchModerate: "中等 —— Bing 默认",
|
|
128
|
+
safeSearchStrict: "严格",
|
|
129
|
+
safeSearchHint: "作用于 Bing(adlt)、DuckDuckGo HTML / Lite(adlt 等级)。如遇引擎自带过滤可在此调整。",
|
|
130
|
+
bingMarketLabel: "Bing 市场(本地化结果)",
|
|
131
|
+
bingMarketHint: "Bing 的 mkt + Accept-Language 跟随此设置。例如 ru-RU 会让西里尔查询返回俄语结果。",
|
|
132
|
+
marketZhCN: "zh-CN —— 中国大陆(默认)",
|
|
133
|
+
marketZhTW: "zh-TW —— 台湾",
|
|
134
|
+
marketEnUS: "en-US —— 美国",
|
|
135
|
+
marketEnGB: "en-GB —— 英国",
|
|
136
|
+
marketRuRU: "ru-RU —— 俄罗斯",
|
|
137
|
+
marketJaJP: "ja-JP —— 日本",
|
|
138
|
+
marketDeDE: "de-DE —— 德国",
|
|
139
|
+
marketFrFR: "fr-FR —— 法国",
|
|
140
|
+
marketEsES: "es-ES —— 西班牙",
|
|
141
|
+
marketKoKR: "ko-KR —— 韩国",
|
|
142
|
+
},
|
|
143
|
+
en: {
|
|
144
|
+
description: "Free web search — no API key needed (Bing / DuckDuckGo / AnySearch / Exa / Tavily / Keenable)",
|
|
145
|
+
unsaved: "unsaved",
|
|
146
|
+
searchEngine: "Search engine",
|
|
147
|
+
visit: "Visit website →",
|
|
148
|
+
getKey: "Get API Key →",
|
|
149
|
+
engineHint: "Bing is the most stable FREE engine. DuckDuckGo may rate-limit on shared IPs. API KEY engines need credentials below.",
|
|
150
|
+
apiKeys: "API keys (optional)",
|
|
151
|
+
exaPh: (c) => c ? "Exa API key (configured)" : "Exa API key (optional, free without)",
|
|
152
|
+
tavilyPh: (c) => c ? "Tavily API key (configured)" : "Tavily API key (optional, free without)",
|
|
153
|
+
keenablePh: (c) => c ? "Keenable API key (configured)" : "Keenable API key (optional, free without)",
|
|
154
|
+
perplexityPh: (c) => c ? "Perplexity API key (configured)" : "Perplexity API key (pplx-...)",
|
|
155
|
+
deepseekPh: (c) => c ? "DeepSeek API key (configured)" : "DeepSeek API key (sk-...)",
|
|
156
|
+
keysHint: "Key resolution: .credentials.yaml credential center > here > environment variables. Recommended: store keys in the credential center (same as official LLM providers, one place for all).",
|
|
157
|
+
keyStorage: "Key storage",
|
|
158
|
+
keyStorageCred: "Credential center (recommended)",
|
|
159
|
+
keyStorageSettings: "Settings page (legacy)",
|
|
160
|
+
keyStorageCredHint: (c) => `Saved to ~/.dsh/.credentials.yaml (highest priority). Currently configured: ${["exa", "tavily", "keenable", "perplexity", "deepseek"].filter((k) => c[k]).map((k) => k.toUpperCase()).join(", ") || "none"}`,
|
|
161
|
+
keyStorageSettingsHint: "Saved to settings.yaml (backward-compatible path; lower priority than the credential center).",
|
|
162
|
+
platformSearch: "Platform search (platform_search tool)",
|
|
163
|
+
platformHint: "Enable platforms for the agent's platform_search tool. Disabled platforms are skipped.",
|
|
164
|
+
cacheTtl: "Result cache TTL (minutes)",
|
|
165
|
+
cacheTtlHint: "0 disables caching, max 5 minutes. Lower = fresher results, higher = less rate-limiting / fewer credits used.",
|
|
166
|
+
unavailable: "Settings unavailable — the free-search bridge is not exposed.",
|
|
167
|
+
saveFailed: "save failed",
|
|
168
|
+
testing: "Testing…",
|
|
169
|
+
testEngine: "Test engine",
|
|
170
|
+
useBing: "Use Bing default",
|
|
171
|
+
discard: "Discard",
|
|
172
|
+
saving: "Saving…",
|
|
173
|
+
save: "Save",
|
|
174
|
+
testOk: (r) => `✓ ${r.count} results (engine: ${r.engine})${r.content ? ` — ${r.content}` : ""}${r.sample ? ` · e.g. "${r.sample.slice(0, 40)}"` : ""}`,
|
|
175
|
+
testFail: (e) => `✗ ${e}`,
|
|
176
|
+
toggleLang: "中文",
|
|
177
|
+
checkUpdate: "Check update",
|
|
178
|
+
checkingUpdate: "Checking…",
|
|
179
|
+
updateAvailable: (c, l) => `New version v${l} available (current v${c})`,
|
|
180
|
+
updateLatest: (c) => `You're on the latest version v${c}`,
|
|
181
|
+
updateCheckFailed: "Update check failed (cannot reach npm registry)",
|
|
182
|
+
updateView: "View →",
|
|
183
|
+
upgrade: "Upgrade",
|
|
184
|
+
upgrading: "Upgrading…",
|
|
185
|
+
upgradeLinkMode: "(local dev install - use git pull to update)",
|
|
186
|
+
upgradeDone: (l) => `Upgraded to v${l} - restart dsh to apply`,
|
|
187
|
+
upgradeFailed: (m) => `Upgrade failed: ${m}`,
|
|
188
|
+
safeSearchLabel: "Safe search filter (adlt)",
|
|
189
|
+
safeSearchOff: "Off - engine default (no filtering)",
|
|
190
|
+
safeSearchModerate: "Moderate - Bing default",
|
|
191
|
+
safeSearchStrict: "Strict",
|
|
192
|
+
safeSearchHint: "Applies to bing (adlt), ddg, ddg-lite (adlt degree). If you see the engine's own filtering, adjust here.",
|
|
193
|
+
bingMarketLabel: "Bing market (localized results)",
|
|
194
|
+
bingMarketHint: "Bing's mkt + Accept-Language follow this. e.g. ru-RU returns Russian results for Cyrillic queries.",
|
|
195
|
+
marketZhCN: "zh-CN - China (default)",
|
|
196
|
+
marketZhTW: "zh-TW - Taiwan",
|
|
197
|
+
marketEnUS: "en-US - United States",
|
|
198
|
+
marketEnGB: "en-GB - United Kingdom",
|
|
199
|
+
marketRuRU: "ru-RU - Russia",
|
|
200
|
+
marketJaJP: "ja-JP - Japan",
|
|
201
|
+
marketDeDE: "de-DE - Germany",
|
|
202
|
+
marketFrFR: "fr-FR - France",
|
|
203
|
+
marketEsES: "es-ES - Spain",
|
|
204
|
+
marketKoKR: "ko-KR - Korea",
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
const tt = (lang) => I18N[lang === "en" ? "en" : "zh"];
|
|
208
|
+
// 当前插件版本(与 lib/index.js 的 PLUGIN_VERSION 保持一致)
|
|
209
|
+
const PLUGIN_VERSION = "0.4.25";
|
|
210
|
+
const ENGINES = [
|
|
211
|
+
{ id: "ddg", label: "DuckDuckGo · HTML", badge: "FREE", link: "https://duckduckgo.com" },
|
|
212
|
+
{ id: "ddg-lite", label: "DuckDuckGo · Lite", badge: "FREE", link: "https://duckduckgo.com" },
|
|
213
|
+
{ id: "bing", label: "Bing", badge: "FREE", link: "https://www.bing.com" },
|
|
214
|
+
{ id: "anysearch", label: "AnySearch · AI", badge: "FREE", link: "https://anysearch.com" },
|
|
215
|
+
{ id: "searxng", label: "SearXNG · 元搜索", badge: "FREE", link: "https://github.com/searxng/searxng" },
|
|
216
|
+
{ id: "exa", label: "Exa", badge: "FREE", link: "https://dashboard.exa.ai/api-keys" },
|
|
217
|
+
{ id: "tavily", label: "Tavily", badge: "FREE", link: "https://app.tavily.com/home" },
|
|
218
|
+
{ id: "keenable", label: "Keenable", badge: "FREE", link: "https://keenable.ai/login" },
|
|
219
|
+
{ id: "perplexity", label: "Perplexity", badge: "API KEY", link: "https://www.perplexity.ai/settings/api" },
|
|
220
|
+
{ id: "deepseek-official", label: "DeepSeek Official", badge: "API KEY", link: "https://platform.deepseek.com/api_keys" },
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
async function bridgeDescribe() {
|
|
224
|
+
const response = await fetch(`${BRIDGE_PREFIX}/describe`, {
|
|
225
|
+
method: "POST",
|
|
226
|
+
headers: { "content-type": "application/json" },
|
|
227
|
+
body: "{}",
|
|
228
|
+
});
|
|
229
|
+
return response.json();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function bridgeMutate(payload) {
|
|
233
|
+
const response = await fetch(`${BRIDGE_PREFIX}/mutate`, {
|
|
234
|
+
method: "POST",
|
|
235
|
+
headers: { "content-type": "application/json" },
|
|
236
|
+
body: JSON.stringify(payload),
|
|
237
|
+
});
|
|
238
|
+
return response.json();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function bridgeRawSearch(payload) {
|
|
242
|
+
const response = await fetch(`${BRIDGE_PREFIX}/raw-search`, {
|
|
243
|
+
method: "POST",
|
|
244
|
+
headers: { "content-type": "application/json" },
|
|
245
|
+
body: JSON.stringify(payload),
|
|
246
|
+
});
|
|
247
|
+
return response.json();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function bridgeCheckUpdate() {
|
|
251
|
+
const response = await fetch(`${BRIDGE_PREFIX}/check-update`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: { "content-type": "application/json" },
|
|
254
|
+
body: "{}",
|
|
255
|
+
});
|
|
256
|
+
return response.json();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function bridgeCredentialsStatus() {
|
|
260
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-status`, {
|
|
261
|
+
method: "POST",
|
|
262
|
+
headers: { "content-type": "application/json" },
|
|
263
|
+
body: "{}",
|
|
264
|
+
});
|
|
265
|
+
return response.json();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function bridgeCredentialsSet(key, value) {
|
|
269
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-set`, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "content-type": "application/json" },
|
|
272
|
+
body: JSON.stringify({ key, value }),
|
|
273
|
+
});
|
|
274
|
+
return response.json();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function bridgeCredentialsUnset(key) {
|
|
278
|
+
const response = await fetch(`${BRIDGE_PREFIX}/credentials-unset`, {
|
|
279
|
+
method: "POST",
|
|
280
|
+
headers: { "content-type": "application/json" },
|
|
281
|
+
body: JSON.stringify({ key }),
|
|
282
|
+
});
|
|
283
|
+
return response.json();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function FreeSearchCard(props) {
|
|
287
|
+
// 检测应用主题深浅(读 body 的 --dsw-alias-bg-base 变量亮度),用于锁定原生下拉配色
|
|
288
|
+
const isDarkScheme = react.useMemo(() => {
|
|
289
|
+
try {
|
|
290
|
+
const root = document.body || document.documentElement;
|
|
291
|
+
const bg = getComputedStyle(root).getPropertyValue("--dsw-alias-bg-base").trim();
|
|
292
|
+
const m = bg.match(/(\d+)\s*[, ]\s*(\d+)\s*[, ]\s*(\d+)/);
|
|
293
|
+
if (m) {
|
|
294
|
+
const l = 0.299 * Number(m[1]) + 0.587 * Number(m[2]) + 0.114 * Number(m[3]);
|
|
295
|
+
return l < 128;
|
|
296
|
+
}
|
|
297
|
+
if (/^#([0-9a-f]{3,8})/i.test(bg)) {
|
|
298
|
+
const hex = bg.slice(1);
|
|
299
|
+
const h = hex.length <= 4 ? hex.replace(/./g, (c) => c + c) : hex;
|
|
300
|
+
const r = parseInt(h.slice(0, 2), 16), g = parseInt(h.slice(2, 4), 16), b = parseInt(h.slice(4, 6), 16);
|
|
301
|
+
return 0.299 * r + 0.587 * g + 0.114 * b < 128;
|
|
302
|
+
}
|
|
303
|
+
} catch {}
|
|
304
|
+
return typeof matchMedia === "function" ? matchMedia("(prefers-color-scheme: dark)").matches : false;
|
|
305
|
+
}, []);
|
|
306
|
+
const selectColorScheme = isDarkScheme ? "dark" : "light";
|
|
307
|
+
const [open, setOpen] = react.useState(false);
|
|
308
|
+
const [state, setState] = react.useState({ status: "loading" });
|
|
309
|
+
const [provider, setProvider] = react.useState("bing");
|
|
310
|
+
const [safeSearch, setSafeSearch] = react.useState("off");
|
|
311
|
+
const [bingMarket, setBingMarket] = react.useState("zh-CN");
|
|
312
|
+
const [exaKey, setExaKey] = react.useState("");
|
|
313
|
+
const [tavilyKey, setTavilyKey] = react.useState("");
|
|
314
|
+
const [keenableKey, setKeenableKey] = react.useState("");
|
|
315
|
+
const [perplexityKey, setPerplexityKey] = react.useState("");
|
|
316
|
+
const [deepseekKey, setDeepseekKey] = react.useState("");
|
|
317
|
+
const [platforms, setPlatforms] = react.useState(["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
318
|
+
const [cacheTtl, setCacheTtl] = react.useState(5);
|
|
319
|
+
const [keysConfigured, setKeysConfigured] = react.useState({});
|
|
320
|
+
// key 存储位置:credentials(凭据中心,默认)| settings(设置页,兼容旧行为)
|
|
321
|
+
const [keyStorage, setKeyStorage] = react.useState("credentials");
|
|
322
|
+
// 凭据中心里已配置的 key(describe 不返回,需单独查)
|
|
323
|
+
const [credConfigured, setCredConfigured] = react.useState({});
|
|
324
|
+
const [lang, setLang] = react.useState("zh");
|
|
325
|
+
const [dirty, setDirty] = react.useState(false);
|
|
326
|
+
const [saving, setSaving] = react.useState(false);
|
|
327
|
+
const [failed, setFailed] = react.useState(false);
|
|
328
|
+
const [testing, setTesting] = react.useState(false);
|
|
329
|
+
const [testResult, setTestResult] = react.useState(null);
|
|
330
|
+
const [checkingUpdate, setCheckingUpdate] = react.useState(false);
|
|
331
|
+
const [upgrading, setUpgrading] = react.useState(false);
|
|
332
|
+
const [updateInfo, setUpdateInfo] = react.useState(null);
|
|
333
|
+
|
|
334
|
+
const load = react.useCallback(async () => {
|
|
335
|
+
try {
|
|
336
|
+
const result = await bridgeDescribe();
|
|
337
|
+
if (result.ok) {
|
|
338
|
+
const view = result.value.namespaces.find((n) => n.ns === NS);
|
|
339
|
+
if (view) {
|
|
340
|
+
const v = view.value ?? {};
|
|
341
|
+
setProvider(v.provider ?? "ddg");
|
|
342
|
+
setSafeSearch(v.safeSearch === "strict" || v.safeSearch === "moderate" ? v.safeSearch : "off");
|
|
343
|
+
setBingMarket(v.bingMarket === undefined ? "zh-CN" : v.bingMarket);
|
|
344
|
+
setLang(v.lang === "en" ? "en" : "zh");
|
|
345
|
+
setExaKey(v.exaApiKey ?? "");
|
|
346
|
+
setTavilyKey(v.tavilyApiKey ?? "");
|
|
347
|
+
setKeenableKey(v.keenableApiKey ?? "");
|
|
348
|
+
setPerplexityKey(v.perplexityApiKey ?? "");
|
|
349
|
+
setDeepseekKey(v.deepseekApiKey ?? "");
|
|
350
|
+
setPlatforms(Array.isArray(v.platforms) && v.platforms.length > 0 ? v.platforms : ["github", "v2ex", "bilibili", "reddit", "hn", "stackoverflow", "wikipedia", "npm"]);
|
|
351
|
+
setCacheTtl(v.cacheTtl === undefined ? 5 : Math.min(Math.max(Number(v.cacheTtl) ?? 5, 0), 5));
|
|
352
|
+
// secrets 字段标记哪些 key 已配置(值被脱敏,仅显示"已配置")
|
|
353
|
+
const configured = {};
|
|
354
|
+
for (const secret of view.secrets ?? []) {
|
|
355
|
+
if (secret.set) {
|
|
356
|
+
const path = secret.path.join(".");
|
|
357
|
+
if (path === "exaApiKey") configured.exa = true;
|
|
358
|
+
if (path === "tavilyApiKey") configured.tavily = true;
|
|
359
|
+
if (path === "keenableApiKey") configured.keenable = true;
|
|
360
|
+
if (path === "perplexityApiKey") configured.perplexity = true;
|
|
361
|
+
if (path === "deepseekApiKey") configured.deepseek = true;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
setKeysConfigured(configured);
|
|
365
|
+
// key 存储位置(默认凭据中心)
|
|
366
|
+
setKeyStorage(v.keyStorage === "settings" ? "settings" : "credentials");
|
|
367
|
+
setState({ status: "ready", writable: result.value.writable });
|
|
368
|
+
// 查询凭据中心里各 key 的配置状态
|
|
369
|
+
try {
|
|
370
|
+
const cred = await bridgeCredentialsStatus();
|
|
371
|
+
if (cred.ok) {
|
|
372
|
+
const cc = {};
|
|
373
|
+
const map = { exaApiKey: "exa", tavilyApiKey: "tavily", keenableApiKey: "keenable", perplexityApiKey: "perplexity", deepseekApiKey: "deepseek" };
|
|
374
|
+
for (const [k, v] of Object.entries(cred.value.configured ?? {})) {
|
|
375
|
+
if (map[k]) cc[map[k]] = v;
|
|
376
|
+
}
|
|
377
|
+
setCredConfigured(cc);
|
|
378
|
+
}
|
|
379
|
+
} catch {}
|
|
380
|
+
} else {
|
|
381
|
+
setState({ status: "unavailable" });
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
setState({ status: "unavailable" });
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
setState({ status: "unavailable" });
|
|
388
|
+
}
|
|
389
|
+
}, []);
|
|
390
|
+
|
|
391
|
+
react.useEffect(() => {
|
|
392
|
+
load();
|
|
393
|
+
}, [load]);
|
|
394
|
+
|
|
395
|
+
const select = (value) => {
|
|
396
|
+
setProvider(value);
|
|
397
|
+
setDirty(true);
|
|
398
|
+
setFailed(false);
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
const save = async () => {
|
|
402
|
+
setSaving(true);
|
|
403
|
+
setFailed(false);
|
|
404
|
+
try {
|
|
405
|
+
// key 存储分流:凭据中心(默认)走 credentials-set;设置页走 settings mutate(兼容)
|
|
406
|
+
const keyFields = [
|
|
407
|
+
["exaApiKey", exaKey],
|
|
408
|
+
["tavilyApiKey", tavilyKey],
|
|
409
|
+
["keenableApiKey", keenableKey],
|
|
410
|
+
["perplexityApiKey", perplexityKey],
|
|
411
|
+
["deepseekApiKey", deepseekKey],
|
|
412
|
+
];
|
|
413
|
+
let credFailed = false;
|
|
414
|
+
if (keyStorage === "credentials") {
|
|
415
|
+
for (const [field, value] of keyFields) {
|
|
416
|
+
if (!value.trim()) continue;
|
|
417
|
+
const r = await bridgeCredentialsSet(field, value.trim());
|
|
418
|
+
if (!r.ok) credFailed = true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const ops = [{ op: "set", path: ["provider"], value: provider }];
|
|
422
|
+
ops.push({ op: "set", path: ["lang"], value: lang });
|
|
423
|
+
ops.push({ op: "set", path: ["keyStorage"], value: keyStorage });
|
|
424
|
+
ops.push({ op: "set", path: ["safeSearch"], value: safeSearch });
|
|
425
|
+
ops.push({ op: "set", path: ["bingMarket"], value: bingMarket });
|
|
426
|
+
if (keyStorage !== "credentials") {
|
|
427
|
+
// settings 模式:key 仍写 settings.yaml(旧行为)
|
|
428
|
+
for (const [field, value] of keyFields) {
|
|
429
|
+
if (value.trim()) ops.push({ op: "set", path: [field], value: value.trim() });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
ops.push({ op: "set", path: ["platforms"], value: platforms });
|
|
433
|
+
ops.push({ op: "set", path: ["cacheTtl"], value: Math.min(Math.max(Number(cacheTtl) ?? 5, 0), 5) });
|
|
434
|
+
const result = await bridgeMutate({ ns: NS, ops });
|
|
435
|
+
if (result.ok && !credFailed) {
|
|
436
|
+
setDirty(false);
|
|
437
|
+
setProvider(result.value.value.provider ?? provider);
|
|
438
|
+
setFailed(false);
|
|
439
|
+
load();
|
|
440
|
+
} else {
|
|
441
|
+
setFailed(true);
|
|
442
|
+
}
|
|
443
|
+
} catch {
|
|
444
|
+
setFailed(true);
|
|
445
|
+
} finally {
|
|
446
|
+
setSaving(false);
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const discard = () => {
|
|
451
|
+
load();
|
|
452
|
+
setDirty(false);
|
|
453
|
+
setFailed(false);
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
const runTest = async () => {
|
|
457
|
+
setTesting(true);
|
|
458
|
+
setTestResult(null);
|
|
459
|
+
setFailed(false);
|
|
460
|
+
try {
|
|
461
|
+
const result = await bridgeRawSearch({
|
|
462
|
+
query: "DeepSeek Harness",
|
|
463
|
+
maxResults: 2,
|
|
464
|
+
engine: provider,
|
|
465
|
+
});
|
|
466
|
+
if (result.ok) {
|
|
467
|
+
const sources = result.value.sources ?? [];
|
|
468
|
+
setTestResult({
|
|
469
|
+
ok: true,
|
|
470
|
+
count: sources.length,
|
|
471
|
+
engine: result.value.provider ?? provider,
|
|
472
|
+
content: result.value.content ?? "",
|
|
473
|
+
sample: sources[0]?.title ?? "",
|
|
474
|
+
});
|
|
475
|
+
} else {
|
|
476
|
+
setTestResult({ ok: false, error: result.message ?? "unknown error" });
|
|
477
|
+
}
|
|
478
|
+
} catch {
|
|
479
|
+
setTestResult({ ok: false, error: "request failed" });
|
|
480
|
+
} finally {
|
|
481
|
+
setTesting(false);
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
const runCheckUpdate = async () => {
|
|
486
|
+
setCheckingUpdate(true);
|
|
487
|
+
setUpdateInfo(null);
|
|
488
|
+
setFailed(false);
|
|
489
|
+
try {
|
|
490
|
+
const result = await bridgeCheckUpdate();
|
|
491
|
+
if (result.ok) {
|
|
492
|
+
setUpdateInfo({ ok: true, ...result.value });
|
|
493
|
+
} else {
|
|
494
|
+
setUpdateInfo({ ok: false });
|
|
495
|
+
}
|
|
496
|
+
} catch {
|
|
497
|
+
setUpdateInfo({ ok: false });
|
|
498
|
+
} finally {
|
|
499
|
+
setCheckingUpdate(false);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const runUpdate = async () => {
|
|
504
|
+
setUpgrading(true);
|
|
505
|
+
setUpdateInfo(null);
|
|
506
|
+
setFailed(false);
|
|
507
|
+
try {
|
|
508
|
+
const response = await fetch(`${BRIDGE_PREFIX}/update`, {
|
|
509
|
+
method: "POST",
|
|
510
|
+
headers: { "content-type": "application/json" },
|
|
511
|
+
body: "{}",
|
|
512
|
+
});
|
|
513
|
+
const result = await response.json();
|
|
514
|
+
if (result.ok) {
|
|
515
|
+
setUpdateInfo({ ok: true, hasUpdate: false, upgraded: true, message: result.value.message, latest: result.value.latest });
|
|
516
|
+
} else {
|
|
517
|
+
setUpdateInfo({ ok: false, upgradeFailed: result.message ?? "upgrade failed" });
|
|
518
|
+
}
|
|
519
|
+
} catch {
|
|
520
|
+
setUpdateInfo({ ok: false, upgradeFailed: "request failed" });
|
|
521
|
+
} finally {
|
|
522
|
+
setUpgrading(false);
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
if (state.status === "loading") return null;
|
|
527
|
+
const ready = state.status === "ready";
|
|
528
|
+
const t = tt(lang);
|
|
529
|
+
const title = "Free Search";
|
|
530
|
+
const description = t.description;
|
|
531
|
+
const currentEngine = ENGINES.find((e) => e.id === provider) ?? ENGINES[0];
|
|
532
|
+
const badgeClass =
|
|
533
|
+
currentEngine.badge === "FREE" ? "dshfs-badge dshfs-badgeFree" : "dshfs-badge dshfs-badgeKey";
|
|
534
|
+
|
|
535
|
+
const toggleLang = () => {
|
|
536
|
+
setLang((prev) => (prev === "en" ? "zh" : "en"));
|
|
537
|
+
setDirty(true);
|
|
538
|
+
setFailed(false);
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
return react_jsx_runtime.jsx("li", {
|
|
542
|
+
className: open ? "dshfs-card dshfs-cardOpen" : "dshfs-card",
|
|
543
|
+
children: [
|
|
544
|
+
react_jsx_runtime.jsx("button", {
|
|
545
|
+
type: "button",
|
|
546
|
+
className: "dshfs-header",
|
|
547
|
+
"aria-expanded": open,
|
|
548
|
+
onClick: () => setOpen(!open),
|
|
549
|
+
children: [
|
|
550
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-headText", children: [
|
|
551
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-name", children: title }),
|
|
552
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-description", children: description }),
|
|
553
|
+
] }),
|
|
554
|
+
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
555
|
+
dirty ? react_jsx_runtime.jsx("span", { className: "dshfs-pending", children: t.unsaved }) : null,
|
|
556
|
+
react_jsx_runtime.jsx("button", {
|
|
557
|
+
type: "button",
|
|
558
|
+
className: "dshfs-btn dshfs-langToggle",
|
|
559
|
+
onClick: (e) => {
|
|
560
|
+
e.stopPropagation();
|
|
561
|
+
toggleLang();
|
|
562
|
+
},
|
|
563
|
+
children: t.toggleLang,
|
|
564
|
+
}),
|
|
565
|
+
react_jsx_runtime.jsx("span", {
|
|
566
|
+
className: open ? "dshfs-chevron dshfs-chevronOpen" : "dshfs-chevron",
|
|
567
|
+
children: "▾",
|
|
568
|
+
}),
|
|
569
|
+
],
|
|
570
|
+
}),
|
|
571
|
+
open
|
|
572
|
+
? react_jsx_runtime.jsx("div", {
|
|
573
|
+
className: "dshfs-body",
|
|
574
|
+
children: [
|
|
575
|
+
react_jsx_runtime.jsx("div", {
|
|
576
|
+
className: "dshfs-field",
|
|
577
|
+
children: [
|
|
578
|
+
react_jsx_runtime.jsx("div", {
|
|
579
|
+
className: "dshfs-label",
|
|
580
|
+
children: [
|
|
581
|
+
t.searchEngine,
|
|
582
|
+
react_jsx_runtime.jsx("span", { className: badgeClass, children: currentEngine.badge }),
|
|
583
|
+
],
|
|
584
|
+
}),
|
|
585
|
+
react_jsx_runtime.jsx("select", {
|
|
586
|
+
className: "dshfs-select",
|
|
587
|
+
value: provider,
|
|
588
|
+
style: { colorScheme: selectColorScheme },
|
|
589
|
+
disabled: !ready || saving,
|
|
590
|
+
onChange: (e) => select(e.target.value),
|
|
591
|
+
children: ENGINES.map((engine) =>
|
|
592
|
+
react_jsx_runtime.jsx("option", { value: engine.id, children: `${engine.label} (${engine.badge})` }, engine.id)
|
|
593
|
+
),
|
|
594
|
+
}),
|
|
595
|
+
currentEngine.link
|
|
596
|
+
? react_jsx_runtime.jsx("a", {
|
|
597
|
+
className: "dshfs-link",
|
|
598
|
+
href: currentEngine.link,
|
|
599
|
+
target: "_blank",
|
|
600
|
+
rel: "noopener noreferrer",
|
|
601
|
+
children:
|
|
602
|
+
currentEngine.badge === "FREE"
|
|
603
|
+
? t.visit
|
|
604
|
+
: t.getKey,
|
|
605
|
+
})
|
|
606
|
+
: null,
|
|
607
|
+
react_jsx_runtime.jsx("p", {
|
|
608
|
+
className: "dshfs-hint",
|
|
609
|
+
children: t.engineHint,
|
|
610
|
+
}),
|
|
611
|
+
],
|
|
612
|
+
}),
|
|
613
|
+
react_jsx_runtime.jsx("div", {
|
|
614
|
+
className: "dshfs-field",
|
|
615
|
+
children: [
|
|
616
|
+
react_jsx_runtime.jsx("div", {
|
|
617
|
+
className: "dshfs-label",
|
|
618
|
+
children: t.safeSearchLabel,
|
|
619
|
+
}),
|
|
620
|
+
react_jsx_runtime.jsx("select", {
|
|
621
|
+
className: "dshfs-select",
|
|
622
|
+
value: safeSearch,
|
|
623
|
+
style: { colorScheme: selectColorScheme },
|
|
624
|
+
disabled: !ready || saving,
|
|
625
|
+
onChange: (e) => setSafeSearch(e.target.value),
|
|
626
|
+
children: [
|
|
627
|
+
react_jsx_runtime.jsx("option", { value: "off", children: t.safeSearchOff }, "off"),
|
|
628
|
+
react_jsx_runtime.jsx("option", { value: "moderate", children: t.safeSearchModerate }, "moderate"),
|
|
629
|
+
react_jsx_runtime.jsx("option", { value: "strict", children: t.safeSearchStrict }, "strict"),
|
|
630
|
+
],
|
|
631
|
+
}),
|
|
632
|
+
react_jsx_runtime.jsx("p", {
|
|
633
|
+
className: "dshfs-hint",
|
|
634
|
+
children: t.safeSearchHint,
|
|
635
|
+
}),
|
|
636
|
+
],
|
|
637
|
+
}),
|
|
638
|
+
react_jsx_runtime.jsx("div", {
|
|
639
|
+
className: "dshfs-field",
|
|
640
|
+
children: [
|
|
641
|
+
react_jsx_runtime.jsx("div", {
|
|
642
|
+
className: "dshfs-label",
|
|
643
|
+
children: t.bingMarketLabel,
|
|
644
|
+
}),
|
|
645
|
+
react_jsx_runtime.jsx("select", {
|
|
646
|
+
className: "dshfs-select",
|
|
647
|
+
value: bingMarket,
|
|
648
|
+
style: { colorScheme: selectColorScheme },
|
|
649
|
+
disabled: !ready || saving,
|
|
650
|
+
onChange: (e) => setBingMarket(e.target.value),
|
|
651
|
+
children: [
|
|
652
|
+
react_jsx_runtime.jsx("option", { value: "zh-CN", children: t.marketZhCN }, "zh-CN"),
|
|
653
|
+
react_jsx_runtime.jsx("option", { value: "zh-TW", children: t.marketZhTW }, "zh-TW"),
|
|
654
|
+
react_jsx_runtime.jsx("option", { value: "en-US", children: t.marketEnUS }, "en-US"),
|
|
655
|
+
react_jsx_runtime.jsx("option", { value: "en-GB", children: t.marketEnGB }, "en-GB"),
|
|
656
|
+
react_jsx_runtime.jsx("option", { value: "ru-RU", children: t.marketRuRU }, "ru-RU"),
|
|
657
|
+
react_jsx_runtime.jsx("option", { value: "ja-JP", children: t.marketJaJP }, "ja-JP"),
|
|
658
|
+
react_jsx_runtime.jsx("option", { value: "de-DE", children: t.marketDeDE }, "de-DE"),
|
|
659
|
+
react_jsx_runtime.jsx("option", { value: "fr-FR", children: t.marketFrFR }, "fr-FR"),
|
|
660
|
+
react_jsx_runtime.jsx("option", { value: "es-ES", children: t.marketEsES }, "es-ES"),
|
|
661
|
+
react_jsx_runtime.jsx("option", { value: "ko-KR", children: t.marketKoKR }, "ko-KR"),
|
|
662
|
+
],
|
|
663
|
+
}),
|
|
664
|
+
react_jsx_runtime.jsx("p", {
|
|
665
|
+
className: "dshfs-hint",
|
|
666
|
+
children: t.bingMarketHint,
|
|
667
|
+
}),
|
|
668
|
+
],
|
|
669
|
+
}),
|
|
670
|
+
react_jsx_runtime.jsx("div", {
|
|
671
|
+
className: "dshfs-field",
|
|
672
|
+
children: [
|
|
673
|
+
react_jsx_runtime.jsx("div", {
|
|
674
|
+
className: "dshfs-label",
|
|
675
|
+
children: t.apiKeys,
|
|
676
|
+
}),
|
|
677
|
+
react_jsx_runtime.jsx("input", {
|
|
678
|
+
className: "dshfs-input",
|
|
679
|
+
type: "password",
|
|
680
|
+
placeholder: t.exaPh(keysConfigured.exa),
|
|
681
|
+
value: exaKey,
|
|
682
|
+
disabled: !ready || saving,
|
|
683
|
+
onChange: (e) => {
|
|
684
|
+
setExaKey(e.target.value);
|
|
685
|
+
setDirty(true);
|
|
686
|
+
setFailed(false);
|
|
687
|
+
},
|
|
688
|
+
}),
|
|
689
|
+
react_jsx_runtime.jsx("input", {
|
|
690
|
+
className: "dshfs-input",
|
|
691
|
+
type: "password",
|
|
692
|
+
placeholder: t.tavilyPh(keysConfigured.tavily),
|
|
693
|
+
value: tavilyKey,
|
|
694
|
+
disabled: !ready || saving,
|
|
695
|
+
onChange: (e) => {
|
|
696
|
+
setTavilyKey(e.target.value);
|
|
697
|
+
setDirty(true);
|
|
698
|
+
setFailed(false);
|
|
699
|
+
},
|
|
700
|
+
}),
|
|
701
|
+
react_jsx_runtime.jsx("input", {
|
|
702
|
+
className: "dshfs-input",
|
|
703
|
+
type: "password",
|
|
704
|
+
placeholder: t.keenablePh(keysConfigured.keenable),
|
|
705
|
+
value: keenableKey,
|
|
706
|
+
disabled: !ready || saving,
|
|
707
|
+
onChange: (e) => {
|
|
708
|
+
setKeenableKey(e.target.value);
|
|
709
|
+
setDirty(true);
|
|
710
|
+
setFailed(false);
|
|
711
|
+
},
|
|
712
|
+
}),
|
|
713
|
+
react_jsx_runtime.jsx("input", {
|
|
714
|
+
className: "dshfs-input",
|
|
715
|
+
type: "password",
|
|
716
|
+
placeholder: t.perplexityPh(keysConfigured.perplexity),
|
|
717
|
+
value: perplexityKey,
|
|
718
|
+
disabled: !ready || saving,
|
|
719
|
+
onChange: (e) => {
|
|
720
|
+
setPerplexityKey(e.target.value);
|
|
721
|
+
setDirty(true);
|
|
722
|
+
setFailed(false);
|
|
723
|
+
},
|
|
724
|
+
}),
|
|
725
|
+
react_jsx_runtime.jsx("input", {
|
|
726
|
+
className: "dshfs-input",
|
|
727
|
+
type: "password",
|
|
728
|
+
placeholder: t.deepseekPh(keysConfigured.deepseek),
|
|
729
|
+
value: deepseekKey,
|
|
730
|
+
disabled: !ready || saving,
|
|
731
|
+
onChange: (e) => {
|
|
732
|
+
setDeepseekKey(e.target.value);
|
|
733
|
+
setDirty(true);
|
|
734
|
+
setFailed(false);
|
|
735
|
+
},
|
|
736
|
+
}),
|
|
737
|
+
react_jsx_runtime.jsx("p", {
|
|
738
|
+
className: "dshfs-hint",
|
|
739
|
+
children: t.keysHint,
|
|
740
|
+
}),
|
|
741
|
+
react_jsx_runtime.jsx("div", {
|
|
742
|
+
className: "dshfs-fieldRow",
|
|
743
|
+
children: [
|
|
744
|
+
react_jsx_runtime.jsx("label", {
|
|
745
|
+
className: "dshfs-label",
|
|
746
|
+
children: t.keyStorage,
|
|
747
|
+
}),
|
|
748
|
+
react_jsx_runtime.jsx("select", {
|
|
749
|
+
className: "dshfs-select dshfs-keyStorage",
|
|
750
|
+
value: keyStorage,
|
|
751
|
+
style: { colorScheme: selectColorScheme },
|
|
752
|
+
disabled: !ready || saving,
|
|
753
|
+
onChange: (e) => {
|
|
754
|
+
setKeyStorage(e.target.value);
|
|
755
|
+
setDirty(true);
|
|
756
|
+
setFailed(false);
|
|
757
|
+
},
|
|
758
|
+
children: [
|
|
759
|
+
react_jsx_runtime.jsx("option", { value: "credentials", children: t.keyStorageCred }),
|
|
760
|
+
react_jsx_runtime.jsx("option", { value: "settings", children: t.keyStorageSettings }),
|
|
761
|
+
],
|
|
762
|
+
}),
|
|
763
|
+
],
|
|
764
|
+
}),
|
|
765
|
+
react_jsx_runtime.jsx("p", {
|
|
766
|
+
className: "dshfs-hint",
|
|
767
|
+
children: keyStorage === "credentials" ? t.keyStorageCredHint(credConfigured) : t.keyStorageSettingsHint,
|
|
768
|
+
}),
|
|
769
|
+
],
|
|
770
|
+
}),
|
|
771
|
+
react_jsx_runtime.jsx("div", {
|
|
772
|
+
className: "dshfs-field",
|
|
773
|
+
children: [
|
|
774
|
+
react_jsx_runtime.jsx("div", {
|
|
775
|
+
className: "dshfs-label",
|
|
776
|
+
children: t.platformSearch,
|
|
777
|
+
}),
|
|
778
|
+
react_jsx_runtime.jsx("div", {
|
|
779
|
+
className: "dshfs-platforms",
|
|
780
|
+
children: [
|
|
781
|
+
["github", "GitHub"], ["v2ex", "V2EX"], ["bilibili", "Bilibili"], ["reddit", "Reddit"],
|
|
782
|
+
["hn", "Hacker News"], ["stackoverflow", "Stack Overflow"], ["wikipedia", "Wikipedia"], ["npm", "npm"],
|
|
783
|
+
].map(([id, label]) =>
|
|
784
|
+
react_jsx_runtime.jsx("label", {
|
|
785
|
+
className: "dshfs-platform",
|
|
786
|
+
children: [
|
|
787
|
+
react_jsx_runtime.jsx("input", {
|
|
788
|
+
type: "checkbox",
|
|
789
|
+
checked: platforms.includes(id),
|
|
790
|
+
disabled: !ready || saving,
|
|
791
|
+
onChange: (e) => {
|
|
792
|
+
setPlatforms((prev) =>
|
|
793
|
+
e.target.checked ? [...prev, id] : prev.filter((p) => p !== id)
|
|
794
|
+
);
|
|
795
|
+
setDirty(true);
|
|
796
|
+
setFailed(false);
|
|
797
|
+
},
|
|
798
|
+
}),
|
|
799
|
+
label,
|
|
800
|
+
],
|
|
801
|
+
}, id)
|
|
802
|
+
),
|
|
803
|
+
}),
|
|
804
|
+
react_jsx_runtime.jsx("p", {
|
|
805
|
+
className: "dshfs-hint",
|
|
806
|
+
children: t.platformHint,
|
|
807
|
+
}),
|
|
808
|
+
],
|
|
809
|
+
}),
|
|
810
|
+
react_jsx_runtime.jsx("div", {
|
|
811
|
+
className: "dshfs-field",
|
|
812
|
+
children: [
|
|
813
|
+
react_jsx_runtime.jsx("div", {
|
|
814
|
+
className: "dshfs-label",
|
|
815
|
+
children: t.cacheTtl,
|
|
816
|
+
}),
|
|
817
|
+
react_jsx_runtime.jsx("input", {
|
|
818
|
+
className: "dshfs-input dshfs-ttl",
|
|
819
|
+
type: "number",
|
|
820
|
+
min: 0,
|
|
821
|
+
max: 5,
|
|
822
|
+
step: 1,
|
|
823
|
+
value: cacheTtl,
|
|
824
|
+
disabled: !ready || saving,
|
|
825
|
+
onChange: (e) => {
|
|
826
|
+
setCacheTtl(Number(e.target.value));
|
|
827
|
+
setDirty(true);
|
|
828
|
+
setFailed(false);
|
|
829
|
+
},
|
|
830
|
+
}),
|
|
831
|
+
react_jsx_runtime.jsx("p", {
|
|
832
|
+
className: "dshfs-hint",
|
|
833
|
+
children: t.cacheTtlHint,
|
|
834
|
+
}),
|
|
835
|
+
],
|
|
836
|
+
}),
|
|
837
|
+
react_jsx_runtime.jsx("div", {
|
|
838
|
+
className: "dshfs-resultRow",
|
|
839
|
+
children: [
|
|
840
|
+
failed ? react_jsx_runtime.jsx("span", { className: "dshfs-failed", children: t.saveFailed }) : null,
|
|
841
|
+
testResult
|
|
842
|
+
? react_jsx_runtime.jsx("span", {
|
|
843
|
+
className: testResult.ok ? "dshfs-testOk" : "dshfs-failed",
|
|
844
|
+
children: testResult.ok
|
|
845
|
+
? t.testOk(testResult)
|
|
846
|
+
: t.testFail(testResult.error),
|
|
847
|
+
})
|
|
848
|
+
: null,
|
|
849
|
+
],
|
|
850
|
+
}),
|
|
851
|
+
!ready
|
|
852
|
+
? react_jsx_runtime.jsx("p", {
|
|
853
|
+
className: "dshfs-hint",
|
|
854
|
+
children: t.unavailable,
|
|
855
|
+
})
|
|
856
|
+
: null,
|
|
857
|
+
react_jsx_runtime.jsx("div", {
|
|
858
|
+
className: "dshfs-footer",
|
|
859
|
+
children: [
|
|
860
|
+
react_jsx_runtime.jsx("div", {
|
|
861
|
+
className: "dshfs-footerLeft",
|
|
862
|
+
children: [
|
|
863
|
+
react_jsx_runtime.jsx("span", { className: "dshfs-version", children: "v" + PLUGIN_VERSION }),
|
|
864
|
+
react_jsx_runtime.jsx("button", {
|
|
865
|
+
className: "dshfs-btn",
|
|
866
|
+
type: "button",
|
|
867
|
+
onClick: runCheckUpdate,
|
|
868
|
+
disabled: checkingUpdate || saving || !ready,
|
|
869
|
+
children: checkingUpdate ? t.checkingUpdate : t.checkUpdate,
|
|
870
|
+
}),
|
|
871
|
+
updateInfo && updateInfo.ok
|
|
872
|
+
? updateInfo.upgraded
|
|
873
|
+
? react_jsx_runtime.jsx("span", {
|
|
874
|
+
className: "dshfs-updateOk",
|
|
875
|
+
children: t.upgradeDone(updateInfo.latest),
|
|
876
|
+
})
|
|
877
|
+
: updateInfo.hasUpdate
|
|
878
|
+
? react_jsx_runtime.jsx("span", {
|
|
879
|
+
className: "dshfs-update",
|
|
880
|
+
children: [
|
|
881
|
+
t.updateAvailable(updateInfo.current, updateInfo.latest),
|
|
882
|
+
" ",
|
|
883
|
+
updateInfo.installable
|
|
884
|
+
? react_jsx_runtime.jsx("button", {
|
|
885
|
+
className: "dshfs-btn dshfs-upgrade",
|
|
886
|
+
type: "button",
|
|
887
|
+
onClick: runUpdate,
|
|
888
|
+
disabled: upgrading || saving || !ready,
|
|
889
|
+
children: upgrading ? t.upgrading : t.upgrade,
|
|
890
|
+
})
|
|
891
|
+
: react_jsx_runtime.jsx("a", {
|
|
892
|
+
className: "dshfs-link",
|
|
893
|
+
href: updateInfo.updateUrl,
|
|
894
|
+
target: "_blank",
|
|
895
|
+
rel: "noopener noreferrer",
|
|
896
|
+
children: t.updateView,
|
|
897
|
+
}),
|
|
898
|
+
updateInfo.installable ? null : " " + t.upgradeLinkMode,
|
|
899
|
+
],
|
|
900
|
+
})
|
|
901
|
+
: react_jsx_runtime.jsx("span", {
|
|
902
|
+
className: "dshfs-updateOk",
|
|
903
|
+
children: t.updateLatest(updateInfo.current),
|
|
904
|
+
})
|
|
905
|
+
: updateInfo && !updateInfo.ok
|
|
906
|
+
? react_jsx_runtime.jsx("span", {
|
|
907
|
+
className: "dshfs-failed",
|
|
908
|
+
children: updateInfo.upgradeFailed ? t.upgradeFailed(updateInfo.upgradeFailed) : t.updateCheckFailed,
|
|
909
|
+
})
|
|
910
|
+
: null,
|
|
911
|
+
],
|
|
912
|
+
}),
|
|
913
|
+
react_jsx_runtime.jsx("div", {
|
|
914
|
+
className: "dshfs-footerRight",
|
|
915
|
+
children: [
|
|
916
|
+
react_jsx_runtime.jsx("button", {
|
|
917
|
+
className: "dshfs-btn",
|
|
918
|
+
type: "button",
|
|
919
|
+
onClick: runTest,
|
|
920
|
+
disabled: testing || saving || !ready,
|
|
921
|
+
children: testing ? t.testing : t.testEngine,
|
|
922
|
+
}),
|
|
923
|
+
react_jsx_runtime.jsx("button", {
|
|
924
|
+
className: "dshfs-btn",
|
|
925
|
+
type: "button",
|
|
926
|
+
onClick: () => {
|
|
927
|
+
setProvider("bing");
|
|
928
|
+
setDirty(true);
|
|
929
|
+
setFailed(false);
|
|
930
|
+
},
|
|
931
|
+
disabled: saving || !ready || provider === "bing",
|
|
932
|
+
children: t.useBing,
|
|
933
|
+
}),
|
|
934
|
+
react_jsx_runtime.jsx("button", {
|
|
935
|
+
className: "dshfs-btn",
|
|
936
|
+
type: "button",
|
|
937
|
+
onClick: discard,
|
|
938
|
+
disabled: saving || !dirty,
|
|
939
|
+
children: t.discard,
|
|
940
|
+
}),
|
|
941
|
+
react_jsx_runtime.jsx("button", {
|
|
942
|
+
className: "dshfs-btn dshfs-save",
|
|
943
|
+
type: "button",
|
|
944
|
+
onClick: save,
|
|
945
|
+
disabled: saving || !dirty || !ready,
|
|
946
|
+
children: saving ? t.saving : t.save,
|
|
947
|
+
}),
|
|
948
|
+
],
|
|
949
|
+
}),
|
|
950
|
+
],
|
|
951
|
+
})
|
|
952
|
+
],
|
|
953
|
+
})
|
|
954
|
+
: null,
|
|
955
|
+
],
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const inject = ["slots", "commandUi"];
|
|
960
|
+
|
|
961
|
+
function apply(ctx) {
|
|
962
|
+
// 挂官方插槽 settings.plugin.item(设置 → 插件 → 可配置标签页)。
|
|
963
|
+
// 不依赖 dsh-web-ui:配置读写走自建 bridge(/api/dsh-free-search-settings)。
|
|
964
|
+
ctx.slots.inject("settings.plugin.item", () =>
|
|
965
|
+
ctx.slots.register(
|
|
966
|
+
{
|
|
967
|
+
name: "settings.plugin.item",
|
|
968
|
+
key: "free-search",
|
|
969
|
+
id: "dsh-free-search",
|
|
970
|
+
order: 120,
|
|
971
|
+
inject: () => ({}),
|
|
972
|
+
},
|
|
973
|
+
FreeSearchCard
|
|
974
|
+
)
|
|
975
|
+
);
|
|
976
|
+
// /free-search-engine 弹出式命令:输入 "/" 选中后弹出引擎列表,点选即切换。
|
|
977
|
+
// 等效于设置页切换引擎+保存;命令只改 provider 配置,搜索仍走回退链。
|
|
978
|
+
// description 必须传函数:ui-commands 读回的是 contribution.description(),
|
|
979
|
+
// 传字符串会抛 TypeError: contribution.description is not a function;该异常
|
|
980
|
+
// 会让整份 "/" 候选列表一起失败,菜单空白、其他命令也一起点不到。
|
|
981
|
+
ctx.inject(["commandUi"], (sctx) => {
|
|
982
|
+
const command = sctx.get("commandUi");
|
|
983
|
+
sctx.effect(() => {
|
|
984
|
+
const dispose = command.register({
|
|
985
|
+
name: "free-search-engine",
|
|
986
|
+
description: () => "切换搜索引擎 / Switch web search engine",
|
|
987
|
+
available: () => true,
|
|
988
|
+
ui: {
|
|
989
|
+
kind: "popupSelect",
|
|
990
|
+
options: async () => {
|
|
991
|
+
const result = await bridgeDescribe();
|
|
992
|
+
const view = result.ok ? result.value.namespaces.find((n) => n.ns === NS) : undefined;
|
|
993
|
+
const current = view?.value?.provider ?? "bing";
|
|
994
|
+
return ENGINES.map((e) => ({
|
|
995
|
+
id: e.id,
|
|
996
|
+
label: `${e.label}${e.badge === "FREE" ? " · 免费" : " · API Key"}`,
|
|
997
|
+
detail: e.id === current ? (view?.value?.lang === "en" ? "current" : "当前") : undefined,
|
|
998
|
+
active: e.id === current,
|
|
999
|
+
}));
|
|
1000
|
+
},
|
|
1001
|
+
onSelect: async (option) => {
|
|
1002
|
+
await bridgeMutate({ ns: NS, ops: [{ op: "set", path: ["provider"], value: option.id }] });
|
|
1003
|
+
},
|
|
1004
|
+
},
|
|
1005
|
+
});
|
|
1006
|
+
return dispose;
|
|
1007
|
+
}, "free-search: /free-search-engine command");
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
exports.apply = apply;
|
|
1012
|
+
exports.inject = inject;
|
|
1013
|
+
return module.exports;
|
|
1014
|
+
},
|
|
1015
|
+
});
|