@objectivex666/dsh-settings-search 1.8.0 → 1.10.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 +6 -2
- package/README.md +6 -2
- package/lib/client.js +559 -154
- package/package.json +2 -1
- package/scripts/release.mjs +5 -0
package/README.en.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](https://awesome-dsh-plugin.com)
|
|
9
|
+
[](https://dsh-plugin.org/plugins/objectivex666/dsh-settings-search)
|
|
9
10
|
|
|
10
11
|
**English** · [中文](./README.md)
|
|
11
12
|
|
|
@@ -13,8 +14,11 @@
|
|
|
13
14
|
|
|
14
15
|
## Features
|
|
15
16
|
|
|
16
|
-
- 🔍 **Live search** — a search box
|
|
17
|
-
-
|
|
17
|
+
- 🔍 **Live search** — a search box in the settings panel filters every registered settings page (`settings.section`) and general item (`settings.general.item`) as you type.
|
|
18
|
+
- 🧩 **Three search styles** — choose the classic sidebar, window top bar, or popup search page. The choice is saved in the current browser.
|
|
19
|
+
- ⌨️ **Search shortcut (v1.10.0)** — focus the search box with `Ctrl+F` or `⌘F` inside the settings panel. You can enable or disable it from the "Settings Search" page; disabling keeps the browser's default find behavior.
|
|
20
|
+
- 🤝 **Import DSH model config (v1.9.0)** — import the Base URL and model name from DSH's current default model in the AI config section. For credential safety, the API key is never imported.
|
|
21
|
+
- 🎯 **First-run onboarding (v1.9.0)** — a welcome modal appears only the first time the settings panel opens after installation. It introduces the four search styles, pinyin/intent/AI hints, click-to-jump, and auto-indexing; there are no "Don't show again" or manual reopen buttons.
|
|
18
22
|
- 📂 **In-page option search (v1.2.0)** — search beyond pages and reach the specific options inside them: Plugins-page tabs (`settings.plugins.tab`), Web UI plugin cards (`web-ui.plugin.item`), general rows, and more, each shown with a "page › option" breadcrumb.
|
|
19
23
|
- 🔤 **Pinyin association search (v1.5.0)** — type initials (`sz`) or full pinyin (`shezhi`) to surface Chinese settings items; navigate with `↑`/`↓`, confirm with `Enter`, dismiss with `Esc`, and matched text is highlighted.
|
|
20
24
|
- 🧠 **Intent search (v1.6.0)** — describe the goal in Chinese (e.g. 「太亮了」 "too bright", 「字太小」 "text too small") to surface matching settings without knowing their names, shown in a "You may want" group.
|
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](./LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
[](https://awesome-dsh-plugin.com)
|
|
9
|
+
[](https://dsh-plugin.org/plugins/objectivex666/dsh-settings-search)
|
|
9
10
|
|
|
10
11
|
**中文** · [English](./README.en.md)
|
|
11
12
|
|
|
@@ -13,8 +14,11 @@
|
|
|
13
14
|
|
|
14
15
|
## 功能特性
|
|
15
16
|
|
|
16
|
-
- 🔍
|
|
17
|
-
-
|
|
17
|
+
- 🔍 **实时搜索**:在设置面板中嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
|
|
18
|
+
- 🧩 **三种搜索样式**:可选择经典侧栏、窗口顶栏或弹出搜索页;选择会保存在当前浏览器。
|
|
19
|
+
- ⌨️ **搜索快捷键(v1.10.0)**:在设置面板内按 `Ctrl+F` 或 `⌘F` 聚焦搜索框;可在「设置搜索」页启用或关闭,关闭后保留浏览器默认查找。
|
|
20
|
+
- 🤝 **导入 DSH 模型配置(v1.9.0)**:在 AI 配置区一键读取 DSH 当前默认模型的 Base URL 与模型名;为避免凭据扩散,导入始终不包含 API Key。
|
|
21
|
+
- 🎯 **新用户引导(v1.9.0)**:仅在安装后首次打开设置面板时弹出引导弹窗,介绍四种搜索样式、拼音/意图/AI 联想、点击即达与自动导入索引等核心功能;不再提供“不再显示”和手动重开按钮。
|
|
18
22
|
- 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
|
|
19
23
|
- 🔤 **拼音联想搜索(v1.5.0)**:输入拼音首字母(如 `sz`)或全拼(如 `shezhi`)即可联想中文设置项;支持 `↑`/`↓` 选择、`Enter` 跳转、`Esc` 关闭,命中的文字自动高亮。
|
|
20
24
|
- 🧠 **意图联想搜索(v1.6.0)**:直接描述想做的事(如「太亮了」「字太小」「忘记密码」),无需知道选项名即可联想真实设置项,结果在「猜你想找」分组展示。
|
package/lib/client.js
CHANGED
|
@@ -30,12 +30,20 @@ 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.10.0'
|
|
34
34
|
const NPM_NAME = '@objectivex666/dsh-settings-search'
|
|
35
35
|
const zh = {
|
|
36
36
|
title: '设置',
|
|
37
37
|
placeholder: '搜索设置…',
|
|
38
38
|
searchBtn: '搜索',
|
|
39
|
+
styleTitle: '搜索框样式',
|
|
40
|
+
styleHint: '选择搜索入口的展示方式;设置会保存在本机浏览器。',
|
|
41
|
+
styleClassic: '经典侧栏',
|
|
42
|
+
styleTop: '窗口顶栏',
|
|
43
|
+
stylePage: '弹出搜索页',
|
|
44
|
+
openSearch: '打开搜索',
|
|
45
|
+
shortcutTitle: 'Ctrl+F / ⌘F 快捷键',
|
|
46
|
+
shortcutHint: '在设置面板内按 Ctrl+F 或 ⌘F 聚焦搜索框;关闭后保留浏览器默认查找。',
|
|
39
47
|
warming: '正在导入各分区选项…',
|
|
40
48
|
empty: '没有匹配的设置',
|
|
41
49
|
pages: '设置页',
|
|
@@ -43,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
43
51
|
guess: '猜你想找',
|
|
44
52
|
page: '页面',
|
|
45
53
|
option: '选项',
|
|
54
|
+
pageSearchTitle: '搜索结果',
|
|
46
55
|
jumpFail: '未能自动跳转,请手动打开:{path}',
|
|
47
56
|
updn: '设置搜索',
|
|
48
57
|
updDesc: '设置面板顶部搜索框由本插件提供。在此检查插件的版本与更新。',
|
|
@@ -77,6 +86,11 @@ window.__ModuleLoader__.load({
|
|
|
77
86
|
aiBaseUrlHint: '仅支持 http(s) 地址;非本机地址必须使用 https,以免泄露 API Key。',
|
|
78
87
|
aiModel: '模型名',
|
|
79
88
|
aiApiKey: 'API Key',
|
|
89
|
+
aiDshImport: '导入 DSH 模型配置',
|
|
90
|
+
aiDshImporting: '正在导入…',
|
|
91
|
+
aiDshImportOk: '已导入 Base URL 与模型名;API Key 未改动 ✓',
|
|
92
|
+
aiDshImportFail: '导入失败:{reason}',
|
|
93
|
+
aiDshImportEmpty: '未找到 DSH 当前默认模型配置',
|
|
80
94
|
aiConfigSave: '保存配置',
|
|
81
95
|
aiConfigSaved: '已保存 ✓',
|
|
82
96
|
aiClear: '清除配置',
|
|
@@ -115,7 +129,7 @@ window.__ModuleLoader__.load({
|
|
|
115
129
|
wlTitle: '欢迎使用设置搜索',
|
|
116
130
|
wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
|
|
117
131
|
wlFeature1Title: '实时搜索',
|
|
118
|
-
wlFeature1Desc: '
|
|
132
|
+
wlFeature1Desc: '支持经典侧栏、窗口顶栏与弹出搜索页;输入关键词即可过滤设置。',
|
|
119
133
|
wlFeature2Title: '拼音与意图联想',
|
|
120
134
|
wlFeature2Desc: '输入拼音首字母(如 sz)或直接描述需求(如「太亮」),也能联想设置项。',
|
|
121
135
|
wlFeature3Title: 'AI 联想(可选)',
|
|
@@ -125,9 +139,7 @@ window.__ModuleLoader__.load({
|
|
|
125
139
|
wlFeature5Title: '自带设置页',
|
|
126
140
|
wlFeature5Desc: '左侧导航的「设置搜索」页可检查更新、查看说明与导出日志。',
|
|
127
141
|
wlStart: '开始使用',
|
|
128
|
-
wlSkip: '不再显示',
|
|
129
142
|
wlOpenPanel: '打开设置搜索页',
|
|
130
|
-
wlReopen: '新用户引导',
|
|
131
143
|
wlFeature6Title: '自动导入索引',
|
|
132
144
|
wlFeature6Desc: '首次打开时自动遍历一遍各设置分区,把分区内选项一并纳入搜索,无需逐页手动打开即可直接搜到。',
|
|
133
145
|
}
|
|
@@ -135,6 +147,14 @@ window.__ModuleLoader__.load({
|
|
|
135
147
|
title: 'Settings',
|
|
136
148
|
placeholder: 'Search settings…',
|
|
137
149
|
searchBtn: 'Search',
|
|
150
|
+
styleTitle: 'Search style',
|
|
151
|
+
styleHint: 'Choose how the search entry is displayed; saved in this browser.',
|
|
152
|
+
styleClassic: 'Classic sidebar',
|
|
153
|
+
styleTop: 'Top bar',
|
|
154
|
+
stylePage: 'Popup page',
|
|
155
|
+
openSearch: 'Open search',
|
|
156
|
+
shortcutTitle: 'Ctrl+F / ⌘F shortcut',
|
|
157
|
+
shortcutHint: 'Focus the search box with Ctrl+F or ⌘F inside the settings panel; turn it off to keep the browser default.',
|
|
138
158
|
warming: 'Importing section options…',
|
|
139
159
|
empty: 'No matching settings',
|
|
140
160
|
pages: 'Settings pages',
|
|
@@ -142,6 +162,7 @@ window.__ModuleLoader__.load({
|
|
|
142
162
|
guess: 'You may want',
|
|
143
163
|
page: 'Page',
|
|
144
164
|
option: 'Option',
|
|
165
|
+
pageSearchTitle: 'Search results',
|
|
145
166
|
jumpFail: 'Could not navigate automatically — open it here: {path}',
|
|
146
167
|
updn: 'Settings Search',
|
|
147
168
|
updDesc: "The search box atop the settings panel comes from this plugin. Check its version and updates here.",
|
|
@@ -176,6 +197,11 @@ window.__ModuleLoader__.load({
|
|
|
176
197
|
aiBaseUrlHint: 'Only http(s) URLs are allowed; remote hosts must use https to avoid exposing your API key.',
|
|
177
198
|
aiModel: 'Model',
|
|
178
199
|
aiApiKey: 'API Key',
|
|
200
|
+
aiDshImport: 'Import DSH model config',
|
|
201
|
+
aiDshImporting: 'Importing…',
|
|
202
|
+
aiDshImportOk: 'Imported Base URL and model; API key unchanged ✓',
|
|
203
|
+
aiDshImportFail: 'Import failed: {reason}',
|
|
204
|
+
aiDshImportEmpty: 'No current DSH default model config found',
|
|
179
205
|
aiConfigSave: 'Save config',
|
|
180
206
|
aiConfigSaved: 'Saved ✓',
|
|
181
207
|
aiClear: 'Clear config',
|
|
@@ -214,7 +240,7 @@ window.__ModuleLoader__.load({
|
|
|
214
240
|
wlTitle: 'Welcome to Settings Search',
|
|
215
241
|
wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
|
|
216
242
|
wlFeature1Title: 'Instant search',
|
|
217
|
-
wlFeature1Desc: '
|
|
243
|
+
wlFeature1Desc: 'Choose classic sidebar, top bar, or popup search page; typing filters settings.',
|
|
218
244
|
wlFeature2Title: 'Pinyin & intent hints',
|
|
219
245
|
wlFeature2Desc: 'Type pinyin initials (e.g. sz) or describe what you want to do to find settings.',
|
|
220
246
|
wlFeature3Title: 'AI hints (optional)',
|
|
@@ -224,9 +250,7 @@ window.__ModuleLoader__.load({
|
|
|
224
250
|
wlFeature5Title: 'Built-in panel',
|
|
225
251
|
wlFeature5Desc: 'The Settings Search page in the sidebar checks updates, shows notes, and exports logs.',
|
|
226
252
|
wlStart: 'Get started',
|
|
227
|
-
wlSkip: "Don't show again",
|
|
228
253
|
wlOpenPanel: 'Open Settings Search panel',
|
|
229
|
-
wlReopen: 'Onboarding',
|
|
230
254
|
wlFeature6Title: 'Auto-index',
|
|
231
255
|
wlFeature6Desc: 'On first open it automatically passes through every settings section, so in-page options are searchable without opening each page manually.',
|
|
232
256
|
}
|
|
@@ -278,28 +302,42 @@ window.__ModuleLoader__.load({
|
|
|
278
302
|
PINYIN_TABLE.split(/\s+/).filter(Boolean).map((pair) => [pair[0], pair.slice(1)])
|
|
279
303
|
)
|
|
280
304
|
const CJK_RE = /[\u4e00-\u9fff]/
|
|
305
|
+
const ALNUM_RE = /[a-zA-Z0-9]/
|
|
281
306
|
|
|
282
307
|
/**
|
|
283
308
|
* Transliterate a label into full-pinyin and initial-letter keys for
|
|
284
309
|
* association search. Returns null when any CJK character is outside
|
|
285
310
|
* the built-in table (then only substring matching applies).
|
|
311
|
+
*
|
|
312
|
+
* Results are memoized: the same labels and intent keywords are
|
|
313
|
+
* re-transliterated on every keystroke's re-filter, and this CJK loop
|
|
314
|
+
* is the hottest code in search. Cached objects are shared — callers
|
|
315
|
+
* must treat them as read-only.
|
|
286
316
|
*/
|
|
317
|
+
const PINYIN_KEY_CACHE = new Map()
|
|
287
318
|
const pinyinKeys = (text) => {
|
|
319
|
+
const cached = PINYIN_KEY_CACHE.get(text)
|
|
320
|
+
if (cached !== undefined) return cached
|
|
288
321
|
let full = ''
|
|
289
322
|
let initials = ''
|
|
290
323
|
for (const ch of text) {
|
|
291
324
|
if (CJK_RE.test(ch)) {
|
|
292
325
|
const py = PINYIN.get(ch)
|
|
293
|
-
if (py === undefined)
|
|
326
|
+
if (py === undefined) {
|
|
327
|
+
PINYIN_KEY_CACHE.set(text, null)
|
|
328
|
+
return null
|
|
329
|
+
}
|
|
294
330
|
full += py
|
|
295
331
|
initials += py[0]
|
|
296
|
-
} else if (
|
|
332
|
+
} else if (ALNUM_RE.test(ch)) {
|
|
297
333
|
const lower = ch.toLowerCase()
|
|
298
334
|
full += lower
|
|
299
335
|
initials += lower
|
|
300
336
|
}
|
|
301
337
|
}
|
|
302
|
-
|
|
338
|
+
const keys = full === '' ? null : { full, initials }
|
|
339
|
+
PINYIN_KEY_CACHE.set(text, keys)
|
|
340
|
+
return keys
|
|
303
341
|
}
|
|
304
342
|
|
|
305
343
|
/** Match one query token: raw substring, full pinyin, or initial letters. */
|
|
@@ -312,6 +350,17 @@ window.__ModuleLoader__.load({
|
|
|
312
350
|
return null
|
|
313
351
|
}
|
|
314
352
|
|
|
353
|
+
/** Cache the \b-wrapped keyword regexes used by related-keyword matching. */
|
|
354
|
+
const WORD_BOUNDARY_CACHE = new Map()
|
|
355
|
+
const wordBoundaryRegex = (kw) => {
|
|
356
|
+
let re = WORD_BOUNDARY_CACHE.get(kw)
|
|
357
|
+
if (re === undefined) {
|
|
358
|
+
re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
|
|
359
|
+
WORD_BOUNDARY_CACHE.set(kw, re)
|
|
360
|
+
}
|
|
361
|
+
return re
|
|
362
|
+
}
|
|
363
|
+
|
|
315
364
|
/** Character count of the label consumed by a full-pinyin prefix. */
|
|
316
365
|
const pinyinPrefixChars = (label, pinyinLength) => {
|
|
317
366
|
let consumed = 0
|
|
@@ -360,8 +409,14 @@ window.__ModuleLoader__.load({
|
|
|
360
409
|
return { phrase, keywords: entry.slice(at + 1).split(','), keys: pinyinKeys(phrase) }
|
|
361
410
|
})
|
|
362
411
|
|
|
363
|
-
/**
|
|
412
|
+
/**
|
|
413
|
+
* Keywords a query token should additionally search after an intent hit.
|
|
414
|
+
* Memoized per token — tokens repeat on every keystroke's re-filter.
|
|
415
|
+
*/
|
|
416
|
+
const INTENT_KEYWORD_CACHE = new Map()
|
|
364
417
|
const intentKeywords = (tok) => {
|
|
418
|
+
const cached = INTENT_KEYWORD_CACHE.get(tok)
|
|
419
|
+
if (cached !== undefined) return cached
|
|
365
420
|
const hits = []
|
|
366
421
|
for (const intent of INTENTS) {
|
|
367
422
|
const phraseHit = intent.phrase.includes(tok) || tok.includes(intent.phrase) ||
|
|
@@ -373,6 +428,7 @@ window.__ModuleLoader__.load({
|
|
|
373
428
|
}
|
|
374
429
|
}
|
|
375
430
|
}
|
|
431
|
+
INTENT_KEYWORD_CACHE.set(tok, hits)
|
|
376
432
|
return hits
|
|
377
433
|
}
|
|
378
434
|
|
|
@@ -407,6 +463,24 @@ window.__ModuleLoader__.load({
|
|
|
407
463
|
const AI_BASE_URL_KEY = 'dsh-settings-search:ai-base-url'
|
|
408
464
|
const AI_MODEL_KEY = 'dsh-settings-search:ai-model'
|
|
409
465
|
const AI_API_KEY_KEY = 'dsh-settings-search:ai-api-key'
|
|
466
|
+
/** Session-only API key storage avoids durable browser storage exposure. */
|
|
467
|
+
const readApiKey = () => {
|
|
468
|
+
const legacy = typeof localStorage !== 'undefined' ? (localStorage.getItem(AI_API_KEY_KEY) ?? '').trim() : ''
|
|
469
|
+
if (legacy !== '') {
|
|
470
|
+
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(AI_API_KEY_KEY, legacy)
|
|
471
|
+
if (typeof localStorage !== 'undefined') localStorage.removeItem(AI_API_KEY_KEY)
|
|
472
|
+
return legacy
|
|
473
|
+
}
|
|
474
|
+
return typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem(AI_API_KEY_KEY) ?? '').trim() : ''
|
|
475
|
+
}
|
|
476
|
+
const writeApiKey = (value) => {
|
|
477
|
+
const apiKey = String(value ?? '').trim()
|
|
478
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
479
|
+
if (apiKey === '') sessionStorage.removeItem(AI_API_KEY_KEY)
|
|
480
|
+
else sessionStorage.setItem(AI_API_KEY_KEY, apiKey)
|
|
481
|
+
}
|
|
482
|
+
if (typeof localStorage !== 'undefined') localStorage.removeItem(AI_API_KEY_KEY)
|
|
483
|
+
}
|
|
410
484
|
const aiSearchEnabled = () => {
|
|
411
485
|
if (typeof localStorage === 'undefined') return false
|
|
412
486
|
return localStorage.getItem(AI_STORAGE_KEY) === '1'
|
|
@@ -417,26 +491,53 @@ window.__ModuleLoader__.load({
|
|
|
417
491
|
window.dispatchEvent(new CustomEvent(AI_TOGGLE_EVENT, { detail: enabled }))
|
|
418
492
|
}
|
|
419
493
|
const aiConfig = () => {
|
|
420
|
-
if (typeof localStorage === 'undefined'
|
|
421
|
-
|
|
494
|
+
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') {
|
|
495
|
+
return { provider: 'openai', baseUrl: '', model: '', apiKey: '' }
|
|
496
|
+
}
|
|
497
|
+
const providerRaw = (localStorage?.getItem(AI_PROVIDER_KEY) ?? 'openai').trim()
|
|
422
498
|
return {
|
|
423
499
|
provider: providerRaw === 'anthropic' ? 'anthropic' : 'openai',
|
|
424
|
-
baseUrl: (localStorage
|
|
425
|
-
model: (localStorage
|
|
426
|
-
apiKey: (
|
|
500
|
+
baseUrl: (localStorage?.getItem(AI_BASE_URL_KEY) ?? '').trim(),
|
|
501
|
+
model: (localStorage?.getItem(AI_MODEL_KEY) ?? '').trim(),
|
|
502
|
+
apiKey: readApiKey(),
|
|
427
503
|
}
|
|
428
504
|
}
|
|
429
505
|
const setAiConfig = (cfg) => {
|
|
506
|
+
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') return
|
|
507
|
+
writeApiKey(cfg.apiKey)
|
|
430
508
|
if (typeof localStorage === 'undefined') return
|
|
431
509
|
localStorage.setItem(AI_PROVIDER_KEY, cfg.provider === 'anthropic' ? 'anthropic' : 'openai')
|
|
432
510
|
localStorage.setItem(AI_BASE_URL_KEY, cfg.baseUrl ?? '')
|
|
433
511
|
localStorage.setItem(AI_MODEL_KEY, cfg.model ?? '')
|
|
434
|
-
localStorage.setItem(AI_API_KEY_KEY, cfg.apiKey ?? '')
|
|
435
512
|
window.dispatchEvent(new CustomEvent(AI_CONFIG_EVENT))
|
|
436
513
|
}
|
|
437
514
|
/** First-run onboarding: remembered via localStorage so it shows once. */
|
|
438
515
|
const ONBOARD_KEY = 'dsh-settings-search:onboarded'
|
|
439
516
|
const WELCOME_EVENT = 'dsh-settings-search:welcome'
|
|
517
|
+
const SEARCH_STYLE_KEY = 'dsh-settings-search:style'
|
|
518
|
+
const SEARCH_STYLE_EVENT = 'dsh-settings-search:style-change'
|
|
519
|
+
const SEARCH_SHORTCUT_KEY = 'dsh-settings-search:shortcut-enabled'
|
|
520
|
+
const SEARCH_SHORTCUT_EVENT = 'dsh-settings-search:shortcut-change'
|
|
521
|
+
const SEARCH_STYLES = ['sidebar', 'top', 'page']
|
|
522
|
+
const readSearchStyle = () => {
|
|
523
|
+
if (typeof localStorage === 'undefined') return 'sidebar'
|
|
524
|
+
const style = localStorage.getItem(SEARCH_STYLE_KEY)
|
|
525
|
+
return SEARCH_STYLES.includes(style) ? style : 'sidebar'
|
|
526
|
+
}
|
|
527
|
+
const setStoredSearchStyle = (style) => {
|
|
528
|
+
if (typeof localStorage === 'undefined' || !SEARCH_STYLES.includes(style)) return
|
|
529
|
+
localStorage.setItem(SEARCH_STYLE_KEY, style)
|
|
530
|
+
window.dispatchEvent(new CustomEvent(SEARCH_STYLE_EVENT, { detail: style }))
|
|
531
|
+
}
|
|
532
|
+
const searchShortcutEnabled = () => {
|
|
533
|
+
if (typeof localStorage === 'undefined') return true
|
|
534
|
+
return localStorage.getItem(SEARCH_SHORTCUT_KEY) !== '0'
|
|
535
|
+
}
|
|
536
|
+
const setSearchShortcutEnabled = (enabled) => {
|
|
537
|
+
if (typeof localStorage === 'undefined') return
|
|
538
|
+
localStorage.setItem(SEARCH_SHORTCUT_KEY, enabled ? '1' : '0')
|
|
539
|
+
window.dispatchEvent(new CustomEvent(SEARCH_SHORTCUT_EVENT, { detail: enabled }))
|
|
540
|
+
}
|
|
440
541
|
const welcomed = () => {
|
|
441
542
|
if (typeof localStorage === 'undefined') return true
|
|
442
543
|
return localStorage.getItem(ONBOARD_KEY) === '1'
|
|
@@ -446,9 +547,6 @@ window.__ModuleLoader__.load({
|
|
|
446
547
|
localStorage.setItem(ONBOARD_KEY, '1')
|
|
447
548
|
window.dispatchEvent(new CustomEvent(WELCOME_EVENT, { detail: false }))
|
|
448
549
|
}
|
|
449
|
-
const openWelcome = () => {
|
|
450
|
-
window.dispatchEvent(new CustomEvent(WELCOME_EVENT, { detail: true }))
|
|
451
|
-
}
|
|
452
550
|
const extractAiKeywords = (raw) => {
|
|
453
551
|
const text = String(raw ?? '').replace(/^```(?:json)?/i, '').replace(/```$/, '').trim()
|
|
454
552
|
const start = text.indexOf('{')
|
|
@@ -489,17 +587,17 @@ window.__ModuleLoader__.load({
|
|
|
489
587
|
return url.origin + path.replace(/\/+$/, '')
|
|
490
588
|
}
|
|
491
589
|
const chatCompletionsUrl = (baseUrl) => {
|
|
590
|
+
// safeBaseUrl already strips trailing slashes
|
|
492
591
|
const safe = safeBaseUrl(baseUrl)
|
|
493
592
|
if (!safe) return ''
|
|
494
|
-
|
|
495
|
-
return /\/chat\/completions$/i.test(trimmed) ? trimmed : `${trimmed}/chat/completions`
|
|
593
|
+
return /\/chat\/completions$/i.test(safe) ? safe : `${safe}/chat/completions`
|
|
496
594
|
}
|
|
497
595
|
const anthropicMessagesUrl = (baseUrl) => {
|
|
596
|
+
// safeBaseUrl already strips trailing slashes
|
|
498
597
|
const safe = safeBaseUrl(baseUrl)
|
|
499
598
|
if (!safe) return ''
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return /\/v1$/i.test(trimmed) ? `${trimmed}/messages` : `${trimmed}/v1/messages`
|
|
599
|
+
if (/\/v1\/messages$/i.test(safe)) return safe
|
|
600
|
+
return /\/v1$/i.test(safe) ? `${safe}/messages` : `${safe}/v1/messages`
|
|
503
601
|
}
|
|
504
602
|
const requestUserAi = async (query, labels, signal) => {
|
|
505
603
|
const { provider, baseUrl, model, apiKey } = aiConfig()
|
|
@@ -545,6 +643,7 @@ window.__ModuleLoader__.load({
|
|
|
545
643
|
method: 'POST',
|
|
546
644
|
headers,
|
|
547
645
|
signal,
|
|
646
|
+
redirect: 'error',
|
|
548
647
|
body: JSON.stringify(body),
|
|
549
648
|
})
|
|
550
649
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
@@ -573,7 +672,11 @@ window.__ModuleLoader__.load({
|
|
|
573
672
|
const cacheKey = query.toLowerCase()
|
|
574
673
|
if (AI_CACHE.has(cacheKey)) return AI_CACHE.get(cacheKey)
|
|
575
674
|
const keywords = await requestUserAi(query, labels, signal)
|
|
576
|
-
if (keywords.length > 0)
|
|
675
|
+
if (keywords.length > 0) {
|
|
676
|
+
// bound the cache so a long session cannot grow it without limit
|
|
677
|
+
if (AI_CACHE.size >= 100) AI_CACHE.clear()
|
|
678
|
+
AI_CACHE.set(cacheKey, keywords)
|
|
679
|
+
}
|
|
577
680
|
return keywords
|
|
578
681
|
}
|
|
579
682
|
|
|
@@ -583,16 +686,17 @@ window.__ModuleLoader__.load({
|
|
|
583
686
|
* Returns { ok, code, detail } where code is a stable error kind used to
|
|
584
687
|
* look up a localized hint in the UI.
|
|
585
688
|
*/
|
|
689
|
+
const HTTP_TEST_CODES = {
|
|
690
|
+
400: 'bad-request',
|
|
691
|
+
401: 'auth',
|
|
692
|
+
403: 'forbidden',
|
|
693
|
+
404: 'not-found',
|
|
694
|
+
408: 'timeout',
|
|
695
|
+
429: 'rate-limit',
|
|
696
|
+
}
|
|
586
697
|
const testHttpCode = (status) => {
|
|
587
698
|
const code = Number(status)
|
|
588
|
-
|
|
589
|
-
if (code === 403) return 'forbidden'
|
|
590
|
-
if (code === 404) return 'not-found'
|
|
591
|
-
if (code === 408) return 'timeout'
|
|
592
|
-
if (code === 429) return 'rate-limit'
|
|
593
|
-
if (code === 400) return 'bad-request'
|
|
594
|
-
if (code >= 500 && code <= 599) return 'server'
|
|
595
|
-
return 'http'
|
|
699
|
+
return HTTP_TEST_CODES[code] ?? (code >= 500 && code <= 599 ? 'server' : 'http')
|
|
596
700
|
}
|
|
597
701
|
const testAiService = async (cfg) => {
|
|
598
702
|
const provider = cfg.provider === 'anthropic' ? 'anthropic' : 'openai'
|
|
@@ -626,6 +730,7 @@ window.__ModuleLoader__.load({
|
|
|
626
730
|
const response = await fetch(requestUrl, {
|
|
627
731
|
method: 'POST',
|
|
628
732
|
headers,
|
|
733
|
+
redirect: 'error',
|
|
629
734
|
body: JSON.stringify(body),
|
|
630
735
|
})
|
|
631
736
|
if (!response.ok) return { ok: false, code: testHttpCode(response.status), detail: `HTTP ${response.status}` }
|
|
@@ -687,6 +792,24 @@ window.__ModuleLoader__.load({
|
|
|
687
792
|
.sss-ai-note { margin: 0; padding: 6px 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
688
793
|
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
689
794
|
.sss-guide { padding: 7px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
|
|
795
|
+
.sss-wrap.sss-top { position: absolute; top: 20px; left: 198px; right: 152px; z-index: 950; width: auto; height: 28px; gap: 8px; }
|
|
796
|
+
.sss-top .sss-title { display: none; }
|
|
797
|
+
.sss-top .sss-box { height: 28px; padding: 0 8px; border-radius: 8px; border-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-shadow-lv2); }
|
|
798
|
+
.sss-top .sss-input { font-size: 15px; }
|
|
799
|
+
.sss-top .sss-top-trigger { height: 28px; padding: 0 8px; border-color: var(--dsw-alias-border-l1); box-shadow: var(--dsw-shadow-lv2); font-size: 15px; }
|
|
800
|
+
.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
|
+
.sss-trigger:hover { border-color: var(--dsw-alias-label-dimmed); color: var(--dsw-alias-label-secondary); }
|
|
802
|
+
.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: 900; 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
|
+
.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
|
+
.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
|
+
.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); }
|
|
807
|
+
.sss-page-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
808
|
+
.sss-page-close { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary); cursor: pointer; }
|
|
809
|
+
.sss-page-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
810
|
+
.sss-page-results { flex: 1; min-height: 0; overflow: auto; padding: 8px; }
|
|
811
|
+
.sss-page-results .sss-group { padding: 2px; }
|
|
812
|
+
.sss-page-results .sss-item { padding: 10px 8px; border-radius: 8px; }
|
|
690
813
|
@keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
|
|
691
814
|
.sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
|
|
692
815
|
.sss-vr { list-style: none; border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); border-radius: 12px; max-width: 780px; width: 100%; box-sizing: border-box; transition: border-color .16s, background .16s; }
|
|
@@ -717,6 +840,10 @@ window.__ModuleLoader__.load({
|
|
|
717
840
|
.sss-vr-btn:hover:not(:disabled) { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
718
841
|
.sss-vr-btn:disabled { opacity: .4; cursor: default; }
|
|
719
842
|
.sss-vr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
|
|
843
|
+
.sss-vr-segment { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
844
|
+
.sss-vr-seg { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 6px 10px; font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); background: 0 0; }
|
|
845
|
+
.sss-vr-seg:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
846
|
+
.sss-vr-seg.is-active { color: var(--dsw-alias-bg-layer-3); background: var(--dsw-alias-label-primary); border-color: transparent; }
|
|
720
847
|
.sss-vr-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
721
848
|
.sss-vr-primary:hover:not(:disabled) { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
722
849
|
.sss-vr-badge { white-space: nowrap; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-secondary); border-radius: 999px; padding: 1px 8px; font-size: 11px; font-weight: 500; line-height: 17px; }
|
|
@@ -797,7 +924,6 @@ window.__ModuleLoader__.load({
|
|
|
797
924
|
function createModel(ctx) {
|
|
798
925
|
/** id → harvested visible title, per slot. */
|
|
799
926
|
const harvest = new Map()
|
|
800
|
-
let harvestSeq = 0
|
|
801
927
|
|
|
802
928
|
const entriesOf = (slot) => {
|
|
803
929
|
try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
|
|
@@ -834,7 +960,7 @@ window.__ModuleLoader__.load({
|
|
|
834
960
|
const trimDescription = (text) => {
|
|
835
961
|
const t = (text ?? '').replace(/\s+/g, ' ').trim()
|
|
836
962
|
if (t === '') return ''
|
|
837
|
-
const re = /[\s,,。;;](关闭后|开启后|启用后|禁用后|创建后|使用时|点击后|勾选后|开启时|关闭时|完成后|启用时|禁用时|开始后|停止后)/
|
|
963
|
+
const re = /(?:[\s,,。;;])?(关闭后|开启后|启用后|禁用后|创建后|使用时|点击后|勾选后|开启时|关闭时|完成后|启用时|禁用时|开始后|停止后)/
|
|
838
964
|
const m = re.exec(t)
|
|
839
965
|
if (m) {
|
|
840
966
|
const head = t.slice(0, m.index).trim()
|
|
@@ -914,7 +1040,7 @@ window.__ModuleLoader__.load({
|
|
|
914
1040
|
if (containers.length === 1 && containers[0].childElementCount > 0) {
|
|
915
1041
|
nodes = [...containers[0].children]
|
|
916
1042
|
} else {
|
|
917
|
-
nodes = containers
|
|
1043
|
+
nodes = containers
|
|
918
1044
|
}
|
|
919
1045
|
if (nodes.length === 0) continue
|
|
920
1046
|
map = harvest.get(cfg.slot)
|
|
@@ -972,7 +1098,6 @@ window.__ModuleLoader__.load({
|
|
|
972
1098
|
}
|
|
973
1099
|
}
|
|
974
1100
|
} catch { /* section mining is best-effort */ }
|
|
975
|
-
if (grew) harvestSeq += 1
|
|
976
1101
|
return grew
|
|
977
1102
|
}
|
|
978
1103
|
|
|
@@ -1008,6 +1133,10 @@ window.__ModuleLoader__.load({
|
|
|
1008
1133
|
const parentLabel = (parentId) =>
|
|
1009
1134
|
sections.find((s) => s.id === parentId)?.label ?? parentId
|
|
1010
1135
|
const out = []
|
|
1136
|
+
// Set-based dedup: keys and "parentLabel\u0000label" pairs replace the
|
|
1137
|
+
// former out.some(...) scans, which were O(n²) on every keystroke.
|
|
1138
|
+
const seenKeys = new Set()
|
|
1139
|
+
const seenPairs = new Set()
|
|
1011
1140
|
for (const cfg of DEEP_SLOTS) {
|
|
1012
1141
|
const mapped = harvest.get(cfg.slot)
|
|
1013
1142
|
sortEntries(entriesOf(cfg.slot)).forEach((entry, i) => {
|
|
@@ -1020,8 +1149,10 @@ window.__ModuleLoader__.load({
|
|
|
1020
1149
|
const label = staticLabel(entry, cfg) || hitObj?.title || ''
|
|
1021
1150
|
if (label === '') return // unmapped, unharvested — invisible until it renders once
|
|
1022
1151
|
const key = cfg.slot + '#' + id
|
|
1023
|
-
if (
|
|
1152
|
+
if (seenKeys.has(key)) return
|
|
1024
1153
|
const pageLabel = parentLabel(cfg.parent)
|
|
1154
|
+
seenKeys.add(key)
|
|
1155
|
+
seenPairs.add(pageLabel + '\u0000' + label)
|
|
1025
1156
|
out.push({
|
|
1026
1157
|
kind: 'option',
|
|
1027
1158
|
key,
|
|
@@ -1038,7 +1169,9 @@ window.__ModuleLoader__.load({
|
|
|
1038
1169
|
hitObj.inner.forEach((op, oi) => {
|
|
1039
1170
|
if (op.label === '' || op.label === label) return
|
|
1040
1171
|
const innerKey = key + '#o' + oi
|
|
1041
|
-
if (
|
|
1172
|
+
if (seenKeys.has(innerKey)) return
|
|
1173
|
+
seenKeys.add(innerKey)
|
|
1174
|
+
seenPairs.add(label + '\u0000' + op.label)
|
|
1042
1175
|
out.push({
|
|
1043
1176
|
kind: 'option',
|
|
1044
1177
|
key: innerKey,
|
|
@@ -1072,9 +1205,11 @@ window.__ModuleLoader__.load({
|
|
|
1072
1205
|
rec.inner.forEach((op, oi) => {
|
|
1073
1206
|
if (op.label === '' || op.label === label) return
|
|
1074
1207
|
const innerKey = 'settings.section#' + id + '#o' + oi
|
|
1075
|
-
if (
|
|
1076
|
-
const
|
|
1077
|
-
if (
|
|
1208
|
+
if (seenKeys.has(innerKey)) return
|
|
1209
|
+
const pair = label + '\u0000' + op.label
|
|
1210
|
+
if (seenPairs.has(pair)) return
|
|
1211
|
+
seenKeys.add(innerKey)
|
|
1212
|
+
seenPairs.add(pair)
|
|
1078
1213
|
out.push({
|
|
1079
1214
|
kind: 'option',
|
|
1080
1215
|
key: innerKey,
|
|
@@ -1099,20 +1234,31 @@ window.__ModuleLoader__.load({
|
|
|
1099
1234
|
: a.order - b.order)
|
|
1100
1235
|
}
|
|
1101
1236
|
|
|
1102
|
-
return { readSections, readDeep, runHarvest
|
|
1237
|
+
return { readSections, readDeep, runHarvest }
|
|
1103
1238
|
}
|
|
1104
1239
|
|
|
1105
1240
|
/* ---------- DOM-level navigation into the settings shell ---------- */
|
|
1106
1241
|
|
|
1242
|
+
/**
|
|
1243
|
+
* Root element that hosts the settings panel's nav buttons. Derived from
|
|
1244
|
+
* the plugin's own mounted UI so an unrelated or plugin-owned dialog can
|
|
1245
|
+
* never become the click root; the first-dialog fallback only applies
|
|
1246
|
+
* when the plugin's anchors are not mounted.
|
|
1247
|
+
*/
|
|
1248
|
+
const panelRoot = () => {
|
|
1249
|
+
if (typeof document === 'undefined') return null
|
|
1250
|
+
const anchor = document.querySelector('.sss-wrap, .sss-vr')
|
|
1251
|
+
if (anchor) return anchor.closest('[role="dialog"]') ?? document.body
|
|
1252
|
+
return document.querySelector('[role="dialog"]') ?? document.body
|
|
1253
|
+
}
|
|
1107
1254
|
const panelButtons = () => {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return [...root.querySelectorAll('button')]
|
|
1255
|
+
const root = panelRoot()
|
|
1256
|
+
return root === null ? [] : [...root.querySelectorAll('button')]
|
|
1111
1257
|
}
|
|
1112
1258
|
const clickNavButton = (wantText) => {
|
|
1113
1259
|
const want = String(wantText ?? '').trim()
|
|
1114
1260
|
if (want === '') return false
|
|
1115
|
-
const buttons = panelButtons()
|
|
1261
|
+
const buttons = panelButtons().filter((b) => /_navCell/.test(String(b.className || '')))
|
|
1116
1262
|
const textOf = (b) => (b.textContent ?? '').trim()
|
|
1117
1263
|
const hit = buttons.find((b) => textOf(b) === want)
|
|
1118
1264
|
if (!hit) return false
|
|
@@ -1122,8 +1268,8 @@ window.__ModuleLoader__.load({
|
|
|
1122
1268
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1123
1269
|
/** Label of the currently-active first-level settings nav item. */
|
|
1124
1270
|
const activeSectionLabel = () => {
|
|
1125
|
-
|
|
1126
|
-
|
|
1271
|
+
const root = panelRoot()
|
|
1272
|
+
if (root === null) return ''
|
|
1127
1273
|
const isTab = (b) => b.getAttribute('role') === 'tab'
|
|
1128
1274
|
const buttons = [...root.querySelectorAll('button')].filter((b) => !isTab(b))
|
|
1129
1275
|
const byCurrent = buttons.find((b) => b.getAttribute('aria-current') === 'true')
|
|
@@ -1153,11 +1299,9 @@ window.__ModuleLoader__.load({
|
|
|
1153
1299
|
model.runHarvest()
|
|
1154
1300
|
visited += 1
|
|
1155
1301
|
}
|
|
1156
|
-
if (original
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
model.runHarvest()
|
|
1160
|
-
}
|
|
1302
|
+
if (clickNavButton(original)) {
|
|
1303
|
+
await delay(160)
|
|
1304
|
+
model.runHarvest()
|
|
1161
1305
|
}
|
|
1162
1306
|
} finally {
|
|
1163
1307
|
if (typeof onDone === 'function') onDone()
|
|
@@ -1168,13 +1312,10 @@ window.__ModuleLoader__.load({
|
|
|
1168
1312
|
|
|
1169
1313
|
function createNavigator(ctx) {
|
|
1170
1314
|
/** Jump to a page result; falls back to showing a manual hint. */
|
|
1171
|
-
const jumpToPage = async (item) =>
|
|
1172
|
-
const ok = await Promise.resolve(clickNavButton(item.label))
|
|
1173
|
-
return ok
|
|
1174
|
-
}
|
|
1315
|
+
const jumpToPage = async (item) => clickNavButton(item.label)
|
|
1175
1316
|
/** Jump to an in-page option: open its page, select its tab, flash its row. */
|
|
1176
1317
|
const jumpToOption = async (item) => {
|
|
1177
|
-
const opened =
|
|
1318
|
+
const opened = clickNavButton(item.pageLabel ?? item.parentLabel)
|
|
1178
1319
|
if (opened !== true) { flashOptionRow(item); return false }
|
|
1179
1320
|
await delay(160)
|
|
1180
1321
|
const label = item.label
|
|
@@ -1242,6 +1383,36 @@ window.__ModuleLoader__.load({
|
|
|
1242
1383
|
const model = createModel(ctx)
|
|
1243
1384
|
const navigator = createNavigator(ctx)
|
|
1244
1385
|
|
|
1386
|
+
/** Read the model fields from DSH; credentials are never requested. */
|
|
1387
|
+
const readDshModelConfig = async () => {
|
|
1388
|
+
const connection = ctx.get('connection')
|
|
1389
|
+
const response = await connection.api.settings.describe({})
|
|
1390
|
+
if (!response.result.ok) throw new Error(response.result.error?.message || 'settings.describe failed')
|
|
1391
|
+
const namespaces = new Map(response.result.value.namespaces.map((view) => [view.ns, view]))
|
|
1392
|
+
const defaultModel = namespaces.get('agent-default-model')?.value ?? {}
|
|
1393
|
+
const providerId = String(defaultModel.provider ?? '').trim()
|
|
1394
|
+
const modelId = String(defaultModel.model ?? '').trim()
|
|
1395
|
+
const readPath = (value, path) => (path ?? []).reduce((current, key) => current?.[key], value)
|
|
1396
|
+
const providersResponse = await connection.api.llm.providers({}).catch(() => null)
|
|
1397
|
+
const providerRow = providersResponse?.result?.ok
|
|
1398
|
+
? providersResponse.result.value.providers.find((entry) => entry.provider === providerId)
|
|
1399
|
+
: undefined
|
|
1400
|
+
const modelNamespace = namespaces.get(providerRow?.settingsNs || 'llm-pi-ai')
|
|
1401
|
+
const providerProfile = providerRow
|
|
1402
|
+
? readPath(modelNamespace?.value, providerRow.settingsPath) ?? {}
|
|
1403
|
+
: modelNamespace?.value?.providers?.[providerId] ?? {}
|
|
1404
|
+
const configuredModels = Array.isArray(providerProfile.models) ? providerProfile.models : []
|
|
1405
|
+
const modelProfile = configuredModels.find((entry) => entry?.id === modelId) ?? {}
|
|
1406
|
+
const protocol = String(modelProfile.api ?? providerProfile.api ?? '').trim()
|
|
1407
|
+
const baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1408
|
+
if (!modelId || !baseUrl) return null
|
|
1409
|
+
return {
|
|
1410
|
+
provider: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1411
|
+
baseUrl,
|
|
1412
|
+
model: modelId,
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1245
1416
|
const SearchIcon = React.createElement('svg', {
|
|
1246
1417
|
className: 'sss-icon',
|
|
1247
1418
|
viewBox: '0 0 16.5 16.5',
|
|
@@ -1295,7 +1466,6 @@ window.__ModuleLoader__.load({
|
|
|
1295
1466
|
React.createElement('div', { className: 'sss-wl-footer' }, [
|
|
1296
1467
|
React.createElement('button', { className: 'sss-wl-btn sss-wl-primary', type: 'button', onClick: onClose }, t('wlStart')),
|
|
1297
1468
|
React.createElement('button', { className: 'sss-wl-btn', type: 'button', onClick: onOpenPanel }, t('wlOpenPanel')),
|
|
1298
|
-
React.createElement('button', { className: 'sss-wl-btn sss-wl-skip', type: 'button', onClick: onClose }, t('wlSkip')),
|
|
1299
1469
|
]),
|
|
1300
1470
|
]),
|
|
1301
1471
|
])
|
|
@@ -1326,6 +1496,9 @@ window.__ModuleLoader__.load({
|
|
|
1326
1496
|
function SettingsHeaderSearch() {
|
|
1327
1497
|
const [query, setQuery] = React.useState('')
|
|
1328
1498
|
const [open, setOpen] = React.useState(false)
|
|
1499
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
1500
|
+
const [pageOpen, setPageOpen] = React.useState(false)
|
|
1501
|
+
const [shortcutEnabled, setShortcutEnabled] = React.useState(searchShortcutEnabled)
|
|
1329
1502
|
const [showWelcome, setShowWelcome] = React.useState(() => !welcomed())
|
|
1330
1503
|
const [tick, setTick] = React.useState(0)
|
|
1331
1504
|
const [warming, setWarming] = React.useState(false)
|
|
@@ -1343,17 +1516,29 @@ window.__ModuleLoader__.load({
|
|
|
1343
1516
|
ctx.locale.subscribe(forceTick),
|
|
1344
1517
|
]
|
|
1345
1518
|
// Grow the index whenever the panel body re-renders (visits new pages).
|
|
1519
|
+
// DOM mutations arrive in bursts (animations, list mounts), so the
|
|
1520
|
+
// harvest pass runs at most once per throttle window (trailing edge).
|
|
1346
1521
|
let warmAlive = true
|
|
1347
1522
|
let warmAttempts = 0
|
|
1523
|
+
let mutatorAlive = true
|
|
1524
|
+
let throttleTimer = null
|
|
1525
|
+
const onMutate = () => {
|
|
1526
|
+
if (!mutatorAlive) return
|
|
1527
|
+
if (model.runHarvest()) setTick((x) => x + 1)
|
|
1528
|
+
if (!warmed && activeSectionLabel() !== '') {
|
|
1529
|
+
void warmAllSections(model, () => setWarming(true), () => setWarming(false)).then((done) => {
|
|
1530
|
+
if (!warmAlive) return
|
|
1531
|
+
if (done) setTick((x) => x + 1)
|
|
1532
|
+
})
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1348
1535
|
const observer = typeof MutationObserver === 'function'
|
|
1349
1536
|
? new MutationObserver(() => {
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
})
|
|
1356
|
-
}
|
|
1537
|
+
if (throttleTimer !== null) return
|
|
1538
|
+
throttleTimer = setTimeout(() => {
|
|
1539
|
+
throttleTimer = null
|
|
1540
|
+
onMutate()
|
|
1541
|
+
}, 120)
|
|
1357
1542
|
})
|
|
1358
1543
|
: null
|
|
1359
1544
|
observer?.observe(document.body, { childList: true, subtree: true })
|
|
@@ -1372,9 +1557,11 @@ window.__ModuleLoader__.load({
|
|
|
1372
1557
|
}, 800)
|
|
1373
1558
|
return () => {
|
|
1374
1559
|
warmAlive = false
|
|
1560
|
+
mutatorAlive = false
|
|
1375
1561
|
for (const off of offs) off()
|
|
1376
1562
|
observer?.disconnect()
|
|
1377
1563
|
clearTimeout(warm)
|
|
1564
|
+
if (throttleTimer !== null) clearTimeout(throttleTimer)
|
|
1378
1565
|
}
|
|
1379
1566
|
}, [])
|
|
1380
1567
|
|
|
@@ -1384,6 +1571,27 @@ window.__ModuleLoader__.load({
|
|
|
1384
1571
|
return () => window.removeEventListener(WELCOME_EVENT, onWelcome)
|
|
1385
1572
|
}, [])
|
|
1386
1573
|
|
|
1574
|
+
React.useEffect(() => {
|
|
1575
|
+
const sync = () => setSearchStyleState(readSearchStyle())
|
|
1576
|
+
window.addEventListener(SEARCH_STYLE_EVENT, sync)
|
|
1577
|
+
return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
|
|
1578
|
+
}, [])
|
|
1579
|
+
|
|
1580
|
+
// A style switch must not carry an open popup from the previous layout
|
|
1581
|
+
// into the new one. The host can keep this component mounted while the
|
|
1582
|
+
// settings choice changes, so reset transient view state explicitly.
|
|
1583
|
+
React.useEffect(() => {
|
|
1584
|
+
setOpen(false)
|
|
1585
|
+
setPageOpen(false)
|
|
1586
|
+
setHint(null)
|
|
1587
|
+
}, [searchStyle])
|
|
1588
|
+
|
|
1589
|
+
React.useEffect(() => {
|
|
1590
|
+
const sync = () => setShortcutEnabled(searchShortcutEnabled())
|
|
1591
|
+
window.addEventListener(SEARCH_SHORTCUT_EVENT, sync)
|
|
1592
|
+
return () => window.removeEventListener(SEARCH_SHORTCUT_EVENT, sync)
|
|
1593
|
+
}, [])
|
|
1594
|
+
|
|
1387
1595
|
React.useEffect(() => {
|
|
1388
1596
|
const sync = () => setAiEnabled(aiSearchEnabled())
|
|
1389
1597
|
const onConfig = () => forceTick()
|
|
@@ -1406,7 +1614,7 @@ window.__ModuleLoader__.load({
|
|
|
1406
1614
|
tokens.every((tok) => tokenMatch(haystackLower, item.keys, tok) !== null)
|
|
1407
1615
|
const kwMatch = (item, haystackLower, kw) => {
|
|
1408
1616
|
if (/^[\x00-\x7f]+$/.test(kw)) {
|
|
1409
|
-
if (
|
|
1617
|
+
if (wordBoundaryRegex(kw).test(haystackLower)) return true
|
|
1410
1618
|
} else if (haystackLower.includes(kw)) return true
|
|
1411
1619
|
if (item.keys === null) return false
|
|
1412
1620
|
const kwKeys = pinyinKeys(kw)
|
|
@@ -1498,15 +1706,40 @@ window.__ModuleLoader__.load({
|
|
|
1498
1706
|
const [active, setActive] = React.useState(0)
|
|
1499
1707
|
const popRef = React.useRef(null)
|
|
1500
1708
|
const inputRef = React.useRef(null)
|
|
1709
|
+
const pageMode = searchStyle === 'page'
|
|
1710
|
+
const topMode = searchStyle === 'top'
|
|
1501
1711
|
const activeIndex = Math.min(active, Math.max(results.length - 1, 0))
|
|
1502
1712
|
|
|
1503
1713
|
React.useEffect(() => { setActive(0) }, [query])
|
|
1714
|
+
React.useEffect(() => {
|
|
1715
|
+
if (!pageMode || !pageOpen) return
|
|
1716
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 0)
|
|
1717
|
+
return () => clearTimeout(timer)
|
|
1718
|
+
}, [pageMode, pageOpen])
|
|
1719
|
+
React.useEffect(() => {
|
|
1720
|
+
if (!shortcutEnabled) return
|
|
1721
|
+
const onKeyDown = (event) => {
|
|
1722
|
+
if (!event.ctrlKey && !event.metaKey) return
|
|
1723
|
+
if (event.altKey || event.shiftKey || event.key !== 'f' && event.key !== 'F') return
|
|
1724
|
+
event.preventDefault()
|
|
1725
|
+
if (pageMode) {
|
|
1726
|
+
setPageOpen(true)
|
|
1727
|
+
return
|
|
1728
|
+
}
|
|
1729
|
+
setHint(null)
|
|
1730
|
+
setOpen(true)
|
|
1731
|
+
inputRef.current?.focus()
|
|
1732
|
+
}
|
|
1733
|
+
window.addEventListener('keydown', onKeyDown)
|
|
1734
|
+
return () => window.removeEventListener('keydown', onKeyDown)
|
|
1735
|
+
}, [pageMode, shortcutEnabled])
|
|
1504
1736
|
React.useEffect(() => {
|
|
1505
1737
|
const el = popRef.current?.querySelector('.sss-item.is-active')
|
|
1506
1738
|
el?.scrollIntoView?.({ block: 'nearest' })
|
|
1507
1739
|
}, [activeIndex, q])
|
|
1508
1740
|
const pick = (item) => {
|
|
1509
1741
|
setOpen(false)
|
|
1742
|
+
setPageOpen(false)
|
|
1510
1743
|
log('info', '打开设置', { kind: item.kind, id: item.id || item.key || '', label: String(item.label).slice(0, 60) })
|
|
1511
1744
|
const jumped = item.kind === 'page'
|
|
1512
1745
|
? navigator.jumpToPage(item)
|
|
@@ -1587,6 +1820,159 @@ window.__ModuleLoader__.load({
|
|
|
1587
1820
|
kindBadge(via, t('option')),
|
|
1588
1821
|
])
|
|
1589
1822
|
|
|
1823
|
+
const resultsNode = none
|
|
1824
|
+
? (aiStatus === 'loading'
|
|
1825
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiThinking'))
|
|
1826
|
+
: aiStatus === 'error'
|
|
1827
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiUnavailable'))
|
|
1828
|
+
: (aiEnabled && !aiConfigured)
|
|
1829
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiNeedConfig'))
|
|
1830
|
+
: React.createElement('p', { className: 'sss-empty' }, t('empty')))
|
|
1831
|
+
: React.createElement('div', { className: 'sss-groups' }, [
|
|
1832
|
+
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1833
|
+
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
1834
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1835
|
+
shownPages.map((item, i) => renderPage(item, i))),
|
|
1836
|
+
]) : null,
|
|
1837
|
+
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1838
|
+
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
1839
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1840
|
+
shownOptions.map((item, i) => renderOption(item, shownPages.length + i))),
|
|
1841
|
+
]) : null,
|
|
1842
|
+
related.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1843
|
+
React.createElement('div', { className: 'sss-group-title' }, t('guess')),
|
|
1844
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1845
|
+
related.map((r, i) => r.kind === 'page'
|
|
1846
|
+
? renderPage(r.item, shownPages.length + shownOptions.length + i, r.via)
|
|
1847
|
+
: renderOption(r.item, shownPages.length + shownOptions.length + i, r.via))),
|
|
1848
|
+
]) : null,
|
|
1849
|
+
])
|
|
1850
|
+
|
|
1851
|
+
const searchInput = React.createElement('input', {
|
|
1852
|
+
className: pageMode ? 'sss-input sss-page-input' : 'sss-input',
|
|
1853
|
+
value: query,
|
|
1854
|
+
placeholder: t('placeholder'),
|
|
1855
|
+
spellCheck: false,
|
|
1856
|
+
ref: inputRef,
|
|
1857
|
+
onKeyDown: (e) => {
|
|
1858
|
+
if (e.key === 'ArrowDown') {
|
|
1859
|
+
e.preventDefault()
|
|
1860
|
+
if (results.length > 0) setActive(Math.min(activeIndex + 1, results.length - 1))
|
|
1861
|
+
} else if (e.key === 'ArrowUp') {
|
|
1862
|
+
e.preventDefault()
|
|
1863
|
+
setActive(Math.max(activeIndex - 1, 0))
|
|
1864
|
+
} else if (e.key === 'Enter') {
|
|
1865
|
+
const item = results[activeIndex]
|
|
1866
|
+
if (item) { e.preventDefault(); pick(item) }
|
|
1867
|
+
} else if (e.key === 'Escape') {
|
|
1868
|
+
if (pageMode) setPageOpen(false)
|
|
1869
|
+
else setOpen(false)
|
|
1870
|
+
}
|
|
1871
|
+
},
|
|
1872
|
+
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
|
|
1873
|
+
onFocus: () => setOpen(true),
|
|
1874
|
+
onBlur: () => setOpen(false),
|
|
1875
|
+
})
|
|
1876
|
+
const clearButton = query !== '' ? React.createElement('button', {
|
|
1877
|
+
className: 'sss-clear',
|
|
1878
|
+
type: 'button',
|
|
1879
|
+
onClick: () => { setQuery(''); setHint(null); setOpen(false) },
|
|
1880
|
+
}, '✕') : null
|
|
1881
|
+
const pageTriggerButton = React.createElement('button', {
|
|
1882
|
+
className: 'sss-trigger sss-top-trigger',
|
|
1883
|
+
type: 'button',
|
|
1884
|
+
onClick: () => setPageOpen(true),
|
|
1885
|
+
}, [SearchIcon, t('openSearch')])
|
|
1886
|
+
|
|
1887
|
+
if (pageMode && pageOpen) {
|
|
1888
|
+
return React.createElement(React.Fragment, null, [
|
|
1889
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1890
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1891
|
+
showWelcome
|
|
1892
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1893
|
+
: null,
|
|
1894
|
+
]),
|
|
1895
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [pageTriggerButton]),
|
|
1896
|
+
React.createElement('div', {
|
|
1897
|
+
className: 'sss-page-overlay',
|
|
1898
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) setPageOpen(false) },
|
|
1899
|
+
}, [
|
|
1900
|
+
React.createElement('div', {
|
|
1901
|
+
className: 'sss-page',
|
|
1902
|
+
role: 'dialog',
|
|
1903
|
+
'aria-modal': true,
|
|
1904
|
+
'aria-label': t('placeholder'),
|
|
1905
|
+
}, [
|
|
1906
|
+
React.createElement('div', { className: 'sss-page-head' }, [
|
|
1907
|
+
React.createElement('button', {
|
|
1908
|
+
className: 'sss-icon-btn',
|
|
1909
|
+
type: 'button',
|
|
1910
|
+
'aria-label': t('searchBtn'),
|
|
1911
|
+
title: t('searchBtn'),
|
|
1912
|
+
onClick: () => inputRef.current?.focus(),
|
|
1913
|
+
}, [SearchIcon]),
|
|
1914
|
+
searchInput,
|
|
1915
|
+
clearButton,
|
|
1916
|
+
React.createElement('button', {
|
|
1917
|
+
className: 'sss-page-close',
|
|
1918
|
+
type: 'button',
|
|
1919
|
+
'aria-label': 'Close',
|
|
1920
|
+
onClick: () => setPageOpen(false),
|
|
1921
|
+
}, '✕'),
|
|
1922
|
+
]),
|
|
1923
|
+
React.createElement('div', { className: 'sss-page-results' }, [
|
|
1924
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1925
|
+
q !== '' ? resultsNode : null,
|
|
1926
|
+
hint !== null
|
|
1927
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1928
|
+
: null,
|
|
1929
|
+
]),
|
|
1930
|
+
]),
|
|
1931
|
+
]),
|
|
1932
|
+
])
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
if (pageMode) {
|
|
1936
|
+
return React.createElement(React.Fragment, null, [
|
|
1937
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1938
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1939
|
+
showWelcome
|
|
1940
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1941
|
+
: null,
|
|
1942
|
+
]),
|
|
1943
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [pageTriggerButton]),
|
|
1944
|
+
])
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
if (topMode) {
|
|
1948
|
+
return React.createElement(React.Fragment, null, [
|
|
1949
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1950
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1951
|
+
showWelcome
|
|
1952
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1953
|
+
: null,
|
|
1954
|
+
]),
|
|
1955
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [
|
|
1956
|
+
React.createElement('div', { className: 'sss-box' }, [
|
|
1957
|
+
React.createElement('button', {
|
|
1958
|
+
className: 'sss-icon-btn',
|
|
1959
|
+
type: 'button',
|
|
1960
|
+
'aria-label': t('searchBtn'),
|
|
1961
|
+
title: t('searchBtn'),
|
|
1962
|
+
onClick: () => inputRef.current?.focus(),
|
|
1963
|
+
}, [SearchIcon]),
|
|
1964
|
+
searchInput,
|
|
1965
|
+
clearButton,
|
|
1966
|
+
]),
|
|
1967
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1968
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
|
|
1969
|
+
hint !== null
|
|
1970
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1971
|
+
: null,
|
|
1972
|
+
]),
|
|
1973
|
+
])
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1590
1976
|
return React.createElement('div', { className: 'sss-wrap' }, [
|
|
1591
1977
|
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1592
1978
|
React.createElement('div', { className: 'sss-box' }, [
|
|
@@ -1597,66 +1983,11 @@ window.__ModuleLoader__.load({
|
|
|
1597
1983
|
title: t('searchBtn'),
|
|
1598
1984
|
onClick: () => inputRef.current?.focus(),
|
|
1599
1985
|
}, [SearchIcon]),
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
value: query,
|
|
1603
|
-
placeholder: t('placeholder'),
|
|
1604
|
-
spellCheck: false,
|
|
1605
|
-
ref: inputRef,
|
|
1606
|
-
onKeyDown: (e) => {
|
|
1607
|
-
if (e.key === 'ArrowDown') {
|
|
1608
|
-
e.preventDefault()
|
|
1609
|
-
if (results.length > 0) setActive(Math.min(activeIndex + 1, results.length - 1))
|
|
1610
|
-
} else if (e.key === 'ArrowUp') {
|
|
1611
|
-
e.preventDefault()
|
|
1612
|
-
setActive(Math.max(activeIndex - 1, 0))
|
|
1613
|
-
} else if (e.key === 'Enter') {
|
|
1614
|
-
const item = results[activeIndex]
|
|
1615
|
-
if (item) { e.preventDefault(); pick(item) }
|
|
1616
|
-
} else if (e.key === 'Escape') {
|
|
1617
|
-
setOpen(false)
|
|
1618
|
-
}
|
|
1619
|
-
},
|
|
1620
|
-
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
|
|
1621
|
-
onFocus: () => setOpen(true),
|
|
1622
|
-
onBlur: () => setOpen(false),
|
|
1623
|
-
}),
|
|
1624
|
-
query !== '' ? React.createElement('button', {
|
|
1625
|
-
className: 'sss-clear',
|
|
1626
|
-
type: 'button',
|
|
1627
|
-
onClick: () => { setQuery(''); setHint(null); setOpen(false) },
|
|
1628
|
-
}, '✕') : null,
|
|
1986
|
+
searchInput,
|
|
1987
|
+
clearButton,
|
|
1629
1988
|
]),
|
|
1630
1989
|
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1631
|
-
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [
|
|
1632
|
-
none
|
|
1633
|
-
? (aiStatus === 'loading'
|
|
1634
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiThinking'))
|
|
1635
|
-
: aiStatus === 'error'
|
|
1636
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiUnavailable'))
|
|
1637
|
-
: (aiEnabled && !aiConfigured)
|
|
1638
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiNeedConfig'))
|
|
1639
|
-
: React.createElement('p', { className: 'sss-empty' }, t('empty')))
|
|
1640
|
-
: React.createElement('div', { className: 'sss-groups' }, [
|
|
1641
|
-
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1642
|
-
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
1643
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1644
|
-
shownPages.map((item, i) => renderPage(item, i))),
|
|
1645
|
-
]) : null,
|
|
1646
|
-
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1647
|
-
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
1648
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1649
|
-
shownOptions.map((item, i) => renderOption(item, shownPages.length + i))),
|
|
1650
|
-
]) : null,
|
|
1651
|
-
related.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1652
|
-
React.createElement('div', { className: 'sss-group-title' }, t('guess')),
|
|
1653
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1654
|
-
related.map((r, i) => r.kind === 'page'
|
|
1655
|
-
? renderPage(r.item, shownPages.length + shownOptions.length + i, r.via)
|
|
1656
|
-
: renderOption(r.item, shownPages.length + shownOptions.length + i, r.via))),
|
|
1657
|
-
]) : null,
|
|
1658
|
-
]),
|
|
1659
|
-
]) : null,
|
|
1990
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
|
|
1660
1991
|
hint !== null
|
|
1661
1992
|
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1662
1993
|
: null,
|
|
@@ -1675,8 +2006,19 @@ window.__ModuleLoader__.load({
|
|
|
1675
2006
|
return 0
|
|
1676
2007
|
}
|
|
1677
2008
|
|
|
2009
|
+
/** Auto-clear a transient confirmation flag (copied ✓, cleared…) after ms. */
|
|
2010
|
+
const useAutoReset = (active, reset, ms = 1600) => {
|
|
2011
|
+
React.useEffect(() => {
|
|
2012
|
+
if (!active) return
|
|
2013
|
+
const off = setTimeout(reset, ms)
|
|
2014
|
+
return () => clearTimeout(off)
|
|
2015
|
+
}, [active])
|
|
2016
|
+
}
|
|
2017
|
+
|
|
1678
2018
|
function UpdatePanel() {
|
|
1679
2019
|
const [open, setOpen] = React.useState(true)
|
|
2020
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
2021
|
+
const [shortcutOn, setShortcutOn] = React.useState(searchShortcutEnabled)
|
|
1680
2022
|
const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
|
|
1681
2023
|
const [latest, setLatest] = React.useState('')
|
|
1682
2024
|
const [copied, setCopied] = React.useState(false)
|
|
@@ -1692,6 +2034,8 @@ window.__ModuleLoader__.load({
|
|
|
1692
2034
|
const [aiSaved, setAiSaved] = React.useState(false)
|
|
1693
2035
|
const [testState, setTestState] = React.useState('idle') // idle | loading | ok | fail
|
|
1694
2036
|
const [testMsg, setTestMsg] = React.useState('')
|
|
2037
|
+
const [dshImportState, setDshImportState] = React.useState('idle') // idle | loading | ok | fail
|
|
2038
|
+
const [dshImportMsg, setDshImportMsg] = React.useState('')
|
|
1695
2039
|
const [, setLogVersion] = React.useState(0)
|
|
1696
2040
|
const [logCopied, setLogCopied] = React.useState(false)
|
|
1697
2041
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
@@ -1707,16 +2051,19 @@ window.__ModuleLoader__.load({
|
|
|
1707
2051
|
}, [])
|
|
1708
2052
|
|
|
1709
2053
|
React.useEffect(() => {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
return () =>
|
|
1713
|
-
}, [
|
|
2054
|
+
const sync = () => setSearchStyleState(readSearchStyle())
|
|
2055
|
+
window.addEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2056
|
+
return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2057
|
+
}, [])
|
|
1714
2058
|
|
|
1715
2059
|
React.useEffect(() => {
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
return () =>
|
|
1719
|
-
}, [
|
|
2060
|
+
const sync = () => setShortcutOn(searchShortcutEnabled())
|
|
2061
|
+
window.addEventListener(SEARCH_SHORTCUT_EVENT, sync)
|
|
2062
|
+
return () => window.removeEventListener(SEARCH_SHORTCUT_EVENT, sync)
|
|
2063
|
+
}, [])
|
|
2064
|
+
|
|
2065
|
+
useAutoReset(copied, () => { setCopied(false); setCopyFailed(false) })
|
|
2066
|
+
useAutoReset(aiSaved, () => setAiSaved(false))
|
|
1720
2067
|
|
|
1721
2068
|
React.useEffect(() => {
|
|
1722
2069
|
const sync = () => setLogVersion((v) => v + 1)
|
|
@@ -1724,23 +2071,9 @@ window.__ModuleLoader__.load({
|
|
|
1724
2071
|
return () => window.removeEventListener(LOG_EVENT, sync)
|
|
1725
2072
|
}, [])
|
|
1726
2073
|
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
return () => clearTimeout(off)
|
|
1731
|
-
}, [logCopied])
|
|
1732
|
-
|
|
1733
|
-
React.useEffect(() => {
|
|
1734
|
-
if (!logCleared) return
|
|
1735
|
-
const off = setTimeout(() => setLogCleared(false), 1600)
|
|
1736
|
-
return () => clearTimeout(off)
|
|
1737
|
-
}, [logCleared])
|
|
1738
|
-
|
|
1739
|
-
React.useEffect(() => {
|
|
1740
|
-
if (!logExportFail) return
|
|
1741
|
-
const off = setTimeout(() => setLogExportFail(false), 1600)
|
|
1742
|
-
return () => clearTimeout(off)
|
|
1743
|
-
}, [logExportFail])
|
|
2074
|
+
useAutoReset(logCopied, () => setLogCopied(false))
|
|
2075
|
+
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2076
|
+
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
1744
2077
|
|
|
1745
2078
|
const saveAi = () => {
|
|
1746
2079
|
setAiConfig({
|
|
@@ -1753,6 +2086,39 @@ window.__ModuleLoader__.load({
|
|
|
1753
2086
|
log('info', '保存 AI 配置', { provider, model: model.trim() })
|
|
1754
2087
|
}
|
|
1755
2088
|
|
|
2089
|
+
const importDshModelConfig = async () => {
|
|
2090
|
+
if (dshImportState === 'loading') return
|
|
2091
|
+
setDshImportState('loading')
|
|
2092
|
+
setDshImportMsg('')
|
|
2093
|
+
try {
|
|
2094
|
+
const imported = await readDshModelConfig()
|
|
2095
|
+
if (!imported) throw new Error(t('aiDshImportEmpty'))
|
|
2096
|
+
setAiConfig({ ...imported, apiKey })
|
|
2097
|
+
setProvider(imported.provider)
|
|
2098
|
+
setBaseUrl(imported.baseUrl)
|
|
2099
|
+
setModel(imported.model)
|
|
2100
|
+
setDshImportState('ok')
|
|
2101
|
+
setDshImportMsg(t('aiDshImportOk'))
|
|
2102
|
+
log('info', '导入 DSH 模型配置', { provider: imported.provider, model: imported.model })
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
const reason = String(error?.message ?? error).slice(0, 160)
|
|
2105
|
+
setDshImportState('fail')
|
|
2106
|
+
setDshImportMsg(t('aiDshImportFail').replace('{reason}', reason))
|
|
2107
|
+
log('error', '导入 DSH 模型配置失败', { detail: reason })
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
const updateSearchStyle = (style) => {
|
|
2112
|
+
setStoredSearchStyle(style)
|
|
2113
|
+
setSearchStyleState(style)
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
const updateShortcutEnabled = (enabled) => {
|
|
2117
|
+
setSearchShortcutEnabled(enabled)
|
|
2118
|
+
setShortcutOn(enabled)
|
|
2119
|
+
log('info', enabled ? '启用搜索快捷键' : '禁用搜索快捷键', {})
|
|
2120
|
+
}
|
|
2121
|
+
|
|
1756
2122
|
const requestClearAi = () => setConfirmClearAi(true)
|
|
1757
2123
|
const cancelClearAi = () => setConfirmClearAi(false)
|
|
1758
2124
|
const clearAiConfig = () => {
|
|
@@ -1914,6 +2280,29 @@ window.__ModuleLoader__.load({
|
|
|
1914
2280
|
React.createElement('span', { className: 'sss-vr-chevron' + (open ? ' sss-vr-chevron-open' : '') }, '▾'),
|
|
1915
2281
|
]),
|
|
1916
2282
|
open ? React.createElement('div', { className: 'sss-vr-body' }, [
|
|
2283
|
+
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2284
|
+
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2285
|
+
React.createElement('span', { className: 'sss-vr-label' }, t('styleTitle')),
|
|
2286
|
+
]),
|
|
2287
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('styleHint')),
|
|
2288
|
+
React.createElement('div', { className: 'sss-vr-segment' }, [
|
|
2289
|
+
React.createElement('button', {
|
|
2290
|
+
className: 'sss-vr-seg' + (searchStyle === 'sidebar' ? ' is-active' : ''),
|
|
2291
|
+
type: 'button',
|
|
2292
|
+
onClick: () => updateSearchStyle('sidebar'),
|
|
2293
|
+
}, t('styleClassic')),
|
|
2294
|
+
React.createElement('button', {
|
|
2295
|
+
className: 'sss-vr-seg' + (searchStyle === 'top' ? ' is-active' : ''),
|
|
2296
|
+
type: 'button',
|
|
2297
|
+
onClick: () => updateSearchStyle('top'),
|
|
2298
|
+
}, t('styleTop')),
|
|
2299
|
+
React.createElement('button', {
|
|
2300
|
+
className: 'sss-vr-seg' + (searchStyle === 'page' ? ' is-active' : ''),
|
|
2301
|
+
type: 'button',
|
|
2302
|
+
onClick: () => updateSearchStyle('page'),
|
|
2303
|
+
}, t('stylePage')),
|
|
2304
|
+
]),
|
|
2305
|
+
]),
|
|
1917
2306
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
1918
2307
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1919
2308
|
React.createElement('span', { className: 'sss-vr-label' }, t('aiSearch')),
|
|
@@ -1931,6 +2320,18 @@ window.__ModuleLoader__.load({
|
|
|
1931
2320
|
React.createElement('p', { className: 'sss-vr-hint' }, t('aiDesc')),
|
|
1932
2321
|
React.createElement('p', { className: 'sss-vr-hint' }, t('aiPrivacy')),
|
|
1933
2322
|
]),
|
|
2323
|
+
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2324
|
+
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2325
|
+
React.createElement('span', { className: 'sss-vr-label' }, t('shortcutTitle')),
|
|
2326
|
+
React.createElement('input', {
|
|
2327
|
+
className: 'sss-vr-check',
|
|
2328
|
+
type: 'checkbox',
|
|
2329
|
+
checked: shortcutOn,
|
|
2330
|
+
onChange: (event) => updateShortcutEnabled(event.target.checked),
|
|
2331
|
+
}),
|
|
2332
|
+
]),
|
|
2333
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('shortcutHint')),
|
|
2334
|
+
]),
|
|
1934
2335
|
React.createElement('div', { className: 'sss-vr-field sss-vr-form' }, [
|
|
1935
2336
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1936
2337
|
React.createElement('label', { className: 'sss-vr-label' }, t('aiProvider')),
|
|
@@ -2001,8 +2402,13 @@ window.__ModuleLoader__.load({
|
|
|
2001
2402
|
aiSaved ? t('aiConfigSaved') : t('aiConfigSave')),
|
|
2002
2403
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: testState === 'loading', onClick: () => { void testService() } },
|
|
2003
2404
|
testState === 'loading' ? t('aiTesting') : t('aiTest')),
|
|
2405
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: dshImportState === 'loading', onClick: () => { void importDshModelConfig() } },
|
|
2406
|
+
dshImportState === 'loading' ? t('aiDshImporting') : t('aiDshImport')),
|
|
2004
2407
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => requestClearAi() }, t('aiClear')),
|
|
2005
2408
|
]),
|
|
2409
|
+
dshImportMsg
|
|
2410
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, dshImportMsg)
|
|
2411
|
+
: null,
|
|
2006
2412
|
testMsg
|
|
2007
2413
|
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, testMsg)
|
|
2008
2414
|
: null,
|
|
@@ -2038,7 +2444,6 @@ window.__ModuleLoader__.load({
|
|
|
2038
2444
|
state === 'loading' ? t('updChecking') : t('updCheck')),
|
|
2039
2445
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: notesState === 'loading', onClick: () => { void fetchNotes() } },
|
|
2040
2446
|
notesState === 'loading' ? t('updFetchingNotes') : t('updReleaseNotes')),
|
|
2041
|
-
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => openWelcome() }, t('wlReopen')),
|
|
2042
2447
|
]),
|
|
2043
2448
|
notesState === 'loaded'
|
|
2044
2449
|
? React.createElement('div', { className: 'sss-vr-notes' }, notesBody)
|
|
@@ -2105,7 +2510,7 @@ window.__ModuleLoader__.load({
|
|
|
2105
2510
|
}
|
|
2106
2511
|
|
|
2107
2512
|
exports.apply = apply
|
|
2108
|
-
exports.inject = ['slots', 'locale']
|
|
2513
|
+
exports.inject = ['slots', 'locale', 'connection']
|
|
2109
2514
|
return module.exports
|
|
2110
2515
|
},
|
|
2111
2516
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectivex666/dsh-settings-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"client": {
|
|
45
45
|
"platform": "web",
|
|
46
46
|
"inject": [
|
|
47
|
+
"@deepseek-ai/dsh-client-connection",
|
|
47
48
|
"@deepseek-ai/dsh-client-runtime",
|
|
48
49
|
"@deepseek-ai/dsh-client-locale",
|
|
49
50
|
"@deepseek-ai/dsh-client-ui-settings"
|
package/scripts/release.mjs
CHANGED
|
@@ -29,6 +29,7 @@ function arg(name) {
|
|
|
29
29
|
const version = process.argv.includes('--version') ? arg('--version') : undefined
|
|
30
30
|
const publish = process.argv.includes('--publish')
|
|
31
31
|
const dryRun = process.argv.includes('--dry-run')
|
|
32
|
+
const VERSION_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/
|
|
32
33
|
|
|
33
34
|
const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
|
|
34
35
|
const target = version ?? pkg.version
|
|
@@ -36,6 +37,10 @@ if (pkg.version !== target) {
|
|
|
36
37
|
console.error(`version mismatch: package.json=${pkg.version}, requested=${target}`)
|
|
37
38
|
process.exit(1)
|
|
38
39
|
}
|
|
40
|
+
if (!VERSION_RE.test(target)) {
|
|
41
|
+
console.error(`invalid semantic version: ${target}`)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
39
44
|
const tag = `v${target}`
|
|
40
45
|
const repo = pkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '') ?? 'https://github.com/objectivex666/dsh-settings-search'
|
|
41
46
|
|