@objectivex666/dsh-settings-search 1.8.0 → 1.9.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 +4 -2
- package/README.md +4 -2
- package/lib/client.js +519 -154
- package/package.json +2 -1
- package/scripts/release.mjs +5 -0
package/README.en.md
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
## Features
|
|
15
15
|
|
|
16
|
-
- 🔍 **Live search** — a search box
|
|
17
|
-
-
|
|
16
|
+
- 🔍 **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.
|
|
17
|
+
- 🧩 **Four search styles (v1.9.0)** — choose the classic sidebar, window top bar, popup search page, or a mixed window top bar plus popup page layout. The choice is saved in the current browser.
|
|
18
|
+
- 🤝 **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.
|
|
19
|
+
- 🎯 **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
20
|
- 📂 **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
21
|
- 🔤 **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
22
|
- 🧠 **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
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
|
|
14
14
|
## 功能特性
|
|
15
15
|
|
|
16
|
-
- 🔍
|
|
17
|
-
-
|
|
16
|
+
- 🔍 **实时搜索**:在设置面板中嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
|
|
17
|
+
- 🧩 **四种搜索样式(v1.9.0)**:可选择经典侧栏、窗口顶栏、弹出搜索页,或“窗口顶栏 + 弹出搜索页”混合模式;选择会保存在当前浏览器。
|
|
18
|
+
- 🤝 **导入 DSH 模型配置(v1.9.0)**:在 AI 配置区一键读取 DSH 当前默认模型的 Base URL 与模型名;为避免凭据扩散,导入始终不包含 API Key。
|
|
19
|
+
- 🎯 **新用户引导(v1.9.0)**:仅在安装后首次打开设置面板时弹出引导弹窗,介绍四种搜索样式、拼音/意图/AI 联想、点击即达与自动导入索引等核心功能;不再提供“不再显示”和手动重开按钮。
|
|
18
20
|
- 📂 **页内选项搜索(v1.2.0)**:不只搜页面,还能直达每个设置页内部的具体选项——插件页的标签页(`settings.plugins.tab`)、Web UI 插件的卡片(`web-ui.plugin.item`)、通用设置行等,结果以「页面 › 选项」面包屑展示。
|
|
19
21
|
- 🔤 **拼音联想搜索(v1.5.0)**:输入拼音首字母(如 `sz`)或全拼(如 `shezhi`)即可联想中文设置项;支持 `↑`/`↓` 选择、`Enter` 跳转、`Esc` 关闭,命中的文字自动高亮。
|
|
20
22
|
- 🧠 **意图联想搜索(v1.6.0)**:直接描述想做的事(如「太亮了」「字太小」「忘记密码」),无需知道选项名即可联想真实设置项,结果在「猜你想找」分组展示。
|
package/lib/client.js
CHANGED
|
@@ -30,12 +30,19 @@ 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.9.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
|
+
styleMixed: '顶栏 + 搜索页',
|
|
45
|
+
openSearch: '打开搜索',
|
|
39
46
|
warming: '正在导入各分区选项…',
|
|
40
47
|
empty: '没有匹配的设置',
|
|
41
48
|
pages: '设置页',
|
|
@@ -43,6 +50,7 @@ window.__ModuleLoader__.load({
|
|
|
43
50
|
guess: '猜你想找',
|
|
44
51
|
page: '页面',
|
|
45
52
|
option: '选项',
|
|
53
|
+
pageSearchTitle: '搜索结果',
|
|
46
54
|
jumpFail: '未能自动跳转,请手动打开:{path}',
|
|
47
55
|
updn: '设置搜索',
|
|
48
56
|
updDesc: '设置面板顶部搜索框由本插件提供。在此检查插件的版本与更新。',
|
|
@@ -77,6 +85,11 @@ window.__ModuleLoader__.load({
|
|
|
77
85
|
aiBaseUrlHint: '仅支持 http(s) 地址;非本机地址必须使用 https,以免泄露 API Key。',
|
|
78
86
|
aiModel: '模型名',
|
|
79
87
|
aiApiKey: 'API Key',
|
|
88
|
+
aiDshImport: '导入 DSH 模型配置',
|
|
89
|
+
aiDshImporting: '正在导入…',
|
|
90
|
+
aiDshImportOk: '已导入 Base URL 与模型名;API Key 未改动 ✓',
|
|
91
|
+
aiDshImportFail: '导入失败:{reason}',
|
|
92
|
+
aiDshImportEmpty: '未找到 DSH 当前默认模型配置',
|
|
80
93
|
aiConfigSave: '保存配置',
|
|
81
94
|
aiConfigSaved: '已保存 ✓',
|
|
82
95
|
aiClear: '清除配置',
|
|
@@ -115,7 +128,7 @@ window.__ModuleLoader__.load({
|
|
|
115
128
|
wlTitle: '欢迎使用设置搜索',
|
|
116
129
|
wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
|
|
117
130
|
wlFeature1Title: '实时搜索',
|
|
118
|
-
wlFeature1Desc: '
|
|
131
|
+
wlFeature1Desc: '支持经典侧栏、窗口顶栏、弹出搜索页与顶栏加搜索页混合模式;输入关键词即可过滤设置。',
|
|
119
132
|
wlFeature2Title: '拼音与意图联想',
|
|
120
133
|
wlFeature2Desc: '输入拼音首字母(如 sz)或直接描述需求(如「太亮」),也能联想设置项。',
|
|
121
134
|
wlFeature3Title: 'AI 联想(可选)',
|
|
@@ -125,9 +138,7 @@ window.__ModuleLoader__.load({
|
|
|
125
138
|
wlFeature5Title: '自带设置页',
|
|
126
139
|
wlFeature5Desc: '左侧导航的「设置搜索」页可检查更新、查看说明与导出日志。',
|
|
127
140
|
wlStart: '开始使用',
|
|
128
|
-
wlSkip: '不再显示',
|
|
129
141
|
wlOpenPanel: '打开设置搜索页',
|
|
130
|
-
wlReopen: '新用户引导',
|
|
131
142
|
wlFeature6Title: '自动导入索引',
|
|
132
143
|
wlFeature6Desc: '首次打开时自动遍历一遍各设置分区,把分区内选项一并纳入搜索,无需逐页手动打开即可直接搜到。',
|
|
133
144
|
}
|
|
@@ -135,6 +146,13 @@ window.__ModuleLoader__.load({
|
|
|
135
146
|
title: 'Settings',
|
|
136
147
|
placeholder: 'Search settings…',
|
|
137
148
|
searchBtn: 'Search',
|
|
149
|
+
styleTitle: 'Search style',
|
|
150
|
+
styleHint: 'Choose how the search entry is displayed; saved in this browser.',
|
|
151
|
+
styleClassic: 'Classic sidebar',
|
|
152
|
+
styleTop: 'Top bar',
|
|
153
|
+
stylePage: 'Popup page',
|
|
154
|
+
styleMixed: 'Top bar + popup',
|
|
155
|
+
openSearch: 'Open search',
|
|
138
156
|
warming: 'Importing section options…',
|
|
139
157
|
empty: 'No matching settings',
|
|
140
158
|
pages: 'Settings pages',
|
|
@@ -142,6 +160,7 @@ window.__ModuleLoader__.load({
|
|
|
142
160
|
guess: 'You may want',
|
|
143
161
|
page: 'Page',
|
|
144
162
|
option: 'Option',
|
|
163
|
+
pageSearchTitle: 'Search results',
|
|
145
164
|
jumpFail: 'Could not navigate automatically — open it here: {path}',
|
|
146
165
|
updn: 'Settings Search',
|
|
147
166
|
updDesc: "The search box atop the settings panel comes from this plugin. Check its version and updates here.",
|
|
@@ -176,6 +195,11 @@ window.__ModuleLoader__.load({
|
|
|
176
195
|
aiBaseUrlHint: 'Only http(s) URLs are allowed; remote hosts must use https to avoid exposing your API key.',
|
|
177
196
|
aiModel: 'Model',
|
|
178
197
|
aiApiKey: 'API Key',
|
|
198
|
+
aiDshImport: 'Import DSH model config',
|
|
199
|
+
aiDshImporting: 'Importing…',
|
|
200
|
+
aiDshImportOk: 'Imported Base URL and model; API key unchanged ✓',
|
|
201
|
+
aiDshImportFail: 'Import failed: {reason}',
|
|
202
|
+
aiDshImportEmpty: 'No current DSH default model config found',
|
|
179
203
|
aiConfigSave: 'Save config',
|
|
180
204
|
aiConfigSaved: 'Saved ✓',
|
|
181
205
|
aiClear: 'Clear config',
|
|
@@ -214,7 +238,7 @@ window.__ModuleLoader__.load({
|
|
|
214
238
|
wlTitle: 'Welcome to Settings Search',
|
|
215
239
|
wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
|
|
216
240
|
wlFeature1Title: 'Instant search',
|
|
217
|
-
wlFeature1Desc: '
|
|
241
|
+
wlFeature1Desc: 'Choose classic sidebar, top bar, popup page, or mixed top bar plus popup search; typing filters settings.',
|
|
218
242
|
wlFeature2Title: 'Pinyin & intent hints',
|
|
219
243
|
wlFeature2Desc: 'Type pinyin initials (e.g. sz) or describe what you want to do to find settings.',
|
|
220
244
|
wlFeature3Title: 'AI hints (optional)',
|
|
@@ -224,9 +248,7 @@ window.__ModuleLoader__.load({
|
|
|
224
248
|
wlFeature5Title: 'Built-in panel',
|
|
225
249
|
wlFeature5Desc: 'The Settings Search page in the sidebar checks updates, shows notes, and exports logs.',
|
|
226
250
|
wlStart: 'Get started',
|
|
227
|
-
wlSkip: "Don't show again",
|
|
228
251
|
wlOpenPanel: 'Open Settings Search panel',
|
|
229
|
-
wlReopen: 'Onboarding',
|
|
230
252
|
wlFeature6Title: 'Auto-index',
|
|
231
253
|
wlFeature6Desc: 'On first open it automatically passes through every settings section, so in-page options are searchable without opening each page manually.',
|
|
232
254
|
}
|
|
@@ -278,28 +300,42 @@ window.__ModuleLoader__.load({
|
|
|
278
300
|
PINYIN_TABLE.split(/\s+/).filter(Boolean).map((pair) => [pair[0], pair.slice(1)])
|
|
279
301
|
)
|
|
280
302
|
const CJK_RE = /[\u4e00-\u9fff]/
|
|
303
|
+
const ALNUM_RE = /[a-zA-Z0-9]/
|
|
281
304
|
|
|
282
305
|
/**
|
|
283
306
|
* Transliterate a label into full-pinyin and initial-letter keys for
|
|
284
307
|
* association search. Returns null when any CJK character is outside
|
|
285
308
|
* the built-in table (then only substring matching applies).
|
|
309
|
+
*
|
|
310
|
+
* Results are memoized: the same labels and intent keywords are
|
|
311
|
+
* re-transliterated on every keystroke's re-filter, and this CJK loop
|
|
312
|
+
* is the hottest code in search. Cached objects are shared — callers
|
|
313
|
+
* must treat them as read-only.
|
|
286
314
|
*/
|
|
315
|
+
const PINYIN_KEY_CACHE = new Map()
|
|
287
316
|
const pinyinKeys = (text) => {
|
|
317
|
+
const cached = PINYIN_KEY_CACHE.get(text)
|
|
318
|
+
if (cached !== undefined) return cached
|
|
288
319
|
let full = ''
|
|
289
320
|
let initials = ''
|
|
290
321
|
for (const ch of text) {
|
|
291
322
|
if (CJK_RE.test(ch)) {
|
|
292
323
|
const py = PINYIN.get(ch)
|
|
293
|
-
if (py === undefined)
|
|
324
|
+
if (py === undefined) {
|
|
325
|
+
PINYIN_KEY_CACHE.set(text, null)
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
294
328
|
full += py
|
|
295
329
|
initials += py[0]
|
|
296
|
-
} else if (
|
|
330
|
+
} else if (ALNUM_RE.test(ch)) {
|
|
297
331
|
const lower = ch.toLowerCase()
|
|
298
332
|
full += lower
|
|
299
333
|
initials += lower
|
|
300
334
|
}
|
|
301
335
|
}
|
|
302
|
-
|
|
336
|
+
const keys = full === '' ? null : { full, initials }
|
|
337
|
+
PINYIN_KEY_CACHE.set(text, keys)
|
|
338
|
+
return keys
|
|
303
339
|
}
|
|
304
340
|
|
|
305
341
|
/** Match one query token: raw substring, full pinyin, or initial letters. */
|
|
@@ -312,6 +348,17 @@ window.__ModuleLoader__.load({
|
|
|
312
348
|
return null
|
|
313
349
|
}
|
|
314
350
|
|
|
351
|
+
/** Cache the \b-wrapped keyword regexes used by related-keyword matching. */
|
|
352
|
+
const WORD_BOUNDARY_CACHE = new Map()
|
|
353
|
+
const wordBoundaryRegex = (kw) => {
|
|
354
|
+
let re = WORD_BOUNDARY_CACHE.get(kw)
|
|
355
|
+
if (re === undefined) {
|
|
356
|
+
re = new RegExp(`\\b${kw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`)
|
|
357
|
+
WORD_BOUNDARY_CACHE.set(kw, re)
|
|
358
|
+
}
|
|
359
|
+
return re
|
|
360
|
+
}
|
|
361
|
+
|
|
315
362
|
/** Character count of the label consumed by a full-pinyin prefix. */
|
|
316
363
|
const pinyinPrefixChars = (label, pinyinLength) => {
|
|
317
364
|
let consumed = 0
|
|
@@ -360,8 +407,14 @@ window.__ModuleLoader__.load({
|
|
|
360
407
|
return { phrase, keywords: entry.slice(at + 1).split(','), keys: pinyinKeys(phrase) }
|
|
361
408
|
})
|
|
362
409
|
|
|
363
|
-
/**
|
|
410
|
+
/**
|
|
411
|
+
* Keywords a query token should additionally search after an intent hit.
|
|
412
|
+
* Memoized per token — tokens repeat on every keystroke's re-filter.
|
|
413
|
+
*/
|
|
414
|
+
const INTENT_KEYWORD_CACHE = new Map()
|
|
364
415
|
const intentKeywords = (tok) => {
|
|
416
|
+
const cached = INTENT_KEYWORD_CACHE.get(tok)
|
|
417
|
+
if (cached !== undefined) return cached
|
|
365
418
|
const hits = []
|
|
366
419
|
for (const intent of INTENTS) {
|
|
367
420
|
const phraseHit = intent.phrase.includes(tok) || tok.includes(intent.phrase) ||
|
|
@@ -373,6 +426,7 @@ window.__ModuleLoader__.load({
|
|
|
373
426
|
}
|
|
374
427
|
}
|
|
375
428
|
}
|
|
429
|
+
INTENT_KEYWORD_CACHE.set(tok, hits)
|
|
376
430
|
return hits
|
|
377
431
|
}
|
|
378
432
|
|
|
@@ -407,6 +461,24 @@ window.__ModuleLoader__.load({
|
|
|
407
461
|
const AI_BASE_URL_KEY = 'dsh-settings-search:ai-base-url'
|
|
408
462
|
const AI_MODEL_KEY = 'dsh-settings-search:ai-model'
|
|
409
463
|
const AI_API_KEY_KEY = 'dsh-settings-search:ai-api-key'
|
|
464
|
+
/** Session-only API key storage avoids durable browser storage exposure. */
|
|
465
|
+
const readApiKey = () => {
|
|
466
|
+
const legacy = typeof localStorage !== 'undefined' ? (localStorage.getItem(AI_API_KEY_KEY) ?? '').trim() : ''
|
|
467
|
+
if (legacy !== '') {
|
|
468
|
+
if (typeof sessionStorage !== 'undefined') sessionStorage.setItem(AI_API_KEY_KEY, legacy)
|
|
469
|
+
if (typeof localStorage !== 'undefined') localStorage.removeItem(AI_API_KEY_KEY)
|
|
470
|
+
return legacy
|
|
471
|
+
}
|
|
472
|
+
return typeof sessionStorage !== 'undefined' ? (sessionStorage.getItem(AI_API_KEY_KEY) ?? '').trim() : ''
|
|
473
|
+
}
|
|
474
|
+
const writeApiKey = (value) => {
|
|
475
|
+
const apiKey = String(value ?? '').trim()
|
|
476
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
477
|
+
if (apiKey === '') sessionStorage.removeItem(AI_API_KEY_KEY)
|
|
478
|
+
else sessionStorage.setItem(AI_API_KEY_KEY, apiKey)
|
|
479
|
+
}
|
|
480
|
+
if (typeof localStorage !== 'undefined') localStorage.removeItem(AI_API_KEY_KEY)
|
|
481
|
+
}
|
|
410
482
|
const aiSearchEnabled = () => {
|
|
411
483
|
if (typeof localStorage === 'undefined') return false
|
|
412
484
|
return localStorage.getItem(AI_STORAGE_KEY) === '1'
|
|
@@ -417,26 +489,42 @@ window.__ModuleLoader__.load({
|
|
|
417
489
|
window.dispatchEvent(new CustomEvent(AI_TOGGLE_EVENT, { detail: enabled }))
|
|
418
490
|
}
|
|
419
491
|
const aiConfig = () => {
|
|
420
|
-
if (typeof localStorage === 'undefined'
|
|
421
|
-
|
|
492
|
+
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') {
|
|
493
|
+
return { provider: 'openai', baseUrl: '', model: '', apiKey: '' }
|
|
494
|
+
}
|
|
495
|
+
const providerRaw = (localStorage?.getItem(AI_PROVIDER_KEY) ?? 'openai').trim()
|
|
422
496
|
return {
|
|
423
497
|
provider: providerRaw === 'anthropic' ? 'anthropic' : 'openai',
|
|
424
|
-
baseUrl: (localStorage
|
|
425
|
-
model: (localStorage
|
|
426
|
-
apiKey: (
|
|
498
|
+
baseUrl: (localStorage?.getItem(AI_BASE_URL_KEY) ?? '').trim(),
|
|
499
|
+
model: (localStorage?.getItem(AI_MODEL_KEY) ?? '').trim(),
|
|
500
|
+
apiKey: readApiKey(),
|
|
427
501
|
}
|
|
428
502
|
}
|
|
429
503
|
const setAiConfig = (cfg) => {
|
|
504
|
+
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') return
|
|
505
|
+
writeApiKey(cfg.apiKey)
|
|
430
506
|
if (typeof localStorage === 'undefined') return
|
|
431
507
|
localStorage.setItem(AI_PROVIDER_KEY, cfg.provider === 'anthropic' ? 'anthropic' : 'openai')
|
|
432
508
|
localStorage.setItem(AI_BASE_URL_KEY, cfg.baseUrl ?? '')
|
|
433
509
|
localStorage.setItem(AI_MODEL_KEY, cfg.model ?? '')
|
|
434
|
-
localStorage.setItem(AI_API_KEY_KEY, cfg.apiKey ?? '')
|
|
435
510
|
window.dispatchEvent(new CustomEvent(AI_CONFIG_EVENT))
|
|
436
511
|
}
|
|
437
512
|
/** First-run onboarding: remembered via localStorage so it shows once. */
|
|
438
513
|
const ONBOARD_KEY = 'dsh-settings-search:onboarded'
|
|
439
514
|
const WELCOME_EVENT = 'dsh-settings-search:welcome'
|
|
515
|
+
const SEARCH_STYLE_KEY = 'dsh-settings-search:style'
|
|
516
|
+
const SEARCH_STYLE_EVENT = 'dsh-settings-search:style-change'
|
|
517
|
+
const SEARCH_STYLES = ['sidebar', 'top', 'page', 'mixed']
|
|
518
|
+
const readSearchStyle = () => {
|
|
519
|
+
if (typeof localStorage === 'undefined') return 'sidebar'
|
|
520
|
+
const style = localStorage.getItem(SEARCH_STYLE_KEY)
|
|
521
|
+
return SEARCH_STYLES.includes(style) ? style : 'sidebar'
|
|
522
|
+
}
|
|
523
|
+
const setStoredSearchStyle = (style) => {
|
|
524
|
+
if (typeof localStorage === 'undefined' || !SEARCH_STYLES.includes(style)) return
|
|
525
|
+
localStorage.setItem(SEARCH_STYLE_KEY, style)
|
|
526
|
+
window.dispatchEvent(new CustomEvent(SEARCH_STYLE_EVENT, { detail: style }))
|
|
527
|
+
}
|
|
440
528
|
const welcomed = () => {
|
|
441
529
|
if (typeof localStorage === 'undefined') return true
|
|
442
530
|
return localStorage.getItem(ONBOARD_KEY) === '1'
|
|
@@ -446,9 +534,6 @@ window.__ModuleLoader__.load({
|
|
|
446
534
|
localStorage.setItem(ONBOARD_KEY, '1')
|
|
447
535
|
window.dispatchEvent(new CustomEvent(WELCOME_EVENT, { detail: false }))
|
|
448
536
|
}
|
|
449
|
-
const openWelcome = () => {
|
|
450
|
-
window.dispatchEvent(new CustomEvent(WELCOME_EVENT, { detail: true }))
|
|
451
|
-
}
|
|
452
537
|
const extractAiKeywords = (raw) => {
|
|
453
538
|
const text = String(raw ?? '').replace(/^```(?:json)?/i, '').replace(/```$/, '').trim()
|
|
454
539
|
const start = text.indexOf('{')
|
|
@@ -489,17 +574,17 @@ window.__ModuleLoader__.load({
|
|
|
489
574
|
return url.origin + path.replace(/\/+$/, '')
|
|
490
575
|
}
|
|
491
576
|
const chatCompletionsUrl = (baseUrl) => {
|
|
577
|
+
// safeBaseUrl already strips trailing slashes
|
|
492
578
|
const safe = safeBaseUrl(baseUrl)
|
|
493
579
|
if (!safe) return ''
|
|
494
|
-
|
|
495
|
-
return /\/chat\/completions$/i.test(trimmed) ? trimmed : `${trimmed}/chat/completions`
|
|
580
|
+
return /\/chat\/completions$/i.test(safe) ? safe : `${safe}/chat/completions`
|
|
496
581
|
}
|
|
497
582
|
const anthropicMessagesUrl = (baseUrl) => {
|
|
583
|
+
// safeBaseUrl already strips trailing slashes
|
|
498
584
|
const safe = safeBaseUrl(baseUrl)
|
|
499
585
|
if (!safe) return ''
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
return /\/v1$/i.test(trimmed) ? `${trimmed}/messages` : `${trimmed}/v1/messages`
|
|
586
|
+
if (/\/v1\/messages$/i.test(safe)) return safe
|
|
587
|
+
return /\/v1$/i.test(safe) ? `${safe}/messages` : `${safe}/v1/messages`
|
|
503
588
|
}
|
|
504
589
|
const requestUserAi = async (query, labels, signal) => {
|
|
505
590
|
const { provider, baseUrl, model, apiKey } = aiConfig()
|
|
@@ -573,7 +658,11 @@ window.__ModuleLoader__.load({
|
|
|
573
658
|
const cacheKey = query.toLowerCase()
|
|
574
659
|
if (AI_CACHE.has(cacheKey)) return AI_CACHE.get(cacheKey)
|
|
575
660
|
const keywords = await requestUserAi(query, labels, signal)
|
|
576
|
-
if (keywords.length > 0)
|
|
661
|
+
if (keywords.length > 0) {
|
|
662
|
+
// bound the cache so a long session cannot grow it without limit
|
|
663
|
+
if (AI_CACHE.size >= 100) AI_CACHE.clear()
|
|
664
|
+
AI_CACHE.set(cacheKey, keywords)
|
|
665
|
+
}
|
|
577
666
|
return keywords
|
|
578
667
|
}
|
|
579
668
|
|
|
@@ -583,16 +672,17 @@ window.__ModuleLoader__.load({
|
|
|
583
672
|
* Returns { ok, code, detail } where code is a stable error kind used to
|
|
584
673
|
* look up a localized hint in the UI.
|
|
585
674
|
*/
|
|
675
|
+
const HTTP_TEST_CODES = {
|
|
676
|
+
400: 'bad-request',
|
|
677
|
+
401: 'auth',
|
|
678
|
+
403: 'forbidden',
|
|
679
|
+
404: 'not-found',
|
|
680
|
+
408: 'timeout',
|
|
681
|
+
429: 'rate-limit',
|
|
682
|
+
}
|
|
586
683
|
const testHttpCode = (status) => {
|
|
587
684
|
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'
|
|
685
|
+
return HTTP_TEST_CODES[code] ?? (code >= 500 && code <= 599 ? 'server' : 'http')
|
|
596
686
|
}
|
|
597
687
|
const testAiService = async (cfg) => {
|
|
598
688
|
const provider = cfg.provider === 'anthropic' ? 'anthropic' : 'openai'
|
|
@@ -687,6 +777,25 @@ window.__ModuleLoader__.load({
|
|
|
687
777
|
.sss-ai-note { margin: 0; padding: 6px 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
688
778
|
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
689
779
|
.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; }
|
|
780
|
+
.sss-wrap.sss-top { position: absolute; top: 20px; left: 198px; right: 152px; z-index: 950; width: auto; height: 28px; gap: 8px; }
|
|
781
|
+
.sss-top .sss-title { display: none; }
|
|
782
|
+
.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); }
|
|
783
|
+
.sss-top .sss-input { font-size: 15px; }
|
|
784
|
+
.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; }
|
|
785
|
+
.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; }
|
|
786
|
+
.sss-trigger:hover { border-color: var(--dsw-alias-label-dimmed); color: var(--dsw-alias-label-secondary); }
|
|
787
|
+
.sss-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
|
|
788
|
+
.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%)); }
|
|
789
|
+
.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); }
|
|
790
|
+
.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); }
|
|
791
|
+
.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); }
|
|
792
|
+
.sss-page-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
793
|
+
.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; }
|
|
794
|
+
.sss-page-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
795
|
+
.sss-page-results { flex: 1; min-height: 0; overflow: auto; padding: 8px; }
|
|
796
|
+
.sss-page-results .sss-group { padding: 2px; }
|
|
797
|
+
.sss-page-results .sss-item { padding: 10px 8px; border-radius: 8px; }
|
|
798
|
+
.sss-mixed-title { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--dsw-alias-label-primary); font-size: 16px; line-height: 24px; }
|
|
690
799
|
@keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
|
|
691
800
|
.sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
|
|
692
801
|
.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 +826,10 @@ window.__ModuleLoader__.load({
|
|
|
717
826
|
.sss-vr-btn:hover:not(:disabled) { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
718
827
|
.sss-vr-btn:disabled { opacity: .4; cursor: default; }
|
|
719
828
|
.sss-vr-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }
|
|
829
|
+
.sss-vr-segment { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
|
830
|
+
.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; }
|
|
831
|
+
.sss-vr-seg:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
832
|
+
.sss-vr-seg.is-active { color: var(--dsw-alias-bg-layer-3); background: var(--dsw-alias-label-primary); border-color: transparent; }
|
|
720
833
|
.sss-vr-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
721
834
|
.sss-vr-primary:hover:not(:disabled) { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
722
835
|
.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 +910,6 @@ window.__ModuleLoader__.load({
|
|
|
797
910
|
function createModel(ctx) {
|
|
798
911
|
/** id → harvested visible title, per slot. */
|
|
799
912
|
const harvest = new Map()
|
|
800
|
-
let harvestSeq = 0
|
|
801
913
|
|
|
802
914
|
const entriesOf = (slot) => {
|
|
803
915
|
try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
|
|
@@ -834,7 +946,7 @@ window.__ModuleLoader__.load({
|
|
|
834
946
|
const trimDescription = (text) => {
|
|
835
947
|
const t = (text ?? '').replace(/\s+/g, ' ').trim()
|
|
836
948
|
if (t === '') return ''
|
|
837
|
-
const re = /[\s,,。;;](关闭后|开启后|启用后|禁用后|创建后|使用时|点击后|勾选后|开启时|关闭时|完成后|启用时|禁用时|开始后|停止后)/
|
|
949
|
+
const re = /(?:[\s,,。;;])?(关闭后|开启后|启用后|禁用后|创建后|使用时|点击后|勾选后|开启时|关闭时|完成后|启用时|禁用时|开始后|停止后)/
|
|
838
950
|
const m = re.exec(t)
|
|
839
951
|
if (m) {
|
|
840
952
|
const head = t.slice(0, m.index).trim()
|
|
@@ -914,7 +1026,7 @@ window.__ModuleLoader__.load({
|
|
|
914
1026
|
if (containers.length === 1 && containers[0].childElementCount > 0) {
|
|
915
1027
|
nodes = [...containers[0].children]
|
|
916
1028
|
} else {
|
|
917
|
-
nodes = containers
|
|
1029
|
+
nodes = containers
|
|
918
1030
|
}
|
|
919
1031
|
if (nodes.length === 0) continue
|
|
920
1032
|
map = harvest.get(cfg.slot)
|
|
@@ -972,7 +1084,6 @@ window.__ModuleLoader__.load({
|
|
|
972
1084
|
}
|
|
973
1085
|
}
|
|
974
1086
|
} catch { /* section mining is best-effort */ }
|
|
975
|
-
if (grew) harvestSeq += 1
|
|
976
1087
|
return grew
|
|
977
1088
|
}
|
|
978
1089
|
|
|
@@ -1008,6 +1119,10 @@ window.__ModuleLoader__.load({
|
|
|
1008
1119
|
const parentLabel = (parentId) =>
|
|
1009
1120
|
sections.find((s) => s.id === parentId)?.label ?? parentId
|
|
1010
1121
|
const out = []
|
|
1122
|
+
// Set-based dedup: keys and "parentLabel\u0000label" pairs replace the
|
|
1123
|
+
// former out.some(...) scans, which were O(n²) on every keystroke.
|
|
1124
|
+
const seenKeys = new Set()
|
|
1125
|
+
const seenPairs = new Set()
|
|
1011
1126
|
for (const cfg of DEEP_SLOTS) {
|
|
1012
1127
|
const mapped = harvest.get(cfg.slot)
|
|
1013
1128
|
sortEntries(entriesOf(cfg.slot)).forEach((entry, i) => {
|
|
@@ -1020,8 +1135,10 @@ window.__ModuleLoader__.load({
|
|
|
1020
1135
|
const label = staticLabel(entry, cfg) || hitObj?.title || ''
|
|
1021
1136
|
if (label === '') return // unmapped, unharvested — invisible until it renders once
|
|
1022
1137
|
const key = cfg.slot + '#' + id
|
|
1023
|
-
if (
|
|
1138
|
+
if (seenKeys.has(key)) return
|
|
1024
1139
|
const pageLabel = parentLabel(cfg.parent)
|
|
1140
|
+
seenKeys.add(key)
|
|
1141
|
+
seenPairs.add(pageLabel + '\u0000' + label)
|
|
1025
1142
|
out.push({
|
|
1026
1143
|
kind: 'option',
|
|
1027
1144
|
key,
|
|
@@ -1038,7 +1155,9 @@ window.__ModuleLoader__.load({
|
|
|
1038
1155
|
hitObj.inner.forEach((op, oi) => {
|
|
1039
1156
|
if (op.label === '' || op.label === label) return
|
|
1040
1157
|
const innerKey = key + '#o' + oi
|
|
1041
|
-
if (
|
|
1158
|
+
if (seenKeys.has(innerKey)) return
|
|
1159
|
+
seenKeys.add(innerKey)
|
|
1160
|
+
seenPairs.add(label + '\u0000' + op.label)
|
|
1042
1161
|
out.push({
|
|
1043
1162
|
kind: 'option',
|
|
1044
1163
|
key: innerKey,
|
|
@@ -1072,9 +1191,11 @@ window.__ModuleLoader__.load({
|
|
|
1072
1191
|
rec.inner.forEach((op, oi) => {
|
|
1073
1192
|
if (op.label === '' || op.label === label) return
|
|
1074
1193
|
const innerKey = 'settings.section#' + id + '#o' + oi
|
|
1075
|
-
if (
|
|
1076
|
-
const
|
|
1077
|
-
if (
|
|
1194
|
+
if (seenKeys.has(innerKey)) return
|
|
1195
|
+
const pair = label + '\u0000' + op.label
|
|
1196
|
+
if (seenPairs.has(pair)) return
|
|
1197
|
+
seenKeys.add(innerKey)
|
|
1198
|
+
seenPairs.add(pair)
|
|
1078
1199
|
out.push({
|
|
1079
1200
|
kind: 'option',
|
|
1080
1201
|
key: innerKey,
|
|
@@ -1099,15 +1220,19 @@ window.__ModuleLoader__.load({
|
|
|
1099
1220
|
: a.order - b.order)
|
|
1100
1221
|
}
|
|
1101
1222
|
|
|
1102
|
-
return { readSections, readDeep, runHarvest
|
|
1223
|
+
return { readSections, readDeep, runHarvest }
|
|
1103
1224
|
}
|
|
1104
1225
|
|
|
1105
1226
|
/* ---------- DOM-level navigation into the settings shell ---------- */
|
|
1106
1227
|
|
|
1228
|
+
/** Root element that hosts the settings panel's nav buttons. */
|
|
1229
|
+
const panelRoot = () => {
|
|
1230
|
+
if (typeof document === 'undefined') return null
|
|
1231
|
+
return document.querySelector('[role="dialog"]') ?? document.body
|
|
1232
|
+
}
|
|
1107
1233
|
const panelButtons = () => {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
return [...root.querySelectorAll('button')]
|
|
1234
|
+
const root = panelRoot()
|
|
1235
|
+
return root === null ? [] : [...root.querySelectorAll('button')]
|
|
1111
1236
|
}
|
|
1112
1237
|
const clickNavButton = (wantText) => {
|
|
1113
1238
|
const want = String(wantText ?? '').trim()
|
|
@@ -1122,8 +1247,8 @@ window.__ModuleLoader__.load({
|
|
|
1122
1247
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1123
1248
|
/** Label of the currently-active first-level settings nav item. */
|
|
1124
1249
|
const activeSectionLabel = () => {
|
|
1125
|
-
|
|
1126
|
-
|
|
1250
|
+
const root = panelRoot()
|
|
1251
|
+
if (root === null) return ''
|
|
1127
1252
|
const isTab = (b) => b.getAttribute('role') === 'tab'
|
|
1128
1253
|
const buttons = [...root.querySelectorAll('button')].filter((b) => !isTab(b))
|
|
1129
1254
|
const byCurrent = buttons.find((b) => b.getAttribute('aria-current') === 'true')
|
|
@@ -1153,11 +1278,9 @@ window.__ModuleLoader__.load({
|
|
|
1153
1278
|
model.runHarvest()
|
|
1154
1279
|
visited += 1
|
|
1155
1280
|
}
|
|
1156
|
-
if (original
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
model.runHarvest()
|
|
1160
|
-
}
|
|
1281
|
+
if (clickNavButton(original)) {
|
|
1282
|
+
await delay(160)
|
|
1283
|
+
model.runHarvest()
|
|
1161
1284
|
}
|
|
1162
1285
|
} finally {
|
|
1163
1286
|
if (typeof onDone === 'function') onDone()
|
|
@@ -1168,13 +1291,10 @@ window.__ModuleLoader__.load({
|
|
|
1168
1291
|
|
|
1169
1292
|
function createNavigator(ctx) {
|
|
1170
1293
|
/** 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
|
-
}
|
|
1294
|
+
const jumpToPage = async (item) => clickNavButton(item.label)
|
|
1175
1295
|
/** Jump to an in-page option: open its page, select its tab, flash its row. */
|
|
1176
1296
|
const jumpToOption = async (item) => {
|
|
1177
|
-
const opened =
|
|
1297
|
+
const opened = clickNavButton(item.pageLabel ?? item.parentLabel)
|
|
1178
1298
|
if (opened !== true) { flashOptionRow(item); return false }
|
|
1179
1299
|
await delay(160)
|
|
1180
1300
|
const label = item.label
|
|
@@ -1242,6 +1362,36 @@ window.__ModuleLoader__.load({
|
|
|
1242
1362
|
const model = createModel(ctx)
|
|
1243
1363
|
const navigator = createNavigator(ctx)
|
|
1244
1364
|
|
|
1365
|
+
/** Read the model fields from DSH; credentials are never requested. */
|
|
1366
|
+
const readDshModelConfig = async () => {
|
|
1367
|
+
const connection = ctx.get('connection')
|
|
1368
|
+
const response = await connection.api.settings.describe({})
|
|
1369
|
+
if (!response.result.ok) throw new Error(response.result.error?.message || 'settings.describe failed')
|
|
1370
|
+
const namespaces = new Map(response.result.value.namespaces.map((view) => [view.ns, view]))
|
|
1371
|
+
const defaultModel = namespaces.get('agent-default-model')?.value ?? {}
|
|
1372
|
+
const providerId = String(defaultModel.provider ?? '').trim()
|
|
1373
|
+
const modelId = String(defaultModel.model ?? '').trim()
|
|
1374
|
+
const readPath = (value, path) => (path ?? []).reduce((current, key) => current?.[key], value)
|
|
1375
|
+
const providersResponse = await connection.api.llm.providers({}).catch(() => null)
|
|
1376
|
+
const providerRow = providersResponse?.result?.ok
|
|
1377
|
+
? providersResponse.result.value.providers.find((entry) => entry.provider === providerId)
|
|
1378
|
+
: undefined
|
|
1379
|
+
const modelNamespace = namespaces.get(providerRow?.settingsNs || 'llm-pi-ai')
|
|
1380
|
+
const providerProfile = providerRow
|
|
1381
|
+
? readPath(modelNamespace?.value, providerRow.settingsPath) ?? {}
|
|
1382
|
+
: modelNamespace?.value?.providers?.[providerId] ?? {}
|
|
1383
|
+
const configuredModels = Array.isArray(providerProfile.models) ? providerProfile.models : []
|
|
1384
|
+
const modelProfile = configuredModels.find((entry) => entry?.id === modelId) ?? {}
|
|
1385
|
+
const protocol = String(modelProfile.api ?? providerProfile.api ?? '').trim()
|
|
1386
|
+
const baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1387
|
+
if (!modelId || !baseUrl) return null
|
|
1388
|
+
return {
|
|
1389
|
+
provider: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1390
|
+
baseUrl,
|
|
1391
|
+
model: modelId,
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1245
1395
|
const SearchIcon = React.createElement('svg', {
|
|
1246
1396
|
className: 'sss-icon',
|
|
1247
1397
|
viewBox: '0 0 16.5 16.5',
|
|
@@ -1295,7 +1445,6 @@ window.__ModuleLoader__.load({
|
|
|
1295
1445
|
React.createElement('div', { className: 'sss-wl-footer' }, [
|
|
1296
1446
|
React.createElement('button', { className: 'sss-wl-btn sss-wl-primary', type: 'button', onClick: onClose }, t('wlStart')),
|
|
1297
1447
|
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
1448
|
]),
|
|
1300
1449
|
]),
|
|
1301
1450
|
])
|
|
@@ -1326,6 +1475,8 @@ window.__ModuleLoader__.load({
|
|
|
1326
1475
|
function SettingsHeaderSearch() {
|
|
1327
1476
|
const [query, setQuery] = React.useState('')
|
|
1328
1477
|
const [open, setOpen] = React.useState(false)
|
|
1478
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
1479
|
+
const [pageOpen, setPageOpen] = React.useState(false)
|
|
1329
1480
|
const [showWelcome, setShowWelcome] = React.useState(() => !welcomed())
|
|
1330
1481
|
const [tick, setTick] = React.useState(0)
|
|
1331
1482
|
const [warming, setWarming] = React.useState(false)
|
|
@@ -1343,17 +1494,29 @@ window.__ModuleLoader__.load({
|
|
|
1343
1494
|
ctx.locale.subscribe(forceTick),
|
|
1344
1495
|
]
|
|
1345
1496
|
// Grow the index whenever the panel body re-renders (visits new pages).
|
|
1497
|
+
// DOM mutations arrive in bursts (animations, list mounts), so the
|
|
1498
|
+
// harvest pass runs at most once per throttle window (trailing edge).
|
|
1346
1499
|
let warmAlive = true
|
|
1347
1500
|
let warmAttempts = 0
|
|
1501
|
+
let mutatorAlive = true
|
|
1502
|
+
let throttleTimer = null
|
|
1503
|
+
const onMutate = () => {
|
|
1504
|
+
if (!mutatorAlive) return
|
|
1505
|
+
if (model.runHarvest()) setTick((x) => x + 1)
|
|
1506
|
+
if (!warmed && activeSectionLabel() !== '') {
|
|
1507
|
+
void warmAllSections(model, () => setWarming(true), () => setWarming(false)).then((done) => {
|
|
1508
|
+
if (!warmAlive) return
|
|
1509
|
+
if (done) setTick((x) => x + 1)
|
|
1510
|
+
})
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1348
1513
|
const observer = typeof MutationObserver === 'function'
|
|
1349
1514
|
? new MutationObserver(() => {
|
|
1350
|
-
if (
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
})
|
|
1356
|
-
}
|
|
1515
|
+
if (throttleTimer !== null) return
|
|
1516
|
+
throttleTimer = setTimeout(() => {
|
|
1517
|
+
throttleTimer = null
|
|
1518
|
+
onMutate()
|
|
1519
|
+
}, 120)
|
|
1357
1520
|
})
|
|
1358
1521
|
: null
|
|
1359
1522
|
observer?.observe(document.body, { childList: true, subtree: true })
|
|
@@ -1372,9 +1535,11 @@ window.__ModuleLoader__.load({
|
|
|
1372
1535
|
}, 800)
|
|
1373
1536
|
return () => {
|
|
1374
1537
|
warmAlive = false
|
|
1538
|
+
mutatorAlive = false
|
|
1375
1539
|
for (const off of offs) off()
|
|
1376
1540
|
observer?.disconnect()
|
|
1377
1541
|
clearTimeout(warm)
|
|
1542
|
+
if (throttleTimer !== null) clearTimeout(throttleTimer)
|
|
1378
1543
|
}
|
|
1379
1544
|
}, [])
|
|
1380
1545
|
|
|
@@ -1384,6 +1549,12 @@ window.__ModuleLoader__.load({
|
|
|
1384
1549
|
return () => window.removeEventListener(WELCOME_EVENT, onWelcome)
|
|
1385
1550
|
}, [])
|
|
1386
1551
|
|
|
1552
|
+
React.useEffect(() => {
|
|
1553
|
+
const sync = () => setSearchStyleState(readSearchStyle())
|
|
1554
|
+
window.addEventListener(SEARCH_STYLE_EVENT, sync)
|
|
1555
|
+
return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
|
|
1556
|
+
}, [])
|
|
1557
|
+
|
|
1387
1558
|
React.useEffect(() => {
|
|
1388
1559
|
const sync = () => setAiEnabled(aiSearchEnabled())
|
|
1389
1560
|
const onConfig = () => forceTick()
|
|
@@ -1406,7 +1577,7 @@ window.__ModuleLoader__.load({
|
|
|
1406
1577
|
tokens.every((tok) => tokenMatch(haystackLower, item.keys, tok) !== null)
|
|
1407
1578
|
const kwMatch = (item, haystackLower, kw) => {
|
|
1408
1579
|
if (/^[\x00-\x7f]+$/.test(kw)) {
|
|
1409
|
-
if (
|
|
1580
|
+
if (wordBoundaryRegex(kw).test(haystackLower)) return true
|
|
1410
1581
|
} else if (haystackLower.includes(kw)) return true
|
|
1411
1582
|
if (item.keys === null) return false
|
|
1412
1583
|
const kwKeys = pinyinKeys(kw)
|
|
@@ -1498,15 +1669,24 @@ window.__ModuleLoader__.load({
|
|
|
1498
1669
|
const [active, setActive] = React.useState(0)
|
|
1499
1670
|
const popRef = React.useRef(null)
|
|
1500
1671
|
const inputRef = React.useRef(null)
|
|
1672
|
+
const pageMode = searchStyle === 'page'
|
|
1673
|
+
const mixedMode = searchStyle === 'mixed'
|
|
1674
|
+
const topMode = searchStyle === 'top' || mixedMode
|
|
1501
1675
|
const activeIndex = Math.min(active, Math.max(results.length - 1, 0))
|
|
1502
1676
|
|
|
1503
1677
|
React.useEffect(() => { setActive(0) }, [query])
|
|
1678
|
+
React.useEffect(() => {
|
|
1679
|
+
if (!pageMode || !pageOpen) return
|
|
1680
|
+
const timer = setTimeout(() => inputRef.current?.focus(), 0)
|
|
1681
|
+
return () => clearTimeout(timer)
|
|
1682
|
+
}, [pageMode, pageOpen])
|
|
1504
1683
|
React.useEffect(() => {
|
|
1505
1684
|
const el = popRef.current?.querySelector('.sss-item.is-active')
|
|
1506
1685
|
el?.scrollIntoView?.({ block: 'nearest' })
|
|
1507
1686
|
}, [activeIndex, q])
|
|
1508
1687
|
const pick = (item) => {
|
|
1509
1688
|
setOpen(false)
|
|
1689
|
+
setPageOpen(false)
|
|
1510
1690
|
log('info', '打开设置', { kind: item.kind, id: item.id || item.key || '', label: String(item.label).slice(0, 60) })
|
|
1511
1691
|
const jumped = item.kind === 'page'
|
|
1512
1692
|
? navigator.jumpToPage(item)
|
|
@@ -1587,6 +1767,192 @@ window.__ModuleLoader__.load({
|
|
|
1587
1767
|
kindBadge(via, t('option')),
|
|
1588
1768
|
])
|
|
1589
1769
|
|
|
1770
|
+
const resultsNode = none
|
|
1771
|
+
? (aiStatus === 'loading'
|
|
1772
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiThinking'))
|
|
1773
|
+
: aiStatus === 'error'
|
|
1774
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiUnavailable'))
|
|
1775
|
+
: (aiEnabled && !aiConfigured)
|
|
1776
|
+
? React.createElement('p', { className: 'sss-ai-note' }, t('aiNeedConfig'))
|
|
1777
|
+
: React.createElement('p', { className: 'sss-empty' }, t('empty')))
|
|
1778
|
+
: React.createElement('div', { className: 'sss-groups' }, [
|
|
1779
|
+
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1780
|
+
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
1781
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1782
|
+
shownPages.map((item, i) => renderPage(item, i))),
|
|
1783
|
+
]) : null,
|
|
1784
|
+
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1785
|
+
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
1786
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1787
|
+
shownOptions.map((item, i) => renderOption(item, shownPages.length + i))),
|
|
1788
|
+
]) : null,
|
|
1789
|
+
related.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1790
|
+
React.createElement('div', { className: 'sss-group-title' }, t('guess')),
|
|
1791
|
+
React.createElement('ul', { className: 'sss-list' },
|
|
1792
|
+
related.map((r, i) => r.kind === 'page'
|
|
1793
|
+
? renderPage(r.item, shownPages.length + shownOptions.length + i, r.via)
|
|
1794
|
+
: renderOption(r.item, shownPages.length + shownOptions.length + i, r.via))),
|
|
1795
|
+
]) : null,
|
|
1796
|
+
])
|
|
1797
|
+
|
|
1798
|
+
const searchInput = React.createElement('input', {
|
|
1799
|
+
className: pageMode ? 'sss-input sss-page-input' : 'sss-input',
|
|
1800
|
+
value: query,
|
|
1801
|
+
placeholder: t('placeholder'),
|
|
1802
|
+
spellCheck: false,
|
|
1803
|
+
ref: inputRef,
|
|
1804
|
+
onKeyDown: (e) => {
|
|
1805
|
+
if (e.key === 'ArrowDown') {
|
|
1806
|
+
e.preventDefault()
|
|
1807
|
+
if (results.length > 0) setActive(Math.min(activeIndex + 1, results.length - 1))
|
|
1808
|
+
} else if (e.key === 'ArrowUp') {
|
|
1809
|
+
e.preventDefault()
|
|
1810
|
+
setActive(Math.max(activeIndex - 1, 0))
|
|
1811
|
+
} else if (e.key === 'Enter') {
|
|
1812
|
+
const item = results[activeIndex]
|
|
1813
|
+
if (item) { e.preventDefault(); pick(item) }
|
|
1814
|
+
} else if (e.key === 'Escape') {
|
|
1815
|
+
if (pageMode || mixedMode) setPageOpen(false)
|
|
1816
|
+
else setOpen(false)
|
|
1817
|
+
}
|
|
1818
|
+
},
|
|
1819
|
+
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true); if (mixedMode) setPageOpen(true) },
|
|
1820
|
+
onFocus: () => { setOpen(true); if (mixedMode) setPageOpen(true) },
|
|
1821
|
+
onBlur: () => setOpen(false),
|
|
1822
|
+
})
|
|
1823
|
+
const clearButton = query !== '' ? React.createElement('button', {
|
|
1824
|
+
className: 'sss-clear',
|
|
1825
|
+
type: 'button',
|
|
1826
|
+
onClick: () => { setQuery(''); setHint(null); setOpen(false) },
|
|
1827
|
+
}, '✕') : null
|
|
1828
|
+
const pageTriggerButton = React.createElement('button', {
|
|
1829
|
+
className: 'sss-trigger sss-top-trigger',
|
|
1830
|
+
type: 'button',
|
|
1831
|
+
onClick: () => setPageOpen(true),
|
|
1832
|
+
}, [SearchIcon, t('openSearch')])
|
|
1833
|
+
|
|
1834
|
+
if (pageMode && pageOpen) {
|
|
1835
|
+
return React.createElement(React.Fragment, null, [
|
|
1836
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1837
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1838
|
+
showWelcome
|
|
1839
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1840
|
+
: null,
|
|
1841
|
+
]),
|
|
1842
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [pageTriggerButton]),
|
|
1843
|
+
React.createElement('div', {
|
|
1844
|
+
className: 'sss-page-overlay',
|
|
1845
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) setPageOpen(false) },
|
|
1846
|
+
}, [
|
|
1847
|
+
React.createElement('div', {
|
|
1848
|
+
className: 'sss-page',
|
|
1849
|
+
role: 'dialog',
|
|
1850
|
+
'aria-modal': true,
|
|
1851
|
+
'aria-label': t('placeholder'),
|
|
1852
|
+
}, [
|
|
1853
|
+
React.createElement('div', { className: 'sss-page-head' }, [
|
|
1854
|
+
React.createElement('button', {
|
|
1855
|
+
className: 'sss-icon-btn',
|
|
1856
|
+
type: 'button',
|
|
1857
|
+
'aria-label': t('searchBtn'),
|
|
1858
|
+
title: t('searchBtn'),
|
|
1859
|
+
onClick: () => inputRef.current?.focus(),
|
|
1860
|
+
}, [SearchIcon]),
|
|
1861
|
+
searchInput,
|
|
1862
|
+
clearButton,
|
|
1863
|
+
React.createElement('button', {
|
|
1864
|
+
className: 'sss-page-close',
|
|
1865
|
+
type: 'button',
|
|
1866
|
+
'aria-label': 'Close',
|
|
1867
|
+
onClick: () => setPageOpen(false),
|
|
1868
|
+
}, '✕'),
|
|
1869
|
+
]),
|
|
1870
|
+
React.createElement('div', { className: 'sss-page-results' }, [
|
|
1871
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1872
|
+
q !== '' ? resultsNode : null,
|
|
1873
|
+
hint !== null
|
|
1874
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1875
|
+
: null,
|
|
1876
|
+
]),
|
|
1877
|
+
]),
|
|
1878
|
+
]),
|
|
1879
|
+
])
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
if (pageMode) {
|
|
1883
|
+
return React.createElement(React.Fragment, null, [
|
|
1884
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1885
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1886
|
+
showWelcome
|
|
1887
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1888
|
+
: null,
|
|
1889
|
+
]),
|
|
1890
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [pageTriggerButton]),
|
|
1891
|
+
])
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
if (topMode) {
|
|
1895
|
+
return React.createElement(React.Fragment, null, [
|
|
1896
|
+
React.createElement('div', { className: 'sss-wrap' }, [
|
|
1897
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1898
|
+
showWelcome
|
|
1899
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1900
|
+
: null,
|
|
1901
|
+
]),
|
|
1902
|
+
React.createElement('div', { className: 'sss-wrap sss-top' }, [
|
|
1903
|
+
React.createElement('div', { className: 'sss-box' }, [
|
|
1904
|
+
React.createElement('button', {
|
|
1905
|
+
className: 'sss-icon-btn',
|
|
1906
|
+
type: 'button',
|
|
1907
|
+
'aria-label': t('searchBtn'),
|
|
1908
|
+
title: t('searchBtn'),
|
|
1909
|
+
onClick: () => inputRef.current?.focus(),
|
|
1910
|
+
}, [SearchIcon]),
|
|
1911
|
+
searchInput,
|
|
1912
|
+
clearButton,
|
|
1913
|
+
]),
|
|
1914
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1915
|
+
!mixedMode && open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
|
|
1916
|
+
!mixedMode && hint !== null
|
|
1917
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1918
|
+
: null,
|
|
1919
|
+
]),
|
|
1920
|
+
mixedMode && pageOpen
|
|
1921
|
+
? React.createElement('div', {
|
|
1922
|
+
className: 'sss-page-overlay',
|
|
1923
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) setPageOpen(false) },
|
|
1924
|
+
}, [
|
|
1925
|
+
React.createElement('div', { className: 'sss-page' }, [
|
|
1926
|
+
React.createElement('div', { className: 'sss-page-head' }, [
|
|
1927
|
+
React.createElement('button', {
|
|
1928
|
+
className: 'sss-icon-btn',
|
|
1929
|
+
type: 'button',
|
|
1930
|
+
'aria-label': t('searchBtn'),
|
|
1931
|
+
title: t('searchBtn'),
|
|
1932
|
+
onClick: () => inputRef.current?.focus(),
|
|
1933
|
+
}, [SearchIcon]),
|
|
1934
|
+
React.createElement('span', { className: 'sss-page-input sss-mixed-title' }, t('pageSearchTitle')),
|
|
1935
|
+
clearButton,
|
|
1936
|
+
React.createElement('button', {
|
|
1937
|
+
className: 'sss-page-close',
|
|
1938
|
+
type: 'button',
|
|
1939
|
+
'aria-label': 'Close',
|
|
1940
|
+
onClick: () => setPageOpen(false),
|
|
1941
|
+
}, '✕'),
|
|
1942
|
+
]),
|
|
1943
|
+
React.createElement('div', { className: 'sss-page-results' }, [
|
|
1944
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1945
|
+
q !== '' ? resultsNode : null,
|
|
1946
|
+
hint !== null
|
|
1947
|
+
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1948
|
+
: null,
|
|
1949
|
+
]),
|
|
1950
|
+
]),
|
|
1951
|
+
])
|
|
1952
|
+
: null,
|
|
1953
|
+
])
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1590
1956
|
return React.createElement('div', { className: 'sss-wrap' }, [
|
|
1591
1957
|
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1592
1958
|
React.createElement('div', { className: 'sss-box' }, [
|
|
@@ -1597,66 +1963,11 @@ window.__ModuleLoader__.load({
|
|
|
1597
1963
|
title: t('searchBtn'),
|
|
1598
1964
|
onClick: () => inputRef.current?.focus(),
|
|
1599
1965
|
}, [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,
|
|
1966
|
+
searchInput,
|
|
1967
|
+
clearButton,
|
|
1629
1968
|
]),
|
|
1630
1969
|
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,
|
|
1970
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
|
|
1660
1971
|
hint !== null
|
|
1661
1972
|
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1662
1973
|
: null,
|
|
@@ -1675,8 +1986,18 @@ window.__ModuleLoader__.load({
|
|
|
1675
1986
|
return 0
|
|
1676
1987
|
}
|
|
1677
1988
|
|
|
1989
|
+
/** Auto-clear a transient confirmation flag (copied ✓, cleared…) after ms. */
|
|
1990
|
+
const useAutoReset = (active, reset, ms = 1600) => {
|
|
1991
|
+
React.useEffect(() => {
|
|
1992
|
+
if (!active) return
|
|
1993
|
+
const off = setTimeout(reset, ms)
|
|
1994
|
+
return () => clearTimeout(off)
|
|
1995
|
+
}, [active])
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1678
1998
|
function UpdatePanel() {
|
|
1679
1999
|
const [open, setOpen] = React.useState(true)
|
|
2000
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
1680
2001
|
const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
|
|
1681
2002
|
const [latest, setLatest] = React.useState('')
|
|
1682
2003
|
const [copied, setCopied] = React.useState(false)
|
|
@@ -1692,6 +2013,8 @@ window.__ModuleLoader__.load({
|
|
|
1692
2013
|
const [aiSaved, setAiSaved] = React.useState(false)
|
|
1693
2014
|
const [testState, setTestState] = React.useState('idle') // idle | loading | ok | fail
|
|
1694
2015
|
const [testMsg, setTestMsg] = React.useState('')
|
|
2016
|
+
const [dshImportState, setDshImportState] = React.useState('idle') // idle | loading | ok | fail
|
|
2017
|
+
const [dshImportMsg, setDshImportMsg] = React.useState('')
|
|
1695
2018
|
const [, setLogVersion] = React.useState(0)
|
|
1696
2019
|
const [logCopied, setLogCopied] = React.useState(false)
|
|
1697
2020
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
@@ -1707,16 +2030,13 @@ window.__ModuleLoader__.load({
|
|
|
1707
2030
|
}, [])
|
|
1708
2031
|
|
|
1709
2032
|
React.useEffect(() => {
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
return () =>
|
|
1713
|
-
}, [
|
|
2033
|
+
const sync = () => setSearchStyleState(readSearchStyle())
|
|
2034
|
+
window.addEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2035
|
+
return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2036
|
+
}, [])
|
|
1714
2037
|
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
const off = setTimeout(() => setAiSaved(false), 1600)
|
|
1718
|
-
return () => clearTimeout(off)
|
|
1719
|
-
}, [aiSaved])
|
|
2038
|
+
useAutoReset(copied, () => { setCopied(false); setCopyFailed(false) })
|
|
2039
|
+
useAutoReset(aiSaved, () => setAiSaved(false))
|
|
1720
2040
|
|
|
1721
2041
|
React.useEffect(() => {
|
|
1722
2042
|
const sync = () => setLogVersion((v) => v + 1)
|
|
@@ -1724,23 +2044,9 @@ window.__ModuleLoader__.load({
|
|
|
1724
2044
|
return () => window.removeEventListener(LOG_EVENT, sync)
|
|
1725
2045
|
}, [])
|
|
1726
2046
|
|
|
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])
|
|
2047
|
+
useAutoReset(logCopied, () => setLogCopied(false))
|
|
2048
|
+
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2049
|
+
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
1744
2050
|
|
|
1745
2051
|
const saveAi = () => {
|
|
1746
2052
|
setAiConfig({
|
|
@@ -1753,6 +2059,33 @@ window.__ModuleLoader__.load({
|
|
|
1753
2059
|
log('info', '保存 AI 配置', { provider, model: model.trim() })
|
|
1754
2060
|
}
|
|
1755
2061
|
|
|
2062
|
+
const importDshModelConfig = async () => {
|
|
2063
|
+
if (dshImportState === 'loading') return
|
|
2064
|
+
setDshImportState('loading')
|
|
2065
|
+
setDshImportMsg('')
|
|
2066
|
+
try {
|
|
2067
|
+
const imported = await readDshModelConfig()
|
|
2068
|
+
if (!imported) throw new Error(t('aiDshImportEmpty'))
|
|
2069
|
+
setAiConfig({ ...imported, apiKey })
|
|
2070
|
+
setProvider(imported.provider)
|
|
2071
|
+
setBaseUrl(imported.baseUrl)
|
|
2072
|
+
setModel(imported.model)
|
|
2073
|
+
setDshImportState('ok')
|
|
2074
|
+
setDshImportMsg(t('aiDshImportOk'))
|
|
2075
|
+
log('info', '导入 DSH 模型配置', { provider: imported.provider, model: imported.model })
|
|
2076
|
+
} catch (error) {
|
|
2077
|
+
const reason = String(error?.message ?? error).slice(0, 160)
|
|
2078
|
+
setDshImportState('fail')
|
|
2079
|
+
setDshImportMsg(t('aiDshImportFail').replace('{reason}', reason))
|
|
2080
|
+
log('error', '导入 DSH 模型配置失败', { detail: reason })
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const updateSearchStyle = (style) => {
|
|
2085
|
+
setStoredSearchStyle(style)
|
|
2086
|
+
setSearchStyleState(style)
|
|
2087
|
+
}
|
|
2088
|
+
|
|
1756
2089
|
const requestClearAi = () => setConfirmClearAi(true)
|
|
1757
2090
|
const cancelClearAi = () => setConfirmClearAi(false)
|
|
1758
2091
|
const clearAiConfig = () => {
|
|
@@ -1914,6 +2247,34 @@ window.__ModuleLoader__.load({
|
|
|
1914
2247
|
React.createElement('span', { className: 'sss-vr-chevron' + (open ? ' sss-vr-chevron-open' : '') }, '▾'),
|
|
1915
2248
|
]),
|
|
1916
2249
|
open ? React.createElement('div', { className: 'sss-vr-body' }, [
|
|
2250
|
+
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2251
|
+
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2252
|
+
React.createElement('span', { className: 'sss-vr-label' }, t('styleTitle')),
|
|
2253
|
+
]),
|
|
2254
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('styleHint')),
|
|
2255
|
+
React.createElement('div', { className: 'sss-vr-segment' }, [
|
|
2256
|
+
React.createElement('button', {
|
|
2257
|
+
className: 'sss-vr-seg' + (searchStyle === 'sidebar' ? ' is-active' : ''),
|
|
2258
|
+
type: 'button',
|
|
2259
|
+
onClick: () => updateSearchStyle('sidebar'),
|
|
2260
|
+
}, t('styleClassic')),
|
|
2261
|
+
React.createElement('button', {
|
|
2262
|
+
className: 'sss-vr-seg' + (searchStyle === 'top' ? ' is-active' : ''),
|
|
2263
|
+
type: 'button',
|
|
2264
|
+
onClick: () => updateSearchStyle('top'),
|
|
2265
|
+
}, t('styleTop')),
|
|
2266
|
+
React.createElement('button', {
|
|
2267
|
+
className: 'sss-vr-seg' + (searchStyle === 'page' ? ' is-active' : ''),
|
|
2268
|
+
type: 'button',
|
|
2269
|
+
onClick: () => updateSearchStyle('page'),
|
|
2270
|
+
}, t('stylePage')),
|
|
2271
|
+
React.createElement('button', {
|
|
2272
|
+
className: 'sss-vr-seg' + (searchStyle === 'mixed' ? ' is-active' : ''),
|
|
2273
|
+
type: 'button',
|
|
2274
|
+
onClick: () => updateSearchStyle('mixed'),
|
|
2275
|
+
}, t('styleMixed')),
|
|
2276
|
+
]),
|
|
2277
|
+
]),
|
|
1917
2278
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
1918
2279
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1919
2280
|
React.createElement('span', { className: 'sss-vr-label' }, t('aiSearch')),
|
|
@@ -2001,8 +2362,13 @@ window.__ModuleLoader__.load({
|
|
|
2001
2362
|
aiSaved ? t('aiConfigSaved') : t('aiConfigSave')),
|
|
2002
2363
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: testState === 'loading', onClick: () => { void testService() } },
|
|
2003
2364
|
testState === 'loading' ? t('aiTesting') : t('aiTest')),
|
|
2365
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: dshImportState === 'loading', onClick: () => { void importDshModelConfig() } },
|
|
2366
|
+
dshImportState === 'loading' ? t('aiDshImporting') : t('aiDshImport')),
|
|
2004
2367
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => requestClearAi() }, t('aiClear')),
|
|
2005
2368
|
]),
|
|
2369
|
+
dshImportMsg
|
|
2370
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, dshImportMsg)
|
|
2371
|
+
: null,
|
|
2006
2372
|
testMsg
|
|
2007
2373
|
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, testMsg)
|
|
2008
2374
|
: null,
|
|
@@ -2038,7 +2404,6 @@ window.__ModuleLoader__.load({
|
|
|
2038
2404
|
state === 'loading' ? t('updChecking') : t('updCheck')),
|
|
2039
2405
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: notesState === 'loading', onClick: () => { void fetchNotes() } },
|
|
2040
2406
|
notesState === 'loading' ? t('updFetchingNotes') : t('updReleaseNotes')),
|
|
2041
|
-
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => openWelcome() }, t('wlReopen')),
|
|
2042
2407
|
]),
|
|
2043
2408
|
notesState === 'loaded'
|
|
2044
2409
|
? React.createElement('div', { className: 'sss-vr-notes' }, notesBody)
|
|
@@ -2105,7 +2470,7 @@ window.__ModuleLoader__.load({
|
|
|
2105
2470
|
}
|
|
2106
2471
|
|
|
2107
2472
|
exports.apply = apply
|
|
2108
|
-
exports.inject = ['slots', 'locale']
|
|
2473
|
+
exports.inject = ['slots', 'locale', 'connection']
|
|
2109
2474
|
return module.exports
|
|
2110
2475
|
},
|
|
2111
2476
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectivex666/dsh-settings-search",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.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
|
|