@objectivex666/dsh-settings-search 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +20 -2
- package/README.md +19 -2
- package/lib/client.js +641 -166
- package/package.json +2 -2
- package/scripts/ai.test.mjs +379 -0
package/lib/client.js
CHANGED
|
@@ -30,8 +30,10 @@ window.__ModuleLoader__.load({
|
|
|
30
30
|
|
|
31
31
|
const NS = 'settings-search-v5'
|
|
32
32
|
/** Single source of truth is package.json — scripts/check.mjs asserts this matches. */
|
|
33
|
-
const PKG_VERSION = '1.
|
|
33
|
+
const PKG_VERSION = '1.12.0'
|
|
34
34
|
const NPM_NAME = '@objectivex666/dsh-settings-search'
|
|
35
|
+
const REPO_URL = 'https://github.com/objectivex666/dsh-settings-search'
|
|
36
|
+
const ISSUES_URL = 'https://github.com/objectivex666/dsh-settings-search/issues/new/choose'
|
|
35
37
|
const zh = {
|
|
36
38
|
title: '设置',
|
|
37
39
|
placeholder: '搜索设置…',
|
|
@@ -44,6 +46,11 @@ window.__ModuleLoader__.load({
|
|
|
44
46
|
openSearch: '打开搜索',
|
|
45
47
|
shortcutTitle: 'Ctrl+F / ⌘F 快捷键',
|
|
46
48
|
shortcutHint: '在设置面板内按 Ctrl+F 或 ⌘F 聚焦搜索框;关闭后保留浏览器默认查找。',
|
|
49
|
+
indexTitle: '设置索引',
|
|
50
|
+
indexDesc: '索引保存在本机浏览器;只在首次安装或更新其它插件后重建,也可以手动重建。',
|
|
51
|
+
indexRebuild: '手动重建索引',
|
|
52
|
+
indexRebuilding: '正在重建索引…',
|
|
53
|
+
indexDone: '索引已重建 ✓',
|
|
47
54
|
warming: '正在导入各分区选项…',
|
|
48
55
|
empty: '没有匹配的设置',
|
|
49
56
|
pages: '设置页',
|
|
@@ -69,22 +76,31 @@ window.__ModuleLoader__.load({
|
|
|
69
76
|
updNpx: 'npx',
|
|
70
77
|
updRelease: '查看 release notes',
|
|
71
78
|
updReleaseNotes: '查看更新说明',
|
|
79
|
+
updRepo: 'GitHub 仓库',
|
|
72
80
|
updFetchingNotes: '正在获取…',
|
|
73
81
|
updRestart: '更新完成后重启 DSH,新版本才会生效。',
|
|
74
82
|
aiSearch: 'AI 联想搜索',
|
|
75
|
-
aiDesc: '没有直接匹配时,用你配置的模型(OpenAI
|
|
83
|
+
aiDesc: '没有直接匹配时,用你配置的模型(OpenAI 兼容、Anthropic 或本机 Ollama)理解目标并推荐设置项。',
|
|
76
84
|
aiPrivacy: 'AI 请求会发送到你配置的模型服务;API Key 仅保存在本机浏览器。',
|
|
77
85
|
aiOn: '已开启',
|
|
78
86
|
aiOff: '已关闭',
|
|
79
87
|
aiThinking: '正在用 AI 理解搜索意图…',
|
|
80
88
|
aiUnavailable: 'AI 联想暂不可用',
|
|
81
|
-
aiNeedConfig: '请先在「设置搜索」页配置 Base URL、模型名与 API Key
|
|
89
|
+
aiNeedConfig: '请先在「设置搜索」页配置 Base URL、模型名与 API Key(Ollama 无需密钥)。',
|
|
82
90
|
aiProvider: '接口类型',
|
|
83
91
|
aiOpenAI: 'OpenAI 兼容',
|
|
84
92
|
aiAnthropic: 'Anthropic',
|
|
93
|
+
aiOllama: 'Ollama(本机)',
|
|
85
94
|
aiBaseUrl: 'Base URL',
|
|
86
95
|
aiBaseUrlHint: '仅支持 http(s) 地址;非本机地址必须使用 https,以免泄露 API Key。',
|
|
87
96
|
aiModel: '模型名',
|
|
97
|
+
aiModelHint: '从接口返回的模型中选择,或手动输入完整模型名。',
|
|
98
|
+
aiModelsLoad: '获取模型',
|
|
99
|
+
aiModelsLoading: '获取中…',
|
|
100
|
+
aiModelsLoaded: '已获取 {count} 个模型,可直接选择。',
|
|
101
|
+
aiModelsEmpty: '服务未返回模型,可手动输入。',
|
|
102
|
+
aiModelsFail: '获取模型失败:{hint}{detail}',
|
|
103
|
+
aiModelsNeedConfig: '先填写有效接口地址;云端接口还需 API Key。',
|
|
88
104
|
aiApiKey: 'API Key',
|
|
89
105
|
aiDshImport: '导入 DSH 模型配置',
|
|
90
106
|
aiDshImporting: '正在导入…',
|
|
@@ -112,10 +128,14 @@ window.__ModuleLoader__.load({
|
|
|
112
128
|
aiTestErrBadResponse: '响应格式异常,请确认接口类型与服务兼容',
|
|
113
129
|
aiTestErrHttp: '请求失败(HTTP 状态码错误),请检查地址与配置',
|
|
114
130
|
aiTestErrUnsafeUrl: '接口地址不安全:仅支持 http(s),非本机必须为 https,且不可包含账号密码',
|
|
131
|
+
aiOllamaErrNetwork: '无法连接 Ollama,请确认本机服务已启动、地址可达,且 OLLAMA_ORIGINS 允许当前 DSH 页面来源',
|
|
132
|
+
aiOllamaErrForbidden: 'Ollama 拒绝访问,请检查 OLLAMA_ORIGINS 是否允许当前 DSH 页面来源',
|
|
133
|
+
aiOllamaErrTimeout: 'Ollama 响应超时,请检查本机服务及模型加载状态',
|
|
115
134
|
logTitle: '日志',
|
|
116
135
|
logDesc: '记录插件运行、搜索与 AI 请求的关键动作,便于排查问题。',
|
|
117
136
|
logEmpty: '暂无日志',
|
|
118
137
|
logExport: '导出日志',
|
|
138
|
+
logIssues: '反馈问题',
|
|
119
139
|
logCopy: '复制日志',
|
|
120
140
|
logCopied: '已复制 ✓',
|
|
121
141
|
logCopyFail: '复制失败,请稍后重试。',
|
|
@@ -156,6 +176,11 @@ window.__ModuleLoader__.load({
|
|
|
156
176
|
shortcutTitle: 'Ctrl+F / ⌘F shortcut',
|
|
157
177
|
shortcutHint: 'Focus the search box with Ctrl+F or ⌘F inside the settings panel; turn it off to keep the browser default.',
|
|
158
178
|
warming: 'Importing section options…',
|
|
179
|
+
indexTitle: 'Settings index',
|
|
180
|
+
indexDesc: 'The index is stored locally; it rebuilds only after another plugin is installed or updated. You can also rebuild it manually.',
|
|
181
|
+
indexRebuild: 'Rebuild index',
|
|
182
|
+
indexRebuilding: 'Rebuilding…',
|
|
183
|
+
indexDone: 'Index rebuilt ✓',
|
|
159
184
|
empty: 'No matching settings',
|
|
160
185
|
pages: 'Settings pages',
|
|
161
186
|
options: 'In-page options',
|
|
@@ -180,22 +205,31 @@ window.__ModuleLoader__.load({
|
|
|
180
205
|
updNpx: 'npx',
|
|
181
206
|
updRelease: 'View release notes',
|
|
182
207
|
updReleaseNotes: 'View changelog',
|
|
208
|
+
updRepo: 'GitHub repository',
|
|
183
209
|
updFetchingNotes: 'Fetching…',
|
|
184
210
|
updRestart: 'Restart DSH after updating for the new version to take effect.',
|
|
185
211
|
aiSearch: 'AI-assisted search',
|
|
186
|
-
aiDesc: 'When no direct match exists, your own model (OpenAI-compatible or
|
|
212
|
+
aiDesc: 'When no direct match exists, your own model (OpenAI-compatible, Anthropic, or local Ollama) understands your goal and suggests settings.',
|
|
187
213
|
aiPrivacy: 'AI requests are sent to the model service you configure; the API key stays only in your browser.',
|
|
188
214
|
aiOn: 'On',
|
|
189
215
|
aiOff: 'Off',
|
|
190
216
|
aiThinking: 'Understanding your intent with AI…',
|
|
191
217
|
aiUnavailable: 'AI suggestions are unavailable',
|
|
192
|
-
aiNeedConfig: 'Configure Base URL, model, and API key in the Settings Search panel first.',
|
|
218
|
+
aiNeedConfig: 'Configure Base URL, model, and API key in the Settings Search panel first (Ollama needs no key).',
|
|
193
219
|
aiProvider: 'API type',
|
|
194
220
|
aiOpenAI: 'OpenAI-compatible',
|
|
195
221
|
aiAnthropic: 'Anthropic',
|
|
222
|
+
aiOllama: 'Ollama (local)',
|
|
196
223
|
aiBaseUrl: 'Base URL',
|
|
197
224
|
aiBaseUrlHint: 'Only http(s) URLs are allowed; remote hosts must use https to avoid exposing your API key.',
|
|
198
225
|
aiModel: 'Model',
|
|
226
|
+
aiModelHint: 'Choose a model returned by the API, or enter a complete model name.',
|
|
227
|
+
aiModelsLoad: 'Get models',
|
|
228
|
+
aiModelsLoading: 'Loading…',
|
|
229
|
+
aiModelsLoaded: 'Loaded {count} models; choose one below.',
|
|
230
|
+
aiModelsEmpty: 'The service returned no models; enter one manually.',
|
|
231
|
+
aiModelsFail: 'Failed to load models: {hint}{detail}',
|
|
232
|
+
aiModelsNeedConfig: 'Enter a valid base URL first; cloud APIs also need an API key.',
|
|
199
233
|
aiApiKey: 'API Key',
|
|
200
234
|
aiDshImport: 'Import DSH model config',
|
|
201
235
|
aiDshImporting: 'Importing…',
|
|
@@ -223,10 +257,14 @@ window.__ModuleLoader__.load({
|
|
|
223
257
|
aiTestErrBadResponse: 'Unexpected response — confirm the API type matches the service',
|
|
224
258
|
aiTestErrHttp: 'Request failed (HTTP status error) — check the URL and config',
|
|
225
259
|
aiTestErrUnsafeUrl: 'Unsafe URL — only http(s) is allowed, remote hosts must use https, and embedded credentials are not permitted',
|
|
260
|
+
aiOllamaErrNetwork: 'Cannot connect to Ollama. Check that the local service is running, the URL is reachable, and OLLAMA_ORIGINS allows this DSH page origin',
|
|
261
|
+
aiOllamaErrForbidden: 'Ollama denied access. Check that OLLAMA_ORIGINS allows this DSH page origin',
|
|
262
|
+
aiOllamaErrTimeout: 'Ollama timed out. Check the local service and model loading status',
|
|
226
263
|
logTitle: 'Logs',
|
|
227
264
|
logDesc: 'Logs key plugin actions, searches, and AI requests to help troubleshoot.',
|
|
228
265
|
logEmpty: 'No logs yet',
|
|
229
266
|
logExport: 'Export logs',
|
|
267
|
+
logIssues: 'Report issue',
|
|
230
268
|
logCopy: 'Copy logs',
|
|
231
269
|
logCopied: 'Copied ✓',
|
|
232
270
|
logCopyFail: 'Copy failed. Please try again.',
|
|
@@ -463,6 +501,20 @@ window.__ModuleLoader__.load({
|
|
|
463
501
|
const AI_BASE_URL_KEY = 'dsh-settings-search:ai-base-url'
|
|
464
502
|
const AI_MODEL_KEY = 'dsh-settings-search:ai-model'
|
|
465
503
|
const AI_API_KEY_KEY = 'dsh-settings-search:ai-api-key'
|
|
504
|
+
const OLLAMA_BASE_URL = 'http://127.0.0.1:11434'
|
|
505
|
+
const normalizeAiConfig = (cfg) => {
|
|
506
|
+
const provider = ['anthropic', 'ollama'].includes(cfg.provider) ? cfg.provider : 'openai'
|
|
507
|
+
return {
|
|
508
|
+
provider,
|
|
509
|
+
baseUrl: String(cfg.baseUrl ?? '').trim() || (provider === 'ollama' ? OLLAMA_BASE_URL : ''),
|
|
510
|
+
model: String(cfg.model ?? '').trim(),
|
|
511
|
+
apiKey: String(cfg.apiKey ?? '').trim(),
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const aiConfigReady = (cfg) => {
|
|
515
|
+
const { provider, baseUrl, model, apiKey } = normalizeAiConfig(cfg)
|
|
516
|
+
return Boolean(baseUrl && model && (provider === 'ollama' || apiKey))
|
|
517
|
+
}
|
|
466
518
|
/** Session-only API key storage avoids durable browser storage exposure. */
|
|
467
519
|
const readApiKey = () => {
|
|
468
520
|
const legacy = typeof localStorage !== 'undefined' ? (localStorage.getItem(AI_API_KEY_KEY) ?? '').trim() : ''
|
|
@@ -491,24 +543,23 @@ window.__ModuleLoader__.load({
|
|
|
491
543
|
window.dispatchEvent(new CustomEvent(AI_TOGGLE_EVENT, { detail: enabled }))
|
|
492
544
|
}
|
|
493
545
|
const aiConfig = () => {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
provider: providerRaw === 'anthropic' ? 'anthropic' : 'openai',
|
|
500
|
-
baseUrl: (localStorage?.getItem(AI_BASE_URL_KEY) ?? '').trim(),
|
|
501
|
-
model: (localStorage?.getItem(AI_MODEL_KEY) ?? '').trim(),
|
|
546
|
+
const storage = typeof localStorage === 'undefined' ? null : localStorage
|
|
547
|
+
return normalizeAiConfig({
|
|
548
|
+
provider: (storage?.getItem(AI_PROVIDER_KEY) ?? 'openai').trim(),
|
|
549
|
+
baseUrl: storage?.getItem(AI_BASE_URL_KEY),
|
|
550
|
+
model: storage?.getItem(AI_MODEL_KEY),
|
|
502
551
|
apiKey: readApiKey(),
|
|
503
|
-
}
|
|
552
|
+
})
|
|
504
553
|
}
|
|
505
554
|
const setAiConfig = (cfg) => {
|
|
555
|
+
AI_CACHE.clear()
|
|
506
556
|
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') return
|
|
507
|
-
|
|
557
|
+
const normalized = normalizeAiConfig(cfg)
|
|
558
|
+
writeApiKey(normalized.apiKey)
|
|
508
559
|
if (typeof localStorage === 'undefined') return
|
|
509
|
-
localStorage.setItem(AI_PROVIDER_KEY,
|
|
510
|
-
localStorage.setItem(AI_BASE_URL_KEY,
|
|
511
|
-
localStorage.setItem(AI_MODEL_KEY,
|
|
560
|
+
localStorage.setItem(AI_PROVIDER_KEY, normalized.provider)
|
|
561
|
+
localStorage.setItem(AI_BASE_URL_KEY, normalized.baseUrl)
|
|
562
|
+
localStorage.setItem(AI_MODEL_KEY, normalized.model)
|
|
512
563
|
window.dispatchEvent(new CustomEvent(AI_CONFIG_EVENT))
|
|
513
564
|
}
|
|
514
565
|
/** First-run onboarding: remembered via localStorage so it shows once. */
|
|
@@ -518,6 +569,10 @@ window.__ModuleLoader__.load({
|
|
|
518
569
|
const SEARCH_STYLE_EVENT = 'dsh-settings-search:style-change'
|
|
519
570
|
const SEARCH_SHORTCUT_KEY = 'dsh-settings-search:shortcut-enabled'
|
|
520
571
|
const SEARCH_SHORTCUT_EVENT = 'dsh-settings-search:shortcut-change'
|
|
572
|
+
const INDEX_STORAGE_KEY = 'dsh-settings-search:index:v1'
|
|
573
|
+
const INDEX_INVALID_EVENT = 'dsh-settings-search:index-invalid'
|
|
574
|
+
const INDEX_REBUILD_EVENT = 'dsh-settings-search:index-rebuild'
|
|
575
|
+
const INDEX_REBUILD_DONE_EVENT = 'dsh-settings-search:index-rebuild-done'
|
|
521
576
|
const SEARCH_STYLES = ['sidebar', 'top', 'page']
|
|
522
577
|
const readSearchStyle = () => {
|
|
523
578
|
if (typeof localStorage === 'undefined') return 'sidebar'
|
|
@@ -599,62 +654,204 @@ window.__ModuleLoader__.load({
|
|
|
599
654
|
if (/\/v1\/messages$/i.test(safe)) return safe
|
|
600
655
|
return /\/v1$/i.test(safe) ? `${safe}/messages` : `${safe}/v1/messages`
|
|
601
656
|
}
|
|
602
|
-
const
|
|
603
|
-
const
|
|
604
|
-
if (!
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
657
|
+
const ollamaChatUrl = (baseUrl) => {
|
|
658
|
+
const safe = safeBaseUrl(baseUrl)
|
|
659
|
+
if (!safe) return ''
|
|
660
|
+
if (/\/api\/chat$/i.test(safe)) return safe
|
|
661
|
+
if (/\/api$/i.test(safe)) return `${safe}/chat`
|
|
662
|
+
// Accept the base URL used by Ollama's OpenAI-compatible endpoint too.
|
|
663
|
+
return `${safe.replace(/\/v1(?:\/chat\/completions)?$/i, '')}/api/chat`
|
|
664
|
+
}
|
|
665
|
+
const ollamaModelsUrl = (baseUrl) => {
|
|
666
|
+
const safe = safeBaseUrl(baseUrl)
|
|
667
|
+
if (!safe) return ''
|
|
668
|
+
const root = safe.replace(/\/v1(?:\/models|\/chat\/completions)?$/i, '').replace(/\/api(?:\/tags|\/chat)?$/i, '')
|
|
669
|
+
return `${root}/api/tags`
|
|
670
|
+
}
|
|
671
|
+
const chatModelsUrl = (baseUrl) => {
|
|
672
|
+
const safe = safeBaseUrl(baseUrl)
|
|
673
|
+
if (!safe) return ''
|
|
674
|
+
const root = safe.replace(/\/(?:models|chat\/completions)$/i, '')
|
|
675
|
+
return /\/v1$/i.test(root) ? `${root}/models` : `${root}/v1/models`
|
|
676
|
+
}
|
|
677
|
+
const anthropicModelsUrl = (baseUrl) => {
|
|
678
|
+
const safe = safeBaseUrl(baseUrl)
|
|
679
|
+
if (!safe) return ''
|
|
680
|
+
const root = safe.replace(/\/v1\/(?:models|messages)$/i, '')
|
|
681
|
+
return `${root}/v1/models`
|
|
682
|
+
}
|
|
683
|
+
const modelsUrl = (provider, baseUrl) => provider === 'ollama'
|
|
684
|
+
? ollamaModelsUrl(baseUrl)
|
|
685
|
+
: provider === 'anthropic' ? anthropicModelsUrl(baseUrl) : chatModelsUrl(baseUrl)
|
|
686
|
+
const extractAiModels = (provider, data) => {
|
|
687
|
+
const rows = provider === 'ollama'
|
|
688
|
+
? (Array.isArray(data?.models) ? data.models : [])
|
|
689
|
+
: (Array.isArray(data?.data) ? data.data : Array.isArray(data?.models) ? data.models : [])
|
|
690
|
+
return [...new Set(rows
|
|
691
|
+
.map((row) => typeof row === 'string' ? row : row?.id ?? row?.name ?? row?.model)
|
|
692
|
+
.filter((value) => typeof value === 'string')
|
|
693
|
+
.map((value) => value.trim())
|
|
694
|
+
.filter(Boolean))].slice(0, 500)
|
|
695
|
+
}
|
|
696
|
+
const fetchAiModels = async (cfg, signal) => {
|
|
697
|
+
cfg = normalizeAiConfig(cfg)
|
|
698
|
+
const { provider, baseUrl, apiKey } = cfg
|
|
699
|
+
const url = modelsUrl(provider, baseUrl)
|
|
700
|
+
if (!url) return { ok: false, code: 'unsafe-url', models: [], detail: '' }
|
|
701
|
+
if (provider !== 'ollama' && !apiKey) return { ok: false, code: 'need-config', models: [], detail: '' }
|
|
702
|
+
const headers = provider === 'anthropic'
|
|
703
|
+
? {
|
|
704
|
+
'x-api-key': apiKey,
|
|
705
|
+
'anthropic-version': '2023-06-01',
|
|
706
|
+
'anthropic-dangerous-direct-browser-access': 'true',
|
|
707
|
+
}
|
|
708
|
+
: provider === 'openai' ? { authorization: `Bearer ${apiKey}` } : {}
|
|
709
|
+
const controller = new AbortController()
|
|
710
|
+
const abort = () => controller.abort()
|
|
711
|
+
if (signal?.aborted) abort()
|
|
712
|
+
else signal?.addEventListener('abort', abort, { once: true })
|
|
610
713
|
let timer
|
|
611
714
|
const deadline = new Promise((_, reject) => {
|
|
612
715
|
timer = setTimeout(() => {
|
|
613
|
-
const error = new Error('AI
|
|
716
|
+
const error = new Error('AI model list timed out')
|
|
614
717
|
error.name = 'AI_TIMEOUT'
|
|
615
718
|
reject(error)
|
|
719
|
+
controller.abort()
|
|
616
720
|
}, 12000)
|
|
617
721
|
})
|
|
618
|
-
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
messages: [{ role: 'user', content: userContent }],
|
|
632
|
-
temperature: 0,
|
|
722
|
+
try {
|
|
723
|
+
const models = await Promise.race([
|
|
724
|
+
(async () => {
|
|
725
|
+
const response = await fetch(url, {
|
|
726
|
+
method: 'GET',
|
|
727
|
+
headers,
|
|
728
|
+
signal: controller.signal,
|
|
729
|
+
redirect: 'error',
|
|
730
|
+
})
|
|
731
|
+
if (!response.ok) {
|
|
732
|
+
const error = new Error(`HTTP ${response.status}`)
|
|
733
|
+
error.status = response.status
|
|
734
|
+
throw error
|
|
633
735
|
}
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
736
|
+
const data = await response.json().catch((error) => {
|
|
737
|
+
if (error.name === 'AbortError') throw error
|
|
738
|
+
return null
|
|
739
|
+
})
|
|
740
|
+
if (data?.error) {
|
|
741
|
+
const error = new Error('Unexpected model list response')
|
|
742
|
+
error.name = 'AI_BAD_RESPONSE'
|
|
743
|
+
throw error
|
|
641
744
|
}
|
|
642
|
-
|
|
643
|
-
|
|
745
|
+
return extractAiModels(provider, data)
|
|
746
|
+
})(),
|
|
747
|
+
deadline,
|
|
748
|
+
])
|
|
749
|
+
return { ok: true, models, code: models.length === 0 ? 'empty' : 'ok', detail: '' }
|
|
750
|
+
} catch (error) {
|
|
751
|
+
if (error.name === 'AbortError') return { ok: false, code: 'cancelled', models: [], detail: '' }
|
|
752
|
+
if (error.name === 'AI_TIMEOUT') return { ok: false, code: 'timeout', models: [], detail: '' }
|
|
753
|
+
const code = error.status ? testHttpCode(error.status) : error.name === 'AI_BAD_RESPONSE' ? 'bad-response' : 'network'
|
|
754
|
+
return { ok: false, code, models: [], detail: String(error?.message ?? error).slice(0, 160) }
|
|
755
|
+
} finally {
|
|
756
|
+
clearTimeout(timer)
|
|
757
|
+
signal?.removeEventListener('abort', abort)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
const aiRequest = (cfg, userContent, testing = false) => {
|
|
761
|
+
const { provider, baseUrl, model, apiKey } = normalizeAiConfig(cfg)
|
|
762
|
+
const messages = [{ role: 'user', content: userContent }]
|
|
763
|
+
const headers = { 'content-type': 'application/json' }
|
|
764
|
+
if (provider === 'anthropic') {
|
|
765
|
+
return {
|
|
766
|
+
url: anthropicMessagesUrl(baseUrl),
|
|
767
|
+
headers: { ...headers, 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' },
|
|
768
|
+
body: { model, messages, max_tokens: testing ? 1 : 1024, ...(testing ? {} : { system: AI_PROMPT, temperature: 0 }) },
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (!testing) messages.unshift({ role: 'system', content: AI_PROMPT })
|
|
772
|
+
if (provider === 'ollama') {
|
|
773
|
+
return {
|
|
774
|
+
url: ollamaChatUrl(baseUrl),
|
|
644
775
|
headers,
|
|
645
|
-
|
|
776
|
+
body: {
|
|
777
|
+
model,
|
|
778
|
+
messages,
|
|
779
|
+
stream: false,
|
|
780
|
+
...(testing ? {} : { format: 'json' }),
|
|
781
|
+
options: { temperature: 0, num_predict: testing ? 1 : 1024 },
|
|
782
|
+
},
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
url: chatCompletionsUrl(baseUrl),
|
|
787
|
+
headers: { ...headers, authorization: `Bearer ${apiKey}` },
|
|
788
|
+
body: { model, messages, temperature: 0, ...(testing ? { max_tokens: 1 } : {}) },
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const aiResponseContent = (provider, data) => {
|
|
792
|
+
if (provider === 'ollama') return data?.message?.content
|
|
793
|
+
if (provider === 'anthropic') {
|
|
794
|
+
if (!Array.isArray(data?.content) || data.content.length === 0) return undefined
|
|
795
|
+
return data.content.filter((block) => block?.type === 'text').map((block) => block.text).join('')
|
|
796
|
+
}
|
|
797
|
+
return data?.choices?.[0]?.message?.content
|
|
798
|
+
}
|
|
799
|
+
const fetchAiData = async (request, provider, signal) => {
|
|
800
|
+
const controller = new AbortController()
|
|
801
|
+
const abort = () => controller.abort()
|
|
802
|
+
if (signal?.aborted) abort()
|
|
803
|
+
else signal?.addEventListener('abort', abort, { once: true })
|
|
804
|
+
let timer
|
|
805
|
+
const deadline = new Promise((_, reject) => {
|
|
806
|
+
timer = setTimeout(() => {
|
|
807
|
+
const error = new Error('AI request timed out')
|
|
808
|
+
error.name = 'AI_TIMEOUT'
|
|
809
|
+
reject(error)
|
|
810
|
+
controller.abort()
|
|
811
|
+
}, provider === 'ollama' ? 120000 : 12000)
|
|
812
|
+
})
|
|
813
|
+
const work = (async () => {
|
|
814
|
+
const response = await fetch(request.url, {
|
|
815
|
+
method: 'POST',
|
|
816
|
+
headers: request.headers,
|
|
817
|
+
signal: controller.signal,
|
|
646
818
|
redirect: 'error',
|
|
647
|
-
body: JSON.stringify(body),
|
|
819
|
+
body: JSON.stringify(request.body),
|
|
648
820
|
})
|
|
649
|
-
if (!response.ok)
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
821
|
+
if (!response.ok) {
|
|
822
|
+
const error = new Error(`HTTP ${response.status}`)
|
|
823
|
+
error.status = response.status
|
|
824
|
+
throw error
|
|
825
|
+
}
|
|
826
|
+
const data = await response.json().catch((error) => {
|
|
827
|
+
if (error.name === 'AbortError') throw error
|
|
828
|
+
return null
|
|
829
|
+
})
|
|
830
|
+
if (data?.error || typeof aiResponseContent(provider, data) !== 'string') {
|
|
831
|
+
const error = new Error('Unexpected AI response')
|
|
832
|
+
error.name = 'AI_BAD_RESPONSE'
|
|
833
|
+
throw error
|
|
834
|
+
}
|
|
835
|
+
return data
|
|
655
836
|
})()
|
|
656
837
|
try {
|
|
657
|
-
|
|
838
|
+
return await Promise.race([work, deadline])
|
|
839
|
+
} finally {
|
|
840
|
+
clearTimeout(timer)
|
|
841
|
+
signal?.removeEventListener('abort', abort)
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
const requestUserAi = async (query, labels, signal) => {
|
|
845
|
+
const cfg = aiConfig()
|
|
846
|
+
if (!aiConfigReady(cfg)) return []
|
|
847
|
+
const { provider, model } = cfg
|
|
848
|
+
const request = aiRequest(cfg, `用户目标:${query}\n设置索引:\n${labels}`)
|
|
849
|
+
if (!request.url) return []
|
|
850
|
+
const startedAt = Date.now()
|
|
851
|
+
log('info', 'AI 请求', { provider, model, query: String(query).slice(0, 50) })
|
|
852
|
+
try {
|
|
853
|
+
const data = await fetchAiData(request, provider, signal)
|
|
854
|
+
const keywords = extractAiKeywords(aiResponseContent(provider, data))
|
|
658
855
|
log('info', 'AI 结果', { count: keywords.length, duration: Date.now() - startedAt })
|
|
659
856
|
return keywords
|
|
660
857
|
} catch (error) {
|
|
@@ -664,15 +861,14 @@ window.__ModuleLoader__.load({
|
|
|
664
861
|
}
|
|
665
862
|
log('error', 'AI 请求失败', { message: String(error?.message ?? error).slice(0, 160), duration: Date.now() - startedAt })
|
|
666
863
|
throw error
|
|
667
|
-
} finally {
|
|
668
|
-
clearTimeout(timer)
|
|
669
864
|
}
|
|
670
865
|
}
|
|
671
866
|
const requestAiKeywords = async (query, labels, signal) => {
|
|
672
|
-
const
|
|
867
|
+
const { provider, baseUrl, model } = aiConfig()
|
|
868
|
+
const cacheKey = JSON.stringify([provider, baseUrl, model, query.toLowerCase(), labels])
|
|
673
869
|
if (AI_CACHE.has(cacheKey)) return AI_CACHE.get(cacheKey)
|
|
674
870
|
const keywords = await requestUserAi(query, labels, signal)
|
|
675
|
-
if (keywords.length > 0) {
|
|
871
|
+
if (!signal?.aborted && keywords.length > 0) {
|
|
676
872
|
// bound the cache so a long session cannot grow it without limit
|
|
677
873
|
if (AI_CACHE.size >= 100) AI_CACHE.clear()
|
|
678
874
|
AI_CACHE.set(cacheKey, keywords)
|
|
@@ -698,68 +894,31 @@ window.__ModuleLoader__.load({
|
|
|
698
894
|
const code = Number(status)
|
|
699
895
|
return HTTP_TEST_CODES[code] ?? (code >= 500 && code <= 599 ? 'server' : 'http')
|
|
700
896
|
}
|
|
701
|
-
const testAiService = async (cfg) => {
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
const
|
|
706
|
-
if (!
|
|
707
|
-
const isAnthropic = provider === 'anthropic'
|
|
708
|
-
const requestUrl = isAnthropic ? anthropicMessagesUrl(baseUrl) : chatCompletionsUrl(baseUrl)
|
|
709
|
-
if (!requestUrl) return { ok: false, code: 'unsafe-url', detail: '' }
|
|
897
|
+
const testAiService = async (cfg, signal) => {
|
|
898
|
+
cfg = normalizeAiConfig(cfg)
|
|
899
|
+
const { provider, model } = cfg
|
|
900
|
+
if (!aiConfigReady(cfg)) return { ok: false, code: 'need-config', detail: '' }
|
|
901
|
+
const request = aiRequest(cfg, 'ping', true)
|
|
902
|
+
if (!request.url) return { ok: false, code: 'unsafe-url', detail: '' }
|
|
710
903
|
const startedAt = Date.now()
|
|
711
904
|
log('info', '测试 API 服务', { provider, model })
|
|
712
|
-
let timer
|
|
713
|
-
const deadline = new Promise((_, reject) => {
|
|
714
|
-
timer = setTimeout(() => {
|
|
715
|
-
const error = new Error('AI request timed out')
|
|
716
|
-
error.name = 'AI_TIMEOUT'
|
|
717
|
-
reject(error)
|
|
718
|
-
}, 12000)
|
|
719
|
-
})
|
|
720
|
-
const work = (async () => {
|
|
721
|
-
const headers = {
|
|
722
|
-
'content-type': 'application/json',
|
|
723
|
-
...(isAnthropic
|
|
724
|
-
? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' }
|
|
725
|
-
: { authorization: `Bearer ${apiKey}` }),
|
|
726
|
-
}
|
|
727
|
-
const body = isAnthropic
|
|
728
|
-
? { model, max_tokens: 1, messages: [{ role: 'user', content: 'ping' }] }
|
|
729
|
-
: { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }
|
|
730
|
-
const response = await fetch(requestUrl, {
|
|
731
|
-
method: 'POST',
|
|
732
|
-
headers,
|
|
733
|
-
redirect: 'error',
|
|
734
|
-
body: JSON.stringify(body),
|
|
735
|
-
})
|
|
736
|
-
if (!response.ok) return { ok: false, code: testHttpCode(response.status), detail: `HTTP ${response.status}` }
|
|
737
|
-
const data = await response.json().catch(() => ({}))
|
|
738
|
-
const valid = isAnthropic
|
|
739
|
-
? (Array.isArray(data?.content) && data.content.length > 0)
|
|
740
|
-
: Boolean(data?.choices?.[0]?.message)
|
|
741
|
-
if (!valid) return { ok: false, code: 'bad-response', detail: '' }
|
|
742
|
-
return { ok: true }
|
|
743
|
-
})()
|
|
744
905
|
try {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
return result
|
|
906
|
+
await fetchAiData(request, provider, signal)
|
|
907
|
+
log('info', '测试 API 成功', { duration: Date.now() - startedAt })
|
|
908
|
+
return { ok: true }
|
|
749
909
|
} catch (error) {
|
|
750
910
|
if (error.name === 'AbortError') {
|
|
751
911
|
log('warn', '测试 API 取消', { duration: Date.now() - startedAt })
|
|
752
|
-
|
|
912
|
+
return { ok: false, code: 'cancelled', detail: '' }
|
|
753
913
|
}
|
|
754
914
|
if (error.name === 'AI_TIMEOUT') {
|
|
755
915
|
log('error', '测试 API 超时', { duration: Date.now() - startedAt })
|
|
756
916
|
return { ok: false, code: 'timeout', detail: '' }
|
|
757
917
|
}
|
|
918
|
+
const code = error.status ? testHttpCode(error.status) : error.name === 'AI_BAD_RESPONSE' ? 'bad-response' : 'network'
|
|
758
919
|
const reason = String(error?.message ?? error).slice(0, 160)
|
|
759
920
|
log('error', '测试 API 失败', { detail: reason, duration: Date.now() - startedAt })
|
|
760
|
-
return { ok: false, code
|
|
761
|
-
} finally {
|
|
762
|
-
clearTimeout(timer)
|
|
921
|
+
return { ok: false, code, detail: reason }
|
|
763
922
|
}
|
|
764
923
|
}
|
|
765
924
|
|
|
@@ -800,7 +959,7 @@ window.__ModuleLoader__.load({
|
|
|
800
959
|
.sss-trigger { display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; height: 32px; padding: 0 9px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-dimmed); font: inherit; font-size: 14px; line-height: 22px; cursor: pointer; text-align: left; }
|
|
801
960
|
.sss-trigger:hover { border-color: var(--dsw-alias-label-dimmed); color: var(--dsw-alias-label-secondary); }
|
|
802
961
|
.sss-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
|
|
803
|
-
.sss-page-overlay { position: fixed; inset: 0; z-index:
|
|
962
|
+
.sss-page-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: flex-start; justify-content: center; padding: 72px 20px 32px; background: var(--dsw-alias-bg-mask, rgb(0 0 0 / 36%)); }
|
|
804
963
|
.sss-page { display: flex; flex-direction: column; width: min(760px, 100%); max-height: min(680px, calc(100vh - 104px)); overflow: hidden; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l1); border-radius: 14px; box-shadow: var(--dsw-shadow-lv3); }
|
|
805
964
|
.sss-page-head { flex: none; display: flex; align-items: center; gap: 8px; height: 52px; padding: 0 10px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
|
|
806
965
|
.sss-page-input { flex: 1; min-width: 0; height: 38px; padding: 0 6px; border: none; outline: none; background: transparent; font-size: 16px; line-height: 24px; color: var(--dsw-alias-label-primary); }
|
|
@@ -921,9 +1080,69 @@ window.__ModuleLoader__.load({
|
|
|
921
1080
|
return typeof value === 'string' ? value : ''
|
|
922
1081
|
}
|
|
923
1082
|
|
|
1083
|
+
const serializeHarvest = (harvest) => {
|
|
1084
|
+
const raw = {}
|
|
1085
|
+
for (const [slot, map] of harvest) {
|
|
1086
|
+
if (!(map instanceof Map) || map.size === 0) continue
|
|
1087
|
+
raw[slot] = Object.fromEntries([...map].filter(([, record]) => record && typeof record.title === 'string' && record.title !== ''))
|
|
1088
|
+
}
|
|
1089
|
+
return raw
|
|
1090
|
+
}
|
|
1091
|
+
const deserializeHarvest = (raw) => {
|
|
1092
|
+
const harvest = new Map()
|
|
1093
|
+
if (!raw || typeof raw !== 'object') return harvest
|
|
1094
|
+
for (const [slot, slotValue] of Object.entries(raw)) {
|
|
1095
|
+
if (!slotValue || typeof slotValue !== 'object') continue
|
|
1096
|
+
const map = new Map()
|
|
1097
|
+
for (const [key, record] of Object.entries(slotValue)) {
|
|
1098
|
+
if (!record || typeof record !== 'object' || typeof record.title !== 'string' || record.title === '') continue
|
|
1099
|
+
const inner = Array.isArray(record.inner)
|
|
1100
|
+
? record.inner.filter((op) => op && typeof op.label === 'string').map((op) => ({ label: op.label, text: typeof op.text === 'string' ? op.text : '' }))
|
|
1101
|
+
: []
|
|
1102
|
+
map.set(key, {
|
|
1103
|
+
title: record.title,
|
|
1104
|
+
idx: Number.isFinite(record.idx) ? record.idx : 0,
|
|
1105
|
+
text: typeof record.text === 'string' ? record.text : '',
|
|
1106
|
+
inner,
|
|
1107
|
+
})
|
|
1108
|
+
}
|
|
1109
|
+
if (map.size > 0) harvest.set(slot, map)
|
|
1110
|
+
}
|
|
1111
|
+
return harvest
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const readIndexCache = () => {
|
|
1115
|
+
if (typeof localStorage === 'undefined') return null
|
|
1116
|
+
try {
|
|
1117
|
+
const raw = JSON.parse(localStorage.getItem(INDEX_STORAGE_KEY) ?? 'null')
|
|
1118
|
+
if (!raw || raw.version !== 1 || typeof raw.fingerprint !== 'string' || raw.fingerprint === '') return null
|
|
1119
|
+
return {
|
|
1120
|
+
fingerprint: typeof raw.fingerprint === 'string' ? raw.fingerprint : null,
|
|
1121
|
+
harvest: deserializeHarvest(raw.harvest),
|
|
1122
|
+
}
|
|
1123
|
+
} catch {
|
|
1124
|
+
return null
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const otherPluginFingerprint = (plugins) => {
|
|
1129
|
+
const rows = Array.isArray(plugins) ? plugins : []
|
|
1130
|
+
const isSelf = (row) => [
|
|
1131
|
+
row?.id, row?.name, row?.npm, row?.package, row?.packageName,
|
|
1132
|
+
].some((value) => typeof value === 'string' && value.includes(NPM_NAME))
|
|
1133
|
+
return JSON.stringify(rows.filter((row) => row && !isSelf(row)).map((row) => [
|
|
1134
|
+
String(row.id ?? row.name ?? ''),
|
|
1135
|
+
String(row.npm ?? row.package ?? row.packageName ?? ''),
|
|
1136
|
+
String(row.version ?? ''),
|
|
1137
|
+
].sort()).sort())
|
|
1138
|
+
}
|
|
1139
|
+
|
|
924
1140
|
function createModel(ctx) {
|
|
1141
|
+
const initialCache = readIndexCache()
|
|
1142
|
+
const cachedFingerprint = initialCache?.fingerprint ?? null
|
|
925
1143
|
/** id → harvested visible title, per slot. */
|
|
926
|
-
const harvest = new Map()
|
|
1144
|
+
const harvest = initialCache?.harvest ?? new Map()
|
|
1145
|
+
let fingerprint = cachedFingerprint
|
|
927
1146
|
|
|
928
1147
|
const entriesOf = (slot) => {
|
|
929
1148
|
try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
|
|
@@ -1234,7 +1453,53 @@ window.__ModuleLoader__.load({
|
|
|
1234
1453
|
: a.order - b.order)
|
|
1235
1454
|
}
|
|
1236
1455
|
|
|
1237
|
-
|
|
1456
|
+
const slotsFingerprint = () => otherPluginFingerprint([
|
|
1457
|
+
...sortEntries(entriesOf('settings.plugin.item')).map((entry) => ({
|
|
1458
|
+
id: String(entry.options?.id ?? entry.options?.key ?? ''),
|
|
1459
|
+
name: optionLabel(entry.options?.label),
|
|
1460
|
+
locale: String(entry.options?.locale ?? ''),
|
|
1461
|
+
})),
|
|
1462
|
+
...sortEntries(entriesOf('web-ui.plugin.item')).map((entry) => ({
|
|
1463
|
+
id: String(entry.options?.id ?? entry.options?.key ?? ''),
|
|
1464
|
+
name: optionLabel(entry.options?.label),
|
|
1465
|
+
locale: String(entry.options?.locale ?? ''),
|
|
1466
|
+
})),
|
|
1467
|
+
])
|
|
1468
|
+
|
|
1469
|
+
const resetHarvest = () => { harvest.clear(); warmed = false }
|
|
1470
|
+
|
|
1471
|
+
const persistIndex = (nextFingerprint = fingerprint) => {
|
|
1472
|
+
if (typeof nextFingerprint !== 'string' || nextFingerprint === '' || harvest.size === 0) return false
|
|
1473
|
+
fingerprint = nextFingerprint
|
|
1474
|
+
if (typeof localStorage === 'undefined') return false
|
|
1475
|
+
try {
|
|
1476
|
+
localStorage.setItem(INDEX_STORAGE_KEY, JSON.stringify({
|
|
1477
|
+
version: 1,
|
|
1478
|
+
fingerprint,
|
|
1479
|
+
updatedAt: Date.now(),
|
|
1480
|
+
harvest: serializeHarvest(harvest),
|
|
1481
|
+
}))
|
|
1482
|
+
return true
|
|
1483
|
+
} catch {
|
|
1484
|
+
return false
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const indexFingerprintReady = () => typeof fingerprint === 'string' && fingerprint !== ''
|
|
1489
|
+
|
|
1490
|
+
const applyIndexFingerprint = (nextFingerprint) => {
|
|
1491
|
+
if (typeof nextFingerprint !== 'string' || nextFingerprint === '') return false
|
|
1492
|
+
if (nextFingerprint === fingerprint) {
|
|
1493
|
+
if (nextFingerprint === cachedFingerprint) warmed = true
|
|
1494
|
+
return false
|
|
1495
|
+
}
|
|
1496
|
+
const wasPopulated = harvest.size > 0
|
|
1497
|
+
fingerprint = nextFingerprint
|
|
1498
|
+
resetHarvest()
|
|
1499
|
+
return wasPopulated
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
return { readSections, readDeep, runHarvest, persistIndex, applyIndexFingerprint, resetHarvest, slotsFingerprint, indexFingerprintReady }
|
|
1238
1503
|
}
|
|
1239
1504
|
|
|
1240
1505
|
/* ---------- DOM-level navigation into the settings shell ---------- */
|
|
@@ -1286,7 +1551,7 @@ window.__ModuleLoader__.load({
|
|
|
1286
1551
|
const warmAllSections = async (model, onStart, onDone) => {
|
|
1287
1552
|
if (warmed || typeof document === 'undefined') return true
|
|
1288
1553
|
const sections = model.readSections()
|
|
1289
|
-
if (sections.length === 0) return
|
|
1554
|
+
if (sections.length === 0) return false
|
|
1290
1555
|
const original = activeSectionLabel()
|
|
1291
1556
|
if (original === '') return false // settings panel not open yet; retry on next mount
|
|
1292
1557
|
warmed = true
|
|
@@ -1306,6 +1571,8 @@ window.__ModuleLoader__.load({
|
|
|
1306
1571
|
} finally {
|
|
1307
1572
|
if (typeof onDone === 'function') onDone()
|
|
1308
1573
|
}
|
|
1574
|
+
if (visited === 0) return false
|
|
1575
|
+
model.persistIndex()
|
|
1309
1576
|
log('info', '预载设置索引', { total: sections.length, visited })
|
|
1310
1577
|
return true
|
|
1311
1578
|
}
|
|
@@ -1381,7 +1648,35 @@ window.__ModuleLoader__.load({
|
|
|
1381
1648
|
}, 'settings-search: styles')
|
|
1382
1649
|
|
|
1383
1650
|
const model = createModel(ctx)
|
|
1651
|
+
const indexControls = { resetHarvest: model.resetHarvest, persistIndex: model.persistIndex }
|
|
1384
1652
|
const navigator = createNavigator(ctx)
|
|
1653
|
+
let pluginManagerSyncToken = 0
|
|
1654
|
+
|
|
1655
|
+
const syncPluginManagerIndex = async (face) => {
|
|
1656
|
+
if (face === null || !face.isLoopback) return
|
|
1657
|
+
const token = ++pluginManagerSyncToken
|
|
1658
|
+
try {
|
|
1659
|
+
const rows = await face.list()
|
|
1660
|
+
if (token !== pluginManagerSyncToken) return
|
|
1661
|
+
if (model.applyIndexFingerprint('manager:' + otherPluginFingerprint(rows))) {
|
|
1662
|
+
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1663
|
+
log('info', '检测到其它插件变化,将重建设置索引', {})
|
|
1664
|
+
}
|
|
1665
|
+
} catch {
|
|
1666
|
+
// Keep the last trusted fingerprint when manager inspection fails.
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
ctx.inject(['pluginManager'], (inner) => {
|
|
1671
|
+
const face = inner.pluginManager ?? null
|
|
1672
|
+
const sync = () => { void syncPluginManagerIndex(face) }
|
|
1673
|
+
sync()
|
|
1674
|
+
const unsubscribe = typeof face?.onChange === 'function' ? face.onChange(sync) : null
|
|
1675
|
+
return () => {
|
|
1676
|
+
pluginManagerSyncToken += 1
|
|
1677
|
+
if (typeof unsubscribe === 'function') unsubscribe()
|
|
1678
|
+
}
|
|
1679
|
+
}, 'settings-search: plugin manager index fingerprint')
|
|
1385
1680
|
|
|
1386
1681
|
/** Read the model fields from DSH; credentials are never requested. */
|
|
1387
1682
|
const readDshModelConfig = async () => {
|
|
@@ -1407,7 +1702,9 @@ window.__ModuleLoader__.load({
|
|
|
1407
1702
|
const baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1408
1703
|
if (!modelId || !baseUrl) return null
|
|
1409
1704
|
return {
|
|
1410
|
-
provider:
|
|
1705
|
+
provider: providerId.toLowerCase() === 'ollama' || protocol === 'ollama'
|
|
1706
|
+
? 'ollama'
|
|
1707
|
+
: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1411
1708
|
baseUrl,
|
|
1412
1709
|
model: modelId,
|
|
1413
1710
|
}
|
|
@@ -1565,6 +1862,24 @@ window.__ModuleLoader__.load({
|
|
|
1565
1862
|
}
|
|
1566
1863
|
}, [])
|
|
1567
1864
|
|
|
1865
|
+
React.useEffect(() => {
|
|
1866
|
+
// If no plugin manager is present, the slot registrations are the
|
|
1867
|
+
// only available change signal. Waiting briefly avoids racing the
|
|
1868
|
+
// optional manager during startup.
|
|
1869
|
+
const fallback = setTimeout(() => {
|
|
1870
|
+
if (model.indexFingerprintReady()) return
|
|
1871
|
+
if (model.applyIndexFingerprint(model.slotsFingerprint())) {
|
|
1872
|
+
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1873
|
+
}
|
|
1874
|
+
}, 3000)
|
|
1875
|
+
const onInvalid = () => { setWarming(false); setTick((value) => value + 1) }
|
|
1876
|
+
window.addEventListener(INDEX_INVALID_EVENT, onInvalid)
|
|
1877
|
+
return () => {
|
|
1878
|
+
clearTimeout(fallback)
|
|
1879
|
+
window.removeEventListener(INDEX_INVALID_EVENT, onInvalid)
|
|
1880
|
+
}
|
|
1881
|
+
}, [])
|
|
1882
|
+
|
|
1568
1883
|
React.useEffect(() => {
|
|
1569
1884
|
const onWelcome = (event) => { if (event?.detail === true) setShowWelcome(true) }
|
|
1570
1885
|
window.addEventListener(WELCOME_EVENT, onWelcome)
|
|
@@ -1659,12 +1974,12 @@ window.__ModuleLoader__.load({
|
|
|
1659
1974
|
const results = [...shownPages, ...shownOptions, ...related.map((r) => r.item)]
|
|
1660
1975
|
|
|
1661
1976
|
const cfg = aiConfig()
|
|
1662
|
-
const aiConfigured =
|
|
1977
|
+
const aiConfigured = aiConfigReady(cfg)
|
|
1663
1978
|
const aiReady = aiEnabled && aiConfigured && q.length >= 2 && noLocalMatch
|
|
1664
1979
|
React.useEffect(() => {
|
|
1665
|
-
if (!aiReady) return
|
|
1666
1980
|
setAiKeywords([])
|
|
1667
1981
|
setAiStatus('idle')
|
|
1982
|
+
if (!aiReady) return
|
|
1668
1983
|
let alive = true
|
|
1669
1984
|
const controller = new AbortController()
|
|
1670
1985
|
const timer = setTimeout(() => {
|
|
@@ -1692,7 +2007,7 @@ window.__ModuleLoader__.load({
|
|
|
1692
2007
|
clearTimeout(timer)
|
|
1693
2008
|
controller.abort()
|
|
1694
2009
|
}
|
|
1695
|
-
}, [aiReady, aiEnabled, q])
|
|
2010
|
+
}, [aiReady, aiEnabled, q, cfg.provider, cfg.baseUrl, cfg.model, cfg.apiKey])
|
|
1696
2011
|
|
|
1697
2012
|
const loggedQueryRef = React.useRef('')
|
|
1698
2013
|
React.useEffect(() => {
|
|
@@ -1892,7 +2207,6 @@ window.__ModuleLoader__.load({
|
|
|
1892
2207
|
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1893
2208
|
: null,
|
|
1894
2209
|
]),
|
|
1895
|
-
React.createElement('div', { className: 'sss-wrap sss-top' }, [pageTriggerButton]),
|
|
1896
2210
|
React.createElement('div', {
|
|
1897
2211
|
className: 'sss-page-overlay',
|
|
1898
2212
|
onMouseDown: (event) => { if (event.target === event.currentTarget) setPageOpen(false) },
|
|
@@ -2031,9 +2345,15 @@ window.__ModuleLoader__.load({
|
|
|
2031
2345
|
const [baseUrl, setBaseUrl] = React.useState(cfg.baseUrl)
|
|
2032
2346
|
const [model, setModel] = React.useState(cfg.model)
|
|
2033
2347
|
const [apiKey, setApiKey] = React.useState(cfg.apiKey)
|
|
2348
|
+
const providerDrafts = React.useRef({})
|
|
2349
|
+
const testAbortRef = React.useRef(null)
|
|
2350
|
+
const modelsAbortRef = React.useRef(null)
|
|
2034
2351
|
const [aiSaved, setAiSaved] = React.useState(false)
|
|
2035
2352
|
const [testState, setTestState] = React.useState('idle') // idle | loading | ok | fail
|
|
2036
2353
|
const [testMsg, setTestMsg] = React.useState('')
|
|
2354
|
+
const [aiModels, setAiModels] = React.useState([])
|
|
2355
|
+
const [modelsState, setModelsState] = React.useState('idle') // idle | loading | ready | fail
|
|
2356
|
+
const [modelsMsg, setModelsMsg] = React.useState('')
|
|
2037
2357
|
const [dshImportState, setDshImportState] = React.useState('idle') // idle | loading | ok | fail
|
|
2038
2358
|
const [dshImportMsg, setDshImportMsg] = React.useState('')
|
|
2039
2359
|
const [, setLogVersion] = React.useState(0)
|
|
@@ -2041,6 +2361,8 @@ window.__ModuleLoader__.load({
|
|
|
2041
2361
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
2042
2362
|
const [logExportFail, setLogExportFail] = React.useState(false)
|
|
2043
2363
|
const [logCleared, setLogCleared] = React.useState(false)
|
|
2364
|
+
const [indexState, setIndexState] = React.useState('idle')
|
|
2365
|
+
const [warming, setWarming] = React.useState(false)
|
|
2044
2366
|
const [confirmClear, setConfirmClear] = React.useState(false)
|
|
2045
2367
|
const [confirmClearAi, setConfirmClearAi] = React.useState(false)
|
|
2046
2368
|
|
|
@@ -2065,6 +2387,15 @@ window.__ModuleLoader__.load({
|
|
|
2065
2387
|
useAutoReset(copied, () => { setCopied(false); setCopyFailed(false) })
|
|
2066
2388
|
useAutoReset(aiSaved, () => setAiSaved(false))
|
|
2067
2389
|
|
|
2390
|
+
React.useEffect(() => {
|
|
2391
|
+
setTestState('idle')
|
|
2392
|
+
setTestMsg('')
|
|
2393
|
+
setAiSaved(false)
|
|
2394
|
+
return () => testAbortRef.current?.abort()
|
|
2395
|
+
}, [provider, baseUrl, model, apiKey])
|
|
2396
|
+
|
|
2397
|
+
React.useEffect(() => () => modelsAbortRef.current?.abort(), [provider, baseUrl, apiKey])
|
|
2398
|
+
|
|
2068
2399
|
React.useEffect(() => {
|
|
2069
2400
|
const sync = () => setLogVersion((v) => v + 1)
|
|
2070
2401
|
window.addEventListener(LOG_EVENT, sync)
|
|
@@ -2075,13 +2406,84 @@ window.__ModuleLoader__.load({
|
|
|
2075
2406
|
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2076
2407
|
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
2077
2408
|
|
|
2409
|
+
const changeProvider = (next) => {
|
|
2410
|
+
providerDrafts.current[provider] = { baseUrl, model }
|
|
2411
|
+
const draft = providerDrafts.current[next]
|
|
2412
|
+
setProvider(next)
|
|
2413
|
+
setBaseUrl(draft?.baseUrl ?? (next === 'ollama' ? OLLAMA_BASE_URL : ''))
|
|
2414
|
+
setModel(draft?.model ?? '')
|
|
2415
|
+
resetModelList()
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
const resetModelList = () => {
|
|
2419
|
+
modelsAbortRef.current?.abort()
|
|
2420
|
+
setAiModels([])
|
|
2421
|
+
setModelsState('idle')
|
|
2422
|
+
setModelsMsg('')
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const modelsErrorHint = (code) => {
|
|
2426
|
+
if (code === 'need-config') return t('aiModelsNeedConfig')
|
|
2427
|
+
const cloudKeys = {
|
|
2428
|
+
auth: 'aiTestErrAuth',
|
|
2429
|
+
forbidden: 'aiTestErrForbidden',
|
|
2430
|
+
'not-found': 'aiTestErrNotFound',
|
|
2431
|
+
'rate-limit': 'aiTestErrRateLimit',
|
|
2432
|
+
'bad-request': 'aiTestErrBadRequest',
|
|
2433
|
+
server: 'aiTestErrServer',
|
|
2434
|
+
timeout: 'aiTestErrTimeout',
|
|
2435
|
+
network: 'aiTestErrNetwork',
|
|
2436
|
+
'bad-response': 'aiTestErrBadResponse',
|
|
2437
|
+
'unsafe-url': 'aiTestErrUnsafeUrl',
|
|
2438
|
+
}
|
|
2439
|
+
const ollamaKeys = { ...cloudKeys, network: 'aiOllamaErrNetwork', forbidden: 'aiOllamaErrForbidden', timeout: 'aiOllamaErrTimeout' }
|
|
2440
|
+
return t((provider === 'ollama' ? ollamaKeys : cloudKeys)[code] ?? 'aiTestErrHttp')
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
const loadModels = async () => {
|
|
2444
|
+
if (modelsState === 'loading') return
|
|
2445
|
+
const source = normalizeAiConfig({ provider, baseUrl, model, apiKey })
|
|
2446
|
+
if (!modelsUrl(source.provider, source.baseUrl) || (source.provider !== 'ollama' && !source.apiKey)) {
|
|
2447
|
+
setModelsState('fail')
|
|
2448
|
+
setModelsMsg(t('aiModelsFail').replace('{hint}', t('aiModelsNeedConfig')).replace('{detail}', ''))
|
|
2449
|
+
return
|
|
2450
|
+
}
|
|
2451
|
+
const controller = new AbortController()
|
|
2452
|
+
modelsAbortRef.current?.abort()
|
|
2453
|
+
modelsAbortRef.current = controller
|
|
2454
|
+
setModelsState('loading')
|
|
2455
|
+
setModelsMsg('')
|
|
2456
|
+
const startedAt = Date.now()
|
|
2457
|
+
log('info', '获取 AI 模型列表', { provider: source.provider })
|
|
2458
|
+
const result = await fetchAiModels(source, controller.signal)
|
|
2459
|
+
if (controller.signal.aborted) return
|
|
2460
|
+
if (modelsAbortRef.current !== controller) return
|
|
2461
|
+
modelsAbortRef.current = null
|
|
2462
|
+
if (result.ok) {
|
|
2463
|
+
setAiModels(result.models)
|
|
2464
|
+
setModelsState('ready')
|
|
2465
|
+
setModelsMsg(result.models.length === 0
|
|
2466
|
+
? t('aiModelsEmpty')
|
|
2467
|
+
: t('aiModelsLoaded').replace('{count}', String(result.models.length)))
|
|
2468
|
+
log('info', '获取 AI 模型列表成功', { count: result.models.length, duration: Date.now() - startedAt })
|
|
2469
|
+
} else {
|
|
2470
|
+
setAiModels([])
|
|
2471
|
+
setModelsState('fail')
|
|
2472
|
+
const hint = modelsErrorHint(result.code)
|
|
2473
|
+
const detail = result.detail ? `(${result.detail})` : ''
|
|
2474
|
+
setModelsMsg(t('aiModelsFail').replace('{hint}', hint).replace('{detail}', detail))
|
|
2475
|
+
log('error', '获取 AI 模型列表失败', { code: result.code, detail: result.detail, duration: Date.now() - startedAt })
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2078
2479
|
const saveAi = () => {
|
|
2079
|
-
|
|
2480
|
+
const next = normalizeAiConfig({
|
|
2080
2481
|
provider,
|
|
2081
|
-
baseUrl
|
|
2082
|
-
model
|
|
2083
|
-
apiKey
|
|
2482
|
+
baseUrl,
|
|
2483
|
+
model,
|
|
2484
|
+
apiKey,
|
|
2084
2485
|
})
|
|
2486
|
+
setAiConfig(next)
|
|
2085
2487
|
setAiSaved(true)
|
|
2086
2488
|
log('info', '保存 AI 配置', { provider, model: model.trim() })
|
|
2087
2489
|
}
|
|
@@ -2093,10 +2495,12 @@ window.__ModuleLoader__.load({
|
|
|
2093
2495
|
try {
|
|
2094
2496
|
const imported = await readDshModelConfig()
|
|
2095
2497
|
if (!imported) throw new Error(t('aiDshImportEmpty'))
|
|
2498
|
+
providerDrafts.current[provider] = { baseUrl, model }
|
|
2096
2499
|
setAiConfig({ ...imported, apiKey })
|
|
2097
2500
|
setProvider(imported.provider)
|
|
2098
2501
|
setBaseUrl(imported.baseUrl)
|
|
2099
2502
|
setModel(imported.model)
|
|
2503
|
+
resetModelList()
|
|
2100
2504
|
setDshImportState('ok')
|
|
2101
2505
|
setDshImportMsg(t('aiDshImportOk'))
|
|
2102
2506
|
log('info', '导入 DSH 模型配置', { provider: imported.provider, model: imported.model })
|
|
@@ -2113,6 +2517,25 @@ window.__ModuleLoader__.load({
|
|
|
2113
2517
|
setSearchStyleState(style)
|
|
2114
2518
|
}
|
|
2115
2519
|
|
|
2520
|
+
const rebuildIndex = async () => {
|
|
2521
|
+
if (indexState === 'loading' || warming) return
|
|
2522
|
+
setIndexState('loading')
|
|
2523
|
+
setWarming(true)
|
|
2524
|
+
indexControls.resetHarvest()
|
|
2525
|
+
try {
|
|
2526
|
+
const done = await warmAllSections(model, () => setWarming(true), () => setWarming(false))
|
|
2527
|
+
if (!done) throw new Error('index rebuild did not visit any sections')
|
|
2528
|
+
indexControls.persistIndex()
|
|
2529
|
+
setIndexState('done')
|
|
2530
|
+
window.dispatchEvent(new CustomEvent(INDEX_REBUILD_DONE_EVENT))
|
|
2531
|
+
setTimeout(() => setIndexState((state) => state === 'done' ? 'idle' : state), 2400)
|
|
2532
|
+
} catch (error) {
|
|
2533
|
+
setIndexState('idle')
|
|
2534
|
+
setWarming(false)
|
|
2535
|
+
log('error', '手动重建设置索引失败', { detail: String(error?.message ?? error).slice(0, 160) })
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2116
2539
|
const updateShortcutEnabled = (enabled) => {
|
|
2117
2540
|
setSearchShortcutEnabled(enabled)
|
|
2118
2541
|
setShortcutOn(enabled)
|
|
@@ -2128,26 +2551,38 @@ window.__ModuleLoader__.load({
|
|
|
2128
2551
|
setBaseUrl('')
|
|
2129
2552
|
setModel('')
|
|
2130
2553
|
setApiKey('')
|
|
2554
|
+
providerDrafts.current = {}
|
|
2555
|
+
resetModelList()
|
|
2131
2556
|
setAiSaved(false)
|
|
2132
2557
|
log('info', '清除 API 配置', {})
|
|
2133
2558
|
}
|
|
2134
2559
|
|
|
2135
2560
|
const testService = async () => {
|
|
2136
2561
|
if (testState === 'loading') return
|
|
2137
|
-
if (!
|
|
2562
|
+
if (!aiConfigReady({ provider, baseUrl, model, apiKey })) {
|
|
2138
2563
|
setTestState('fail')
|
|
2139
2564
|
setTestMsg(t('aiNeedConfig'))
|
|
2140
2565
|
return
|
|
2141
2566
|
}
|
|
2142
2567
|
setTestState('loading')
|
|
2143
2568
|
setTestMsg('')
|
|
2144
|
-
const
|
|
2569
|
+
const controller = new AbortController()
|
|
2570
|
+
testAbortRef.current?.abort()
|
|
2571
|
+
testAbortRef.current = controller
|
|
2572
|
+
const result = await testAiService({ provider, baseUrl, model, apiKey }, controller.signal)
|
|
2573
|
+
if (controller.signal.aborted) return
|
|
2574
|
+
if (testAbortRef.current === controller) testAbortRef.current = null
|
|
2145
2575
|
if (result.ok) {
|
|
2146
2576
|
setTestState('ok')
|
|
2147
2577
|
setTestMsg(t('aiTestOk'))
|
|
2148
2578
|
} else {
|
|
2149
2579
|
setTestState('fail')
|
|
2150
|
-
const
|
|
2580
|
+
const ollamaHintKey = provider === 'ollama' ? {
|
|
2581
|
+
network: 'aiOllamaErrNetwork',
|
|
2582
|
+
forbidden: 'aiOllamaErrForbidden',
|
|
2583
|
+
timeout: 'aiOllamaErrTimeout',
|
|
2584
|
+
}[result.code] : null
|
|
2585
|
+
const hintKey = ollamaHintKey ?? {
|
|
2151
2586
|
auth: 'aiTestErrAuth',
|
|
2152
2587
|
forbidden: 'aiTestErrForbidden',
|
|
2153
2588
|
'not-found': 'aiTestErrNotFound',
|
|
@@ -2332,6 +2767,23 @@ window.__ModuleLoader__.load({
|
|
|
2332
2767
|
]),
|
|
2333
2768
|
React.createElement('p', { className: 'sss-vr-hint' }, t('shortcutHint')),
|
|
2334
2769
|
]),
|
|
2770
|
+
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2771
|
+
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2772
|
+
React.createElement('span', { className: 'sss-vr-label' }, t('indexTitle')),
|
|
2773
|
+
]),
|
|
2774
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('indexDesc')),
|
|
2775
|
+
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
2776
|
+
React.createElement('button', {
|
|
2777
|
+
className: 'sss-vr-btn',
|
|
2778
|
+
type: 'button',
|
|
2779
|
+
disabled: indexState === 'loading' || warming,
|
|
2780
|
+
onClick: () => { void rebuildIndex() },
|
|
2781
|
+
}, indexState === 'loading' || warming ? t('indexRebuilding') : t('indexRebuild')),
|
|
2782
|
+
]),
|
|
2783
|
+
indexState === 'done'
|
|
2784
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: '#1e8e3e' } }, t('indexDone'))
|
|
2785
|
+
: null,
|
|
2786
|
+
]),
|
|
2335
2787
|
React.createElement('div', { className: 'sss-vr-field sss-vr-form' }, [
|
|
2336
2788
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2337
2789
|
React.createElement('label', { className: 'sss-vr-label' }, t('aiProvider')),
|
|
@@ -2339,14 +2791,16 @@ window.__ModuleLoader__.load({
|
|
|
2339
2791
|
React.createElement('select', {
|
|
2340
2792
|
className: 'sss-vr-input sss-vr-select',
|
|
2341
2793
|
name: 'dsh-settings-search-provider',
|
|
2794
|
+
'aria-label': t('aiProvider'),
|
|
2342
2795
|
value: provider,
|
|
2343
2796
|
autoComplete: 'off',
|
|
2344
2797
|
'data-lpignore': 'true',
|
|
2345
2798
|
'data-form-type': 'other',
|
|
2346
|
-
onChange: (event) =>
|
|
2799
|
+
onChange: (event) => changeProvider(event.target.value),
|
|
2347
2800
|
}, [
|
|
2348
2801
|
React.createElement('option', { value: 'openai' }, t('aiOpenAI')),
|
|
2349
2802
|
React.createElement('option', { value: 'anthropic' }, t('aiAnthropic')),
|
|
2803
|
+
React.createElement('option', { value: 'ollama' }, t('aiOllama')),
|
|
2350
2804
|
]),
|
|
2351
2805
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2352
2806
|
React.createElement('label', { className: 'sss-vr-label' }, t('aiBaseUrl')),
|
|
@@ -2355,48 +2809,65 @@ window.__ModuleLoader__.load({
|
|
|
2355
2809
|
className: 'sss-vr-input',
|
|
2356
2810
|
type: 'url',
|
|
2357
2811
|
name: 'dsh-settings-search-base-url',
|
|
2812
|
+
'aria-label': t('aiBaseUrl'),
|
|
2358
2813
|
value: baseUrl,
|
|
2359
|
-
placeholder: provider === '
|
|
2360
|
-
|
|
2361
|
-
autoComplete: 'off',
|
|
2362
|
-
'data-lpignore': 'true',
|
|
2363
|
-
'data-1p-ignore': 'true',
|
|
2364
|
-
'data-form-type': 'other',
|
|
2365
|
-
onChange: (event) => setBaseUrl(event.target.value),
|
|
2366
|
-
}),
|
|
2367
|
-
React.createElement('p', { className: 'sss-vr-hint' }, t('aiBaseUrlHint')),
|
|
2368
|
-
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2369
|
-
React.createElement('label', { className: 'sss-vr-label' }, t('aiModel')),
|
|
2370
|
-
]),
|
|
2371
|
-
React.createElement('input', {
|
|
2372
|
-
className: 'sss-vr-input',
|
|
2373
|
-
type: 'text',
|
|
2374
|
-
name: 'dsh-settings-search-model',
|
|
2375
|
-
value: model,
|
|
2376
|
-
placeholder: provider === 'anthropic' ? 'claude-sonnet-4-20250514' : 'gpt-4o-mini',
|
|
2377
|
-
spellCheck: false,
|
|
2378
|
-
autoComplete: 'off',
|
|
2379
|
-
'data-lpignore': 'true',
|
|
2380
|
-
'data-1p-ignore': 'true',
|
|
2381
|
-
'data-form-type': 'other',
|
|
2382
|
-
onChange: (event) => setModel(event.target.value),
|
|
2383
|
-
}),
|
|
2384
|
-
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2385
|
-
React.createElement('label', { className: 'sss-vr-label' }, t('aiApiKey')),
|
|
2386
|
-
]),
|
|
2387
|
-
React.createElement('input', {
|
|
2388
|
-
className: 'sss-vr-input sss-vr-mask',
|
|
2389
|
-
type: 'text',
|
|
2390
|
-
name: 'dsh-settings-search-api-key',
|
|
2391
|
-
value: apiKey,
|
|
2392
|
-
placeholder: 'sk-…',
|
|
2814
|
+
placeholder: provider === 'ollama' ? OLLAMA_BASE_URL
|
|
2815
|
+
: provider === 'anthropic' ? 'https://api.anthropic.com' : 'https://api.openai.com/v1',
|
|
2393
2816
|
spellCheck: false,
|
|
2394
2817
|
autoComplete: 'off',
|
|
2395
2818
|
'data-lpignore': 'true',
|
|
2396
2819
|
'data-1p-ignore': 'true',
|
|
2397
2820
|
'data-form-type': 'other',
|
|
2398
|
-
|
|
2399
|
-
|
|
2821
|
+
onChange: (event) => { setBaseUrl(event.target.value); resetModelList() },
|
|
2822
|
+
}),
|
|
2823
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('aiBaseUrlHint')),
|
|
2824
|
+
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2825
|
+
React.createElement('label', { className: 'sss-vr-label' }, t('aiModel')),
|
|
2826
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: modelsState === 'loading', onClick: () => { void loadModels() } },
|
|
2827
|
+
modelsState === 'loading' ? t('aiModelsLoading') : t('aiModelsLoad')),
|
|
2828
|
+
]),
|
|
2829
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('aiModelHint')),
|
|
2830
|
+
React.createElement('input', {
|
|
2831
|
+
className: 'sss-vr-input',
|
|
2832
|
+
type: 'text',
|
|
2833
|
+
name: 'dsh-settings-search-model',
|
|
2834
|
+
'aria-label': t('aiModel'),
|
|
2835
|
+
value: model,
|
|
2836
|
+
list: 'dsh-settings-search-model-options',
|
|
2837
|
+
placeholder: provider === 'ollama' ? 'qwen3:8b'
|
|
2838
|
+
: provider === 'anthropic' ? 'claude-sonnet-4-20250514' : 'gpt-4o-mini',
|
|
2839
|
+
spellCheck: false,
|
|
2840
|
+
autoComplete: 'off',
|
|
2841
|
+
'data-lpignore': 'true',
|
|
2842
|
+
'data-1p-ignore': 'true',
|
|
2843
|
+
'data-form-type': 'other',
|
|
2844
|
+
onChange: (event) => setModel(event.target.value),
|
|
2845
|
+
}),
|
|
2846
|
+
React.createElement('datalist', { id: 'dsh-settings-search-model-options' },
|
|
2847
|
+
aiModels.map((item) => React.createElement('option', { key: item, value: item }, item))),
|
|
2848
|
+
modelsMsg
|
|
2849
|
+
? React.createElement('p', {
|
|
2850
|
+
className: modelsState === 'fail' ? 'sss-vr-note' : 'sss-vr-hint',
|
|
2851
|
+
style: modelsState === 'fail' ? { color: 'var(--dsw-alias-label-danger)' } : undefined,
|
|
2852
|
+
}, modelsMsg)
|
|
2853
|
+
: null,
|
|
2854
|
+
provider !== 'ollama' ? React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2855
|
+
React.createElement('label', { className: 'sss-vr-label' }, t('aiApiKey')),
|
|
2856
|
+
]) : null,
|
|
2857
|
+
provider !== 'ollama' ? React.createElement('input', {
|
|
2858
|
+
className: 'sss-vr-input sss-vr-mask',
|
|
2859
|
+
type: 'text',
|
|
2860
|
+
name: 'dsh-settings-search-api-key',
|
|
2861
|
+
'aria-label': t('aiApiKey'),
|
|
2862
|
+
value: apiKey,
|
|
2863
|
+
placeholder: 'sk-…',
|
|
2864
|
+
spellCheck: false,
|
|
2865
|
+
autoComplete: 'off',
|
|
2866
|
+
'data-lpignore': 'true',
|
|
2867
|
+
'data-1p-ignore': 'true',
|
|
2868
|
+
'data-form-type': 'other',
|
|
2869
|
+
onChange: (event) => { setApiKey(event.target.value); resetModelList() },
|
|
2870
|
+
}) : null,
|
|
2400
2871
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
2401
2872
|
React.createElement('button', { className: 'sss-vr-btn sss-vr-primary', type: 'button', onClick: () => { void saveAi() } },
|
|
2402
2873
|
aiSaved ? t('aiConfigSaved') : t('aiConfigSave')),
|
|
@@ -2444,6 +2915,8 @@ window.__ModuleLoader__.load({
|
|
|
2444
2915
|
state === 'loading' ? t('updChecking') : t('updCheck')),
|
|
2445
2916
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: notesState === 'loading', onClick: () => { void fetchNotes() } },
|
|
2446
2917
|
notesState === 'loading' ? t('updFetchingNotes') : t('updReleaseNotes')),
|
|
2918
|
+
React.createElement('a', { className: 'sss-vr-link', href: REPO_URL, target: '_blank', rel: 'noopener noreferrer' },
|
|
2919
|
+
t('updRepo')),
|
|
2447
2920
|
]),
|
|
2448
2921
|
notesState === 'loaded'
|
|
2449
2922
|
? React.createElement('div', { className: 'sss-vr-notes' }, notesBody)
|
|
@@ -2460,6 +2933,8 @@ window.__ModuleLoader__.load({
|
|
|
2460
2933
|
: null,
|
|
2461
2934
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
2462
2935
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void exportLogs() } }, t('logExport')),
|
|
2936
|
+
React.createElement('a', { className: 'sss-vr-link', href: ISSUES_URL, target: '_blank', rel: 'noopener noreferrer' },
|
|
2937
|
+
t('logIssues')),
|
|
2463
2938
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void copyLogs() } },
|
|
2464
2939
|
logCopied ? t('logCopied') : t('logCopy')),
|
|
2465
2940
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: LOGS.length === 0, onClick: () => requestClear() },
|