@objectivex666/dsh-settings-search 1.7.2 → 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 +6 -2
- package/README.md +6 -2
- package/lib/client.js +1202 -142
- package/package.json +2 -1
- package/scripts/release.mjs +5 -0
package/lib/client.js
CHANGED
|
@@ -30,17 +30,27 @@ 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
|
+
searchBtn: '搜索',
|
|
39
|
+
styleTitle: '搜索框样式',
|
|
40
|
+
styleHint: '选择搜索入口的展示方式;设置会保存在本机浏览器。',
|
|
41
|
+
styleClassic: '经典侧栏',
|
|
42
|
+
styleTop: '窗口顶栏',
|
|
43
|
+
stylePage: '弹出搜索页',
|
|
44
|
+
styleMixed: '顶栏 + 搜索页',
|
|
45
|
+
openSearch: '打开搜索',
|
|
46
|
+
warming: '正在导入各分区选项…',
|
|
38
47
|
empty: '没有匹配的设置',
|
|
39
48
|
pages: '设置页',
|
|
40
49
|
options: '页内选项',
|
|
41
50
|
guess: '猜你想找',
|
|
42
51
|
page: '页面',
|
|
43
52
|
option: '选项',
|
|
53
|
+
pageSearchTitle: '搜索结果',
|
|
44
54
|
jumpFail: '未能自动跳转,请手动打开:{path}',
|
|
45
55
|
updn: '设置搜索',
|
|
46
56
|
updDesc: '设置面板顶部搜索框由本插件提供。在此检查插件的版本与更新。',
|
|
@@ -72,10 +82,35 @@ window.__ModuleLoader__.load({
|
|
|
72
82
|
aiOpenAI: 'OpenAI 兼容',
|
|
73
83
|
aiAnthropic: 'Anthropic',
|
|
74
84
|
aiBaseUrl: 'Base URL',
|
|
85
|
+
aiBaseUrlHint: '仅支持 http(s) 地址;非本机地址必须使用 https,以免泄露 API Key。',
|
|
75
86
|
aiModel: '模型名',
|
|
76
87
|
aiApiKey: 'API Key',
|
|
88
|
+
aiDshImport: '导入 DSH 模型配置',
|
|
89
|
+
aiDshImporting: '正在导入…',
|
|
90
|
+
aiDshImportOk: '已导入 Base URL 与模型名;API Key 未改动 ✓',
|
|
91
|
+
aiDshImportFail: '导入失败:{reason}',
|
|
92
|
+
aiDshImportEmpty: '未找到 DSH 当前默认模型配置',
|
|
77
93
|
aiConfigSave: '保存配置',
|
|
78
94
|
aiConfigSaved: '已保存 ✓',
|
|
95
|
+
aiClear: '清除配置',
|
|
96
|
+
aiClearTitle: '清除 API 配置',
|
|
97
|
+
aiClearConfirm: '确定要清除 Base URL、模型名与 API Key 吗?此操作不可撤销。',
|
|
98
|
+
aiClearYes: '确认清除',
|
|
99
|
+
aiTest: '测试接口',
|
|
100
|
+
aiTesting: '测试中…',
|
|
101
|
+
aiTestOk: '连接成功 ✓',
|
|
102
|
+
aiTestFail: '连接失败',
|
|
103
|
+
aiTestErrAuth: '认证失败,请检查 API Key 是否正确',
|
|
104
|
+
aiTestErrForbidden: '无权限,请检查 API Key 是否被允许使用该模型',
|
|
105
|
+
aiTestErrNotFound: '接口地址或模型名不存在,请检查 Base URL 与模型名',
|
|
106
|
+
aiTestErrRateLimit: '请求过于频繁或额度不足,请稍后重试或检查套餐',
|
|
107
|
+
aiTestErrBadRequest: '请求参数错误,请检查模型名与接口类型是否匹配',
|
|
108
|
+
aiTestErrServer: '服务端错误,请稍后重试或检查服务状态',
|
|
109
|
+
aiTestErrTimeout: '连接超时,请检查网络与 Base URL 是否可达',
|
|
110
|
+
aiTestErrNetwork: '无法连接,请检查网络或 Base URL',
|
|
111
|
+
aiTestErrBadResponse: '响应格式异常,请确认接口类型与服务兼容',
|
|
112
|
+
aiTestErrHttp: '请求失败(HTTP 状态码错误),请检查地址与配置',
|
|
113
|
+
aiTestErrUnsafeUrl: '接口地址不安全:仅支持 http(s),非本机必须为 https,且不可包含账号密码',
|
|
79
114
|
logTitle: '日志',
|
|
80
115
|
logDesc: '记录插件运行、搜索与 AI 请求的关键动作,便于排查问题。',
|
|
81
116
|
logEmpty: '暂无日志',
|
|
@@ -85,17 +120,47 @@ window.__ModuleLoader__.load({
|
|
|
85
120
|
logCopyFail: '复制失败,请稍后重试。',
|
|
86
121
|
logClear: '清空日志',
|
|
87
122
|
logCleared: '已清空',
|
|
123
|
+
logConfirmTitle: '清空日志',
|
|
124
|
+
logConfirm: '确定要清空全部日志吗?此操作不可撤销。',
|
|
125
|
+
logConfirmYes: '确认清空',
|
|
126
|
+
logConfirmCancel: '取消',
|
|
88
127
|
logExportFail: '导出失败,请重试。',
|
|
128
|
+
wlTitle: '欢迎使用设置搜索',
|
|
129
|
+
wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
|
|
130
|
+
wlFeature1Title: '实时搜索',
|
|
131
|
+
wlFeature1Desc: '支持经典侧栏、窗口顶栏、弹出搜索页与顶栏加搜索页混合模式;输入关键词即可过滤设置。',
|
|
132
|
+
wlFeature2Title: '拼音与意图联想',
|
|
133
|
+
wlFeature2Desc: '输入拼音首字母(如 sz)或直接描述需求(如「太亮」),也能联想设置项。',
|
|
134
|
+
wlFeature3Title: 'AI 联想(可选)',
|
|
135
|
+
wlFeature3Desc: '没有本地匹配时,可用你配置的模型理解意图并推荐设置项。',
|
|
136
|
+
wlFeature4Title: '点击即达',
|
|
137
|
+
wlFeature4Desc: '选中结果自动跳转到对应分区,并高亮闪烁目标选项。',
|
|
138
|
+
wlFeature5Title: '自带设置页',
|
|
139
|
+
wlFeature5Desc: '左侧导航的「设置搜索」页可检查更新、查看说明与导出日志。',
|
|
140
|
+
wlStart: '开始使用',
|
|
141
|
+
wlOpenPanel: '打开设置搜索页',
|
|
142
|
+
wlFeature6Title: '自动导入索引',
|
|
143
|
+
wlFeature6Desc: '首次打开时自动遍历一遍各设置分区,把分区内选项一并纳入搜索,无需逐页手动打开即可直接搜到。',
|
|
89
144
|
}
|
|
90
145
|
const en = {
|
|
91
146
|
title: 'Settings',
|
|
92
147
|
placeholder: 'Search settings…',
|
|
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',
|
|
156
|
+
warming: 'Importing section options…',
|
|
93
157
|
empty: 'No matching settings',
|
|
94
158
|
pages: 'Settings pages',
|
|
95
159
|
options: 'In-page options',
|
|
96
160
|
guess: 'You may want',
|
|
97
161
|
page: 'Page',
|
|
98
162
|
option: 'Option',
|
|
163
|
+
pageSearchTitle: 'Search results',
|
|
99
164
|
jumpFail: 'Could not navigate automatically — open it here: {path}',
|
|
100
165
|
updn: 'Settings Search',
|
|
101
166
|
updDesc: "The search box atop the settings panel comes from this plugin. Check its version and updates here.",
|
|
@@ -127,10 +192,35 @@ window.__ModuleLoader__.load({
|
|
|
127
192
|
aiOpenAI: 'OpenAI-compatible',
|
|
128
193
|
aiAnthropic: 'Anthropic',
|
|
129
194
|
aiBaseUrl: 'Base URL',
|
|
195
|
+
aiBaseUrlHint: 'Only http(s) URLs are allowed; remote hosts must use https to avoid exposing your API key.',
|
|
130
196
|
aiModel: 'Model',
|
|
131
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',
|
|
132
203
|
aiConfigSave: 'Save config',
|
|
133
204
|
aiConfigSaved: 'Saved ✓',
|
|
205
|
+
aiClear: 'Clear config',
|
|
206
|
+
aiClearTitle: 'Clear API config',
|
|
207
|
+
aiClearConfirm: 'Clear Base URL, model, and API key? This cannot be undone.',
|
|
208
|
+
aiClearYes: 'Clear config',
|
|
209
|
+
aiTest: 'Test API',
|
|
210
|
+
aiTesting: 'Testing…',
|
|
211
|
+
aiTestOk: 'Connection OK ✓',
|
|
212
|
+
aiTestFail: 'Connection failed',
|
|
213
|
+
aiTestErrAuth: 'Auth failed — check that your API key is correct',
|
|
214
|
+
aiTestErrForbidden: 'Forbidden — check that the API key is allowed to use this model',
|
|
215
|
+
aiTestErrNotFound: 'Not found — check the Base URL and model name',
|
|
216
|
+
aiTestErrRateLimit: 'Rate limited or quota exceeded — try again later or check your plan',
|
|
217
|
+
aiTestErrBadRequest: 'Bad request — check the model name and API type',
|
|
218
|
+
aiTestErrServer: 'Server error — try again later or check the service status',
|
|
219
|
+
aiTestErrTimeout: 'Timed out — check the network and whether the Base URL is reachable',
|
|
220
|
+
aiTestErrNetwork: 'Cannot connect — check the network or Base URL',
|
|
221
|
+
aiTestErrBadResponse: 'Unexpected response — confirm the API type matches the service',
|
|
222
|
+
aiTestErrHttp: 'Request failed (HTTP status error) — check the URL and config',
|
|
223
|
+
aiTestErrUnsafeUrl: 'Unsafe URL — only http(s) is allowed, remote hosts must use https, and embedded credentials are not permitted',
|
|
134
224
|
logTitle: 'Logs',
|
|
135
225
|
logDesc: 'Logs key plugin actions, searches, and AI requests to help troubleshoot.',
|
|
136
226
|
logEmpty: 'No logs yet',
|
|
@@ -140,12 +230,33 @@ window.__ModuleLoader__.load({
|
|
|
140
230
|
logCopyFail: 'Copy failed. Please try again.',
|
|
141
231
|
logClear: 'Clear logs',
|
|
142
232
|
logCleared: 'Cleared',
|
|
233
|
+
logConfirmTitle: 'Clear logs',
|
|
234
|
+
logConfirm: 'Clear all logs? This cannot be undone.',
|
|
235
|
+
logConfirmYes: 'Clear logs',
|
|
236
|
+
logConfirmCancel: 'Cancel',
|
|
143
237
|
logExportFail: 'Export failed. Please try again.',
|
|
238
|
+
wlTitle: 'Welcome to Settings Search',
|
|
239
|
+
wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
|
|
240
|
+
wlFeature1Title: 'Instant search',
|
|
241
|
+
wlFeature1Desc: 'Choose classic sidebar, top bar, popup page, or mixed top bar plus popup search; typing filters settings.',
|
|
242
|
+
wlFeature2Title: 'Pinyin & intent hints',
|
|
243
|
+
wlFeature2Desc: 'Type pinyin initials (e.g. sz) or describe what you want to do to find settings.',
|
|
244
|
+
wlFeature3Title: 'AI hints (optional)',
|
|
245
|
+
wlFeature3Desc: 'When no local match exists, your own model can understand the intent and suggest settings.',
|
|
246
|
+
wlFeature4Title: 'Click to navigate',
|
|
247
|
+
wlFeature4Desc: 'Selecting a result jumps to its section and flashes the target option.',
|
|
248
|
+
wlFeature5Title: 'Built-in panel',
|
|
249
|
+
wlFeature5Desc: 'The Settings Search page in the sidebar checks updates, shows notes, and exports logs.',
|
|
250
|
+
wlStart: 'Get started',
|
|
251
|
+
wlOpenPanel: 'Open Settings Search panel',
|
|
252
|
+
wlFeature6Title: 'Auto-index',
|
|
253
|
+
wlFeature6Desc: 'On first open it automatically passes through every settings section, so in-page options are searchable without opening each page manually.',
|
|
144
254
|
}
|
|
145
255
|
|
|
146
256
|
/** Inner-row slots that represent specific options inside one page. */
|
|
147
257
|
const DEEP_SLOTS = [
|
|
148
258
|
{ slot: 'settings.plugins.tab', parent: 'plugins' },
|
|
259
|
+
{ slot: 'settings.plugin.item', parent: 'plugins' },
|
|
149
260
|
{ slot: 'web-ui.plugin.item', parent: 'web-ui-plugins' },
|
|
150
261
|
{ slot: 'settings.general.item', parent: 'general' },
|
|
151
262
|
]
|
|
@@ -189,28 +300,42 @@ window.__ModuleLoader__.load({
|
|
|
189
300
|
PINYIN_TABLE.split(/\s+/).filter(Boolean).map((pair) => [pair[0], pair.slice(1)])
|
|
190
301
|
)
|
|
191
302
|
const CJK_RE = /[\u4e00-\u9fff]/
|
|
303
|
+
const ALNUM_RE = /[a-zA-Z0-9]/
|
|
192
304
|
|
|
193
305
|
/**
|
|
194
306
|
* Transliterate a label into full-pinyin and initial-letter keys for
|
|
195
307
|
* association search. Returns null when any CJK character is outside
|
|
196
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.
|
|
197
314
|
*/
|
|
315
|
+
const PINYIN_KEY_CACHE = new Map()
|
|
198
316
|
const pinyinKeys = (text) => {
|
|
317
|
+
const cached = PINYIN_KEY_CACHE.get(text)
|
|
318
|
+
if (cached !== undefined) return cached
|
|
199
319
|
let full = ''
|
|
200
320
|
let initials = ''
|
|
201
321
|
for (const ch of text) {
|
|
202
322
|
if (CJK_RE.test(ch)) {
|
|
203
323
|
const py = PINYIN.get(ch)
|
|
204
|
-
if (py === undefined)
|
|
324
|
+
if (py === undefined) {
|
|
325
|
+
PINYIN_KEY_CACHE.set(text, null)
|
|
326
|
+
return null
|
|
327
|
+
}
|
|
205
328
|
full += py
|
|
206
329
|
initials += py[0]
|
|
207
|
-
} else if (
|
|
330
|
+
} else if (ALNUM_RE.test(ch)) {
|
|
208
331
|
const lower = ch.toLowerCase()
|
|
209
332
|
full += lower
|
|
210
333
|
initials += lower
|
|
211
334
|
}
|
|
212
335
|
}
|
|
213
|
-
|
|
336
|
+
const keys = full === '' ? null : { full, initials }
|
|
337
|
+
PINYIN_KEY_CACHE.set(text, keys)
|
|
338
|
+
return keys
|
|
214
339
|
}
|
|
215
340
|
|
|
216
341
|
/** Match one query token: raw substring, full pinyin, or initial letters. */
|
|
@@ -223,6 +348,17 @@ window.__ModuleLoader__.load({
|
|
|
223
348
|
return null
|
|
224
349
|
}
|
|
225
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
|
+
|
|
226
362
|
/** Character count of the label consumed by a full-pinyin prefix. */
|
|
227
363
|
const pinyinPrefixChars = (label, pinyinLength) => {
|
|
228
364
|
let consumed = 0
|
|
@@ -271,8 +407,14 @@ window.__ModuleLoader__.load({
|
|
|
271
407
|
return { phrase, keywords: entry.slice(at + 1).split(','), keys: pinyinKeys(phrase) }
|
|
272
408
|
})
|
|
273
409
|
|
|
274
|
-
/**
|
|
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()
|
|
275
415
|
const intentKeywords = (tok) => {
|
|
416
|
+
const cached = INTENT_KEYWORD_CACHE.get(tok)
|
|
417
|
+
if (cached !== undefined) return cached
|
|
276
418
|
const hits = []
|
|
277
419
|
for (const intent of INTENTS) {
|
|
278
420
|
const phraseHit = intent.phrase.includes(tok) || tok.includes(intent.phrase) ||
|
|
@@ -284,6 +426,7 @@ window.__ModuleLoader__.load({
|
|
|
284
426
|
}
|
|
285
427
|
}
|
|
286
428
|
}
|
|
429
|
+
INTENT_KEYWORD_CACHE.set(tok, hits)
|
|
287
430
|
return hits
|
|
288
431
|
}
|
|
289
432
|
|
|
@@ -318,6 +461,24 @@ window.__ModuleLoader__.load({
|
|
|
318
461
|
const AI_BASE_URL_KEY = 'dsh-settings-search:ai-base-url'
|
|
319
462
|
const AI_MODEL_KEY = 'dsh-settings-search:ai-model'
|
|
320
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
|
+
}
|
|
321
482
|
const aiSearchEnabled = () => {
|
|
322
483
|
if (typeof localStorage === 'undefined') return false
|
|
323
484
|
return localStorage.getItem(AI_STORAGE_KEY) === '1'
|
|
@@ -328,23 +489,51 @@ window.__ModuleLoader__.load({
|
|
|
328
489
|
window.dispatchEvent(new CustomEvent(AI_TOGGLE_EVENT, { detail: enabled }))
|
|
329
490
|
}
|
|
330
491
|
const aiConfig = () => {
|
|
331
|
-
if (typeof localStorage === 'undefined'
|
|
332
|
-
|
|
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()
|
|
333
496
|
return {
|
|
334
497
|
provider: providerRaw === 'anthropic' ? 'anthropic' : 'openai',
|
|
335
|
-
baseUrl: (localStorage
|
|
336
|
-
model: (localStorage
|
|
337
|
-
apiKey: (
|
|
498
|
+
baseUrl: (localStorage?.getItem(AI_BASE_URL_KEY) ?? '').trim(),
|
|
499
|
+
model: (localStorage?.getItem(AI_MODEL_KEY) ?? '').trim(),
|
|
500
|
+
apiKey: readApiKey(),
|
|
338
501
|
}
|
|
339
502
|
}
|
|
340
503
|
const setAiConfig = (cfg) => {
|
|
504
|
+
if (typeof localStorage === 'undefined' && typeof sessionStorage === 'undefined') return
|
|
505
|
+
writeApiKey(cfg.apiKey)
|
|
341
506
|
if (typeof localStorage === 'undefined') return
|
|
342
507
|
localStorage.setItem(AI_PROVIDER_KEY, cfg.provider === 'anthropic' ? 'anthropic' : 'openai')
|
|
343
508
|
localStorage.setItem(AI_BASE_URL_KEY, cfg.baseUrl ?? '')
|
|
344
509
|
localStorage.setItem(AI_MODEL_KEY, cfg.model ?? '')
|
|
345
|
-
localStorage.setItem(AI_API_KEY_KEY, cfg.apiKey ?? '')
|
|
346
510
|
window.dispatchEvent(new CustomEvent(AI_CONFIG_EVENT))
|
|
347
511
|
}
|
|
512
|
+
/** First-run onboarding: remembered via localStorage so it shows once. */
|
|
513
|
+
const ONBOARD_KEY = 'dsh-settings-search:onboarded'
|
|
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
|
+
}
|
|
528
|
+
const welcomed = () => {
|
|
529
|
+
if (typeof localStorage === 'undefined') return true
|
|
530
|
+
return localStorage.getItem(ONBOARD_KEY) === '1'
|
|
531
|
+
}
|
|
532
|
+
const setWelcomed = () => {
|
|
533
|
+
if (typeof localStorage === 'undefined') return
|
|
534
|
+
localStorage.setItem(ONBOARD_KEY, '1')
|
|
535
|
+
window.dispatchEvent(new CustomEvent(WELCOME_EVENT, { detail: false }))
|
|
536
|
+
}
|
|
348
537
|
const extractAiKeywords = (raw) => {
|
|
349
538
|
const text = String(raw ?? '').replace(/^```(?:json)?/i, '').replace(/```$/, '').trim()
|
|
350
539
|
const start = text.indexOf('{')
|
|
@@ -360,19 +549,49 @@ window.__ModuleLoader__.load({
|
|
|
360
549
|
.slice(0, 6)
|
|
361
550
|
}
|
|
362
551
|
const AI_PROMPT = '你是设置搜索助手。只输出 JSON 对象,格式为 {"keywords":["..."]}。keywords 最多 6 个,每个最多 12 个字符,只能从用户提供的设置索引中选择或提取核心词,不要解释。'
|
|
552
|
+
const loopbackHost = (hostname) => {
|
|
553
|
+
const h = String(hostname || '').replace(/^\[|\]$/g, '').toLowerCase()
|
|
554
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0.0.0.0'
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Normalize and validate an AI service base URL. Prevents the API key
|
|
558
|
+
* from being sent to a dangerous scheme (javascript:, data:, …), to a URL
|
|
559
|
+
* with embedded credentials, or over cleartext HTTP to a non-loopback
|
|
560
|
+
* host. Returns a normalized origin+path (no trailing slash) or ''.
|
|
561
|
+
*/
|
|
562
|
+
const safeBaseUrl = (baseUrl) => {
|
|
563
|
+
const value = String(baseUrl ?? '').trim()
|
|
564
|
+
if (!value) return ''
|
|
565
|
+
const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) ? value : `https://${value}`
|
|
566
|
+
let url
|
|
567
|
+
try { url = new URL(candidate) } catch { return '' }
|
|
568
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:') return ''
|
|
569
|
+
if (url.username || url.password) return ''
|
|
570
|
+
const isHttp = url.protocol === 'http:'
|
|
571
|
+
if (!url.hostname) return ''
|
|
572
|
+
if (isHttp && !loopbackHost(url.hostname)) return ''
|
|
573
|
+
const path = url.pathname === '/' ? '' : url.pathname
|
|
574
|
+
return url.origin + path.replace(/\/+$/, '')
|
|
575
|
+
}
|
|
363
576
|
const chatCompletionsUrl = (baseUrl) => {
|
|
364
|
-
|
|
365
|
-
|
|
577
|
+
// safeBaseUrl already strips trailing slashes
|
|
578
|
+
const safe = safeBaseUrl(baseUrl)
|
|
579
|
+
if (!safe) return ''
|
|
580
|
+
return /\/chat\/completions$/i.test(safe) ? safe : `${safe}/chat/completions`
|
|
366
581
|
}
|
|
367
582
|
const anthropicMessagesUrl = (baseUrl) => {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
583
|
+
// safeBaseUrl already strips trailing slashes
|
|
584
|
+
const safe = safeBaseUrl(baseUrl)
|
|
585
|
+
if (!safe) return ''
|
|
586
|
+
if (/\/v1\/messages$/i.test(safe)) return safe
|
|
587
|
+
return /\/v1$/i.test(safe) ? `${safe}/messages` : `${safe}/v1/messages`
|
|
371
588
|
}
|
|
372
589
|
const requestUserAi = async (query, labels, signal) => {
|
|
373
590
|
const { provider, baseUrl, model, apiKey } = aiConfig()
|
|
374
591
|
if (!baseUrl || !model || !apiKey) return []
|
|
375
592
|
const isAnthropic = provider === 'anthropic'
|
|
593
|
+
const url = isAnthropic ? anthropicMessagesUrl(baseUrl) : chatCompletionsUrl(baseUrl)
|
|
594
|
+
if (!url) return []
|
|
376
595
|
const startedAt = Date.now()
|
|
377
596
|
log('info', 'AI 请求', { provider, model, query: String(query).slice(0, 50) })
|
|
378
597
|
let timer
|
|
@@ -407,7 +626,7 @@ window.__ModuleLoader__.load({
|
|
|
407
626
|
],
|
|
408
627
|
temperature: 0,
|
|
409
628
|
}
|
|
410
|
-
const response = await fetch(
|
|
629
|
+
const response = await fetch(url, {
|
|
411
630
|
method: 'POST',
|
|
412
631
|
headers,
|
|
413
632
|
signal,
|
|
@@ -439,10 +658,96 @@ window.__ModuleLoader__.load({
|
|
|
439
658
|
const cacheKey = query.toLowerCase()
|
|
440
659
|
if (AI_CACHE.has(cacheKey)) return AI_CACHE.get(cacheKey)
|
|
441
660
|
const keywords = await requestUserAi(query, labels, signal)
|
|
442
|
-
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
|
+
}
|
|
443
666
|
return keywords
|
|
444
667
|
}
|
|
445
668
|
|
|
669
|
+
/**
|
|
670
|
+
* Lightweight connectivity test for the configured model service. Sends a
|
|
671
|
+
* minimal chat request and reports whether the endpoint/auth/model work.
|
|
672
|
+
* Returns { ok, code, detail } where code is a stable error kind used to
|
|
673
|
+
* look up a localized hint in the UI.
|
|
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
|
+
}
|
|
683
|
+
const testHttpCode = (status) => {
|
|
684
|
+
const code = Number(status)
|
|
685
|
+
return HTTP_TEST_CODES[code] ?? (code >= 500 && code <= 599 ? 'server' : 'http')
|
|
686
|
+
}
|
|
687
|
+
const testAiService = async (cfg) => {
|
|
688
|
+
const provider = cfg.provider === 'anthropic' ? 'anthropic' : 'openai'
|
|
689
|
+
const baseUrl = String(cfg.baseUrl ?? '').trim()
|
|
690
|
+
const model = String(cfg.model ?? '').trim()
|
|
691
|
+
const apiKey = String(cfg.apiKey ?? '').trim()
|
|
692
|
+
if (!baseUrl || !model || !apiKey) return { ok: false, code: 'need-config', detail: '' }
|
|
693
|
+
const isAnthropic = provider === 'anthropic'
|
|
694
|
+
const requestUrl = isAnthropic ? anthropicMessagesUrl(baseUrl) : chatCompletionsUrl(baseUrl)
|
|
695
|
+
if (!requestUrl) return { ok: false, code: 'unsafe-url', detail: '' }
|
|
696
|
+
const startedAt = Date.now()
|
|
697
|
+
log('info', '测试 API 服务', { provider, model })
|
|
698
|
+
let timer
|
|
699
|
+
const deadline = new Promise((_, reject) => {
|
|
700
|
+
timer = setTimeout(() => {
|
|
701
|
+
const error = new Error('AI request timed out')
|
|
702
|
+
error.name = 'AI_TIMEOUT'
|
|
703
|
+
reject(error)
|
|
704
|
+
}, 12000)
|
|
705
|
+
})
|
|
706
|
+
const work = (async () => {
|
|
707
|
+
const headers = {
|
|
708
|
+
'content-type': 'application/json',
|
|
709
|
+
...(isAnthropic
|
|
710
|
+
? { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'anthropic-dangerous-direct-browser-access': 'true' }
|
|
711
|
+
: { authorization: `Bearer ${apiKey}` }),
|
|
712
|
+
}
|
|
713
|
+
const body = isAnthropic
|
|
714
|
+
? { model, max_tokens: 1, messages: [{ role: 'user', content: 'ping' }] }
|
|
715
|
+
: { model, messages: [{ role: 'user', content: 'ping' }], max_tokens: 1, temperature: 0 }
|
|
716
|
+
const response = await fetch(requestUrl, {
|
|
717
|
+
method: 'POST',
|
|
718
|
+
headers,
|
|
719
|
+
body: JSON.stringify(body),
|
|
720
|
+
})
|
|
721
|
+
if (!response.ok) return { ok: false, code: testHttpCode(response.status), detail: `HTTP ${response.status}` }
|
|
722
|
+
const data = await response.json().catch(() => ({}))
|
|
723
|
+
const valid = isAnthropic
|
|
724
|
+
? (Array.isArray(data?.content) && data.content.length > 0)
|
|
725
|
+
: Boolean(data?.choices?.[0]?.message)
|
|
726
|
+
if (!valid) return { ok: false, code: 'bad-response', detail: '' }
|
|
727
|
+
return { ok: true }
|
|
728
|
+
})()
|
|
729
|
+
try {
|
|
730
|
+
const result = await Promise.race([work, deadline])
|
|
731
|
+
if (result.ok) log('info', '测试 API 成功', { duration: Date.now() - startedAt })
|
|
732
|
+
else log('error', '测试 API 失败', { code: result.code ?? 'http', detail: result.detail ?? '', duration: Date.now() - startedAt })
|
|
733
|
+
return result
|
|
734
|
+
} catch (error) {
|
|
735
|
+
if (error.name === 'AbortError') {
|
|
736
|
+
log('warn', '测试 API 取消', { duration: Date.now() - startedAt })
|
|
737
|
+
throw error
|
|
738
|
+
}
|
|
739
|
+
if (error.name === 'AI_TIMEOUT') {
|
|
740
|
+
log('error', '测试 API 超时', { duration: Date.now() - startedAt })
|
|
741
|
+
return { ok: false, code: 'timeout', detail: '' }
|
|
742
|
+
}
|
|
743
|
+
const reason = String(error?.message ?? error).slice(0, 160)
|
|
744
|
+
log('error', '测试 API 失败', { detail: reason, duration: Date.now() - startedAt })
|
|
745
|
+
return { ok: false, code: 'network', detail: reason }
|
|
746
|
+
} finally {
|
|
747
|
+
clearTimeout(timer)
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
446
751
|
const CSS = `
|
|
447
752
|
.sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
448
753
|
.sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
|
|
@@ -452,6 +757,10 @@ window.__ModuleLoader__.load({
|
|
|
452
757
|
.sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
453
758
|
.sss-clear { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1; cursor: pointer; }
|
|
454
759
|
.sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
760
|
+
.sss-icon-btn { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; }
|
|
761
|
+
.sss-icon-btn:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
762
|
+
.sss-icon-btn:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
|
|
763
|
+
.sss-warm-note { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 12px; line-height: 16px; color: var(--dsw-alias-label-secondary); }
|
|
455
764
|
.sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height: 320px; overflow: auto; }
|
|
456
765
|
.sss-groups { display: flex; flex-direction: column; gap: 8px; }
|
|
457
766
|
.sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
|
|
@@ -460,7 +769,7 @@ window.__ModuleLoader__.load({
|
|
|
460
769
|
.sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
461
770
|
.sss-item.is-active { background: var(--dsw-alias-interactive-bg-hover); }
|
|
462
771
|
.sss-item-col { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
463
|
-
.sss-item-label { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px;
|
|
772
|
+
.sss-item-label { color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 18px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
|
|
464
773
|
.sss-item-label mark { background: transparent; color: inherit; font-weight: 600; }
|
|
465
774
|
.sss-item-path { font-size: 10px; line-height: 14px; color: var(--dsw-alias-label-secondary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
466
775
|
.sss-item-kind { flex-shrink: 0; font-size: 10px; color: var(--dsw-alias-label-secondary); border: 1px solid var(--dsw-alias-border-l2); border-radius: 4px; padding: 1px 5px; }
|
|
@@ -468,6 +777,25 @@ window.__ModuleLoader__.load({
|
|
|
468
777
|
.sss-ai-note { margin: 0; padding: 6px 8px; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
|
|
469
778
|
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
470
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; }
|
|
471
799
|
@keyframes sssFlashBg { 0% { background-color: var(--dsw-alias-interactive-bg-hover); } 100% { background-color: transparent; } }
|
|
472
800
|
.sss-flash { animation: sssFlashBg 1.8s ease-out; border-radius: 8px; }
|
|
473
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; }
|
|
@@ -491,12 +819,17 @@ window.__ModuleLoader__.load({
|
|
|
491
819
|
.sss-vr-input { border: 1px solid var(--dsw-alias-border-l2); background: var(--dsw-alias-bg-layer-3); min-height: 34px; font: inherit; color: var(--dsw-alias-label-primary); border-radius: 8px; padding: 6px 12px; font-size: 13px; line-height: 1.5; width: 100%; box-sizing: border-box; }
|
|
492
820
|
.sss-vr-input:focus-visible { border-color: var(--dsw-alias-brand-primary); outline: none; }
|
|
493
821
|
.sss-vr-input::placeholder { color: var(--dsw-alias-label-dimmed); }
|
|
822
|
+
.sss-vr-input.sss-vr-mask { -webkit-text-security: disc; }
|
|
494
823
|
.sss-vr-select { cursor: pointer; }
|
|
495
824
|
.sss-vr-check { width: 16px; height: 16px; accent-color: var(--dsw-alias-brand-primary); cursor: pointer; margin: 0; }
|
|
496
825
|
.sss-vr-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 5px 14px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-secondary); background: 0 0; }
|
|
497
826
|
.sss-vr-btn:hover:not(:disabled) { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
498
827
|
.sss-vr-btn:disabled { opacity: .4; cursor: default; }
|
|
499
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; }
|
|
500
833
|
.sss-vr-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
501
834
|
.sss-vr-primary:hover:not(:disabled) { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
502
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; }
|
|
@@ -514,6 +847,40 @@ window.__ModuleLoader__.load({
|
|
|
514
847
|
.sss-vr-link { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; padding: 5px 14px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-secondary); background: 0 0; text-decoration: none; display: inline-flex; align-items: center; }
|
|
515
848
|
.sss-vr-link:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
516
849
|
.sss-vr-notes { box-sizing: border-box; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); padding: 10px 12px; font-size: 12px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; max-height: 240px; overflow-y: auto; margin-top: 8px; }
|
|
850
|
+
.sss-wl-overlay { position: fixed; inset: 0; z-index: 999; display: flex; align-items: center; justify-content: center; padding: 20px; background: var(--dsw-alias-bg-mask, rgb(0 0 0 / 36%)); box-sizing: border-box; }
|
|
851
|
+
.sss-wl { width: 100%; max-width: 520px; max-height: min(88vh, 720px); overflow: auto; box-sizing: border-box; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 16px; box-shadow: var(--dsw-shadow-lv3); padding: 22px 24px 20px; }
|
|
852
|
+
.sss-wl-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
853
|
+
.sss-wl-headText { flex: 1; min-width: 0; }
|
|
854
|
+
.sss-wl-title { margin: 0; color: var(--dsw-alias-label-primary); font-size: 19px; font-weight: 650; line-height: 1.4; }
|
|
855
|
+
.sss-wl-subtitle { margin: 6px 0 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 1.6; }
|
|
856
|
+
.sss-wl-close { flex: none; appearance: none; cursor: pointer; width: 28px; height: 28px; padding: 0; border: none; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 15px; line-height: 1; }
|
|
857
|
+
.sss-wl-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
858
|
+
.sss-wl-features { list-style: none; margin: 18px 0 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
|
|
859
|
+
.sss-wl-feature { display: flex; gap: 12px; align-items: flex-start; padding: 11px 12px; border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; background: var(--dsw-alias-bg-layer-1); }
|
|
860
|
+
.sss-wl-feature-icon { flex: none; width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 1; }
|
|
861
|
+
.sss-wl-feature-body { flex: 1; min-width: 0; }
|
|
862
|
+
.sss-wl-feature-title { color: var(--dsw-alias-label-primary); font-size: 13px; font-weight: 600; line-height: 1.5; }
|
|
863
|
+
.sss-wl-feature-desc { margin: 2px 0 0; color: var(--dsw-alias-label-tertiary); font-size: 12px; line-height: 1.6; }
|
|
864
|
+
.sss-wl-footer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 20px; }
|
|
865
|
+
.sss-wl-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; padding: 7px 16px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-secondary); background: 0 0; }
|
|
866
|
+
.sss-wl-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
867
|
+
.sss-wl-primary { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
868
|
+
.sss-wl-primary:hover { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
869
|
+
.sss-wl-skip { margin-left: auto; border: none; background: transparent; color: var(--dsw-alias-label-tertiary); padding: 7px 4px; }
|
|
870
|
+
.sss-wl-skip:hover { color: var(--dsw-alias-label-primary); border-color: transparent; }
|
|
871
|
+
.sss-cf-overlay { position: fixed; inset: 0; z-index: 999; display: flex; align-items: center; justify-content: center; padding: 20px; background: var(--dsw-alias-bg-mask, rgb(0 0 0 / 36%)); box-sizing: border-box; }
|
|
872
|
+
.sss-cf { width: 100%; max-width: 400px; box-sizing: border-box; background: var(--dsw-alias-bg-layer-3); border: 1px solid var(--dsw-alias-border-l2); border-radius: 14px; box-shadow: var(--dsw-shadow-lv3); padding: 20px 22px 18px; }
|
|
873
|
+
.sss-cf-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
874
|
+
.sss-cf-headText { flex: 1; min-width: 0; }
|
|
875
|
+
.sss-cf-title { margin: 0; color: var(--dsw-alias-label-primary); font-size: 16px; font-weight: 650; line-height: 1.4; }
|
|
876
|
+
.sss-cf-desc { margin: 8px 0 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 1.6; }
|
|
877
|
+
.sss-cf-close { flex: none; appearance: none; cursor: pointer; width: 28px; height: 28px; padding: 0; border: none; border-radius: 7px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 15px; line-height: 1; }
|
|
878
|
+
.sss-cf-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
879
|
+
.sss-cf-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
880
|
+
.sss-cf-btn { appearance: none; font: inherit; cursor: pointer; border: 1px solid var(--dsw-alias-border-l2); border-radius: 9px; padding: 7px 16px; font-size: 13px; line-height: 1.5; color: var(--dsw-alias-label-secondary); background: 0 0; }
|
|
881
|
+
.sss-cf-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
882
|
+
.sss-cf-danger { background: #c0392b; color: #fff; border-color: transparent; }
|
|
883
|
+
.sss-cf-danger:hover { background: #a93226; color: #fff; border-color: transparent; }
|
|
517
884
|
`
|
|
518
885
|
let stylesInstalled = false
|
|
519
886
|
function installStyles() {
|
|
@@ -543,7 +910,6 @@ window.__ModuleLoader__.load({
|
|
|
543
910
|
function createModel(ctx) {
|
|
544
911
|
/** id → harvested visible title, per slot. */
|
|
545
912
|
const harvest = new Map()
|
|
546
|
-
let harvestSeq = 0
|
|
547
913
|
|
|
548
914
|
const entriesOf = (slot) => {
|
|
549
915
|
try { return ctx.slots.entries(slot) ?? [] } catch { return [] }
|
|
@@ -558,7 +924,7 @@ window.__ModuleLoader__.load({
|
|
|
558
924
|
if (typeof o.locale === 'string') {
|
|
559
925
|
const t = callSafe(() => ctx.locale.bind(o.locale))
|
|
560
926
|
if (typeof t === 'function') {
|
|
561
|
-
for (const key of ['tab', 'title', 'card.title', 'name']) {
|
|
927
|
+
for (const key of ['tab', 'title', 'card.title', 'name', 'nav']) {
|
|
562
928
|
const value = callSafe(() => t(key))
|
|
563
929
|
if (typeof value === 'string' && value !== '' && value !== key) return value
|
|
564
930
|
}
|
|
@@ -576,6 +942,77 @@ window.__ModuleLoader__.load({
|
|
|
576
942
|
* stacked section): harvest each direct child as one row, keyed by
|
|
577
943
|
* render order ("#i") since id pairing is impossible.
|
|
578
944
|
*/
|
|
945
|
+
/** Strip a trailing description (e.g. "关闭后恢复…") from a row label. */
|
|
946
|
+
const trimDescription = (text) => {
|
|
947
|
+
const t = (text ?? '').replace(/\s+/g, ' ').trim()
|
|
948
|
+
if (t === '') return ''
|
|
949
|
+
const re = /(?:[\s,,。;;])?(关闭后|开启后|启用后|禁用后|创建后|使用时|点击后|勾选后|开启时|关闭时|完成后|启用时|禁用时|开始后|停止后)/
|
|
950
|
+
const m = re.exec(t)
|
|
951
|
+
if (m) {
|
|
952
|
+
const head = t.slice(0, m.index).trim()
|
|
953
|
+
if (head.length >= 2 && head.length <= 60) return head
|
|
954
|
+
}
|
|
955
|
+
const p = t.indexOf('。')
|
|
956
|
+
if (p > 0) {
|
|
957
|
+
const head = t.slice(0, p).trim()
|
|
958
|
+
if (head.length >= 2 && head.length <= 40 && head.length < t.length - p) return head
|
|
959
|
+
}
|
|
960
|
+
return t
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const mineInnerOptions = (node) => {
|
|
964
|
+
try {
|
|
965
|
+
const seen = new Set()
|
|
966
|
+
const out = []
|
|
967
|
+
const textOf = (el) => (el.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
968
|
+
const titleOf = (el) => {
|
|
969
|
+
const t = textOf(el)
|
|
970
|
+
return t.length >= 2 && t.length <= 120 ? t : ''
|
|
971
|
+
}
|
|
972
|
+
const bestTitle = (row) => {
|
|
973
|
+
let best = ''
|
|
974
|
+
let bestLen = 1e9
|
|
975
|
+
const sels = ['h1,h2,h3,h4,h5', '[class*="title"]', '[class*="heading"]', '[class*="name"]', '[class*="label"]', 'label', 'strong', 'b']
|
|
976
|
+
for (const sel of sels) {
|
|
977
|
+
for (const el of row.querySelectorAll(sel)) {
|
|
978
|
+
if (el === row) continue
|
|
979
|
+
const t = titleOf(el)
|
|
980
|
+
if (t === '') continue
|
|
981
|
+
if (t.length < bestLen) { best = t; bestLen = t.length }
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
if (best !== '') return best
|
|
985
|
+
const raw = textOf(row)
|
|
986
|
+
const part = raw.split('·')[0].trim()
|
|
987
|
+
return part.length >= 2 && part.length <= 120 ? part : raw.slice(0, 120)
|
|
988
|
+
}
|
|
989
|
+
const push = (label, el) => {
|
|
990
|
+
const clean = trimDescription(label)
|
|
991
|
+
const text = textOf(el)
|
|
992
|
+
if (clean.length < 2 || el === node || seen.has(clean)) return
|
|
993
|
+
seen.add(clean)
|
|
994
|
+
out.push({ label: clean, text })
|
|
995
|
+
}
|
|
996
|
+
const ctrls = node.querySelectorAll('input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"], [role="radio"], button[aria-pressed], button[aria-checked]')
|
|
997
|
+
for (const ctl of ctrls) {
|
|
998
|
+
let row = ctl
|
|
999
|
+
for (let d = 0; d < 6 && row.parentElement; d += 1) {
|
|
1000
|
+
row = row.parentElement
|
|
1001
|
+
if (row === node) break
|
|
1002
|
+
const t = textOf(row)
|
|
1003
|
+
if (t.length >= 2 && t.length <= 220) break
|
|
1004
|
+
}
|
|
1005
|
+
push(bestTitle(row), row)
|
|
1006
|
+
}
|
|
1007
|
+
const heads = node.querySelectorAll('h1,h2,h3,h4,h5,label,[class*="title"],[class*="heading"],[class*="label"],[class*="name"]')
|
|
1008
|
+
for (const h of heads) push(bestTitle(h) || titleOf(h), h)
|
|
1009
|
+
return out.slice(0, 24)
|
|
1010
|
+
} catch {
|
|
1011
|
+
return []
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
|
|
579
1016
|
const runHarvest = () => {
|
|
580
1017
|
let grew = false
|
|
581
1018
|
if (typeof document === 'undefined') return false
|
|
@@ -589,7 +1026,7 @@ window.__ModuleLoader__.load({
|
|
|
589
1026
|
if (containers.length === 1 && containers[0].childElementCount > 0) {
|
|
590
1027
|
nodes = [...containers[0].children]
|
|
591
1028
|
} else {
|
|
592
|
-
nodes = containers
|
|
1029
|
+
nodes = containers
|
|
593
1030
|
}
|
|
594
1031
|
if (nodes.length === 0) continue
|
|
595
1032
|
map = harvest.get(cfg.slot)
|
|
@@ -601,28 +1038,60 @@ window.__ModuleLoader__.load({
|
|
|
601
1038
|
if (map.has(key)) return
|
|
602
1039
|
const title = titleFromNode(node)
|
|
603
1040
|
if (title === '') return
|
|
604
|
-
const text = (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0,
|
|
605
|
-
|
|
1041
|
+
const text = (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
1042
|
+
const record = { title, idx: i, text, inner: mineInnerOptions(node) }
|
|
1043
|
+
map.set(key, record); grew = true
|
|
606
1044
|
})
|
|
607
1045
|
continue
|
|
608
1046
|
}
|
|
609
1047
|
regs.forEach((entry, i) => {
|
|
610
|
-
const id = String(entry.options?.id ?? i)
|
|
1048
|
+
const id = String(entry.options?.id ?? entry.options?.key ?? i)
|
|
611
1049
|
if (map.has(id)) return
|
|
612
1050
|
const title = titleFromNode(nodes[i])
|
|
613
1051
|
if (title === '') return
|
|
614
|
-
const text = (nodes[i].textContent ?? '').replace(/\s+/g, ' ').trim().slice(0,
|
|
615
|
-
|
|
1052
|
+
const text = (nodes[i].textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
1053
|
+
const record = { title, idx: i, text, inner: mineInnerOptions(nodes[i]) }
|
|
1054
|
+
map.set(id, record); map.set('#' + i, record); grew = true
|
|
616
1055
|
})
|
|
617
1056
|
}
|
|
618
|
-
|
|
1057
|
+
// Mine the active first-level section's own inline controls
|
|
1058
|
+
// (e.g. 创意工坊 → 启用创意工坊卡片), which live outside DEEP_SLOTS.
|
|
1059
|
+
try {
|
|
1060
|
+
const sections = readSections()
|
|
1061
|
+
const activeLabel = activeSectionLabel()
|
|
1062
|
+
const active = activeLabel === '' ? undefined : sections.find((s) => s.label === activeLabel)
|
|
1063
|
+
if (active && active.id !== '') {
|
|
1064
|
+
const containers = [...document.querySelectorAll('[data-slot="settings.section"]')]
|
|
1065
|
+
const node = containers.length === 1 && containers[0].childElementCount > 0
|
|
1066
|
+
? containers[0].children[0] ?? null
|
|
1067
|
+
: containers[0] ?? null
|
|
1068
|
+
if (node) {
|
|
1069
|
+
const inner = mineInnerOptions(node)
|
|
1070
|
+
const text = (node.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 500)
|
|
1071
|
+
if (inner.length > 0 || text !== '') {
|
|
1072
|
+
let map = harvest.get('settings.section')
|
|
1073
|
+
if (map === undefined) { map = new Map(); harvest.set('settings.section', map) }
|
|
1074
|
+
const rec = map.get(active.id)
|
|
1075
|
+
if (!rec) {
|
|
1076
|
+
map.set(active.id, { title: active.label, idx: 0, text, inner })
|
|
1077
|
+
grew = true
|
|
1078
|
+
} else if ((rec.inner?.length ?? 0) === 0 && inner.length > 0) {
|
|
1079
|
+
rec.inner = inner
|
|
1080
|
+
rec.text = text
|
|
1081
|
+
grew = true
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
} catch { /* section mining is best-effort */ }
|
|
619
1087
|
return grew
|
|
620
1088
|
}
|
|
621
1089
|
|
|
622
1090
|
const titleFromNode = (node) => {
|
|
623
1091
|
const pick = (el) => {
|
|
624
1092
|
const text = (el.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
625
|
-
|
|
1093
|
+
if (text === '' || text.length > 200) return ''
|
|
1094
|
+
return trimDescription(text)
|
|
626
1095
|
}
|
|
627
1096
|
for (const sel of ['h1,h2,h3,h4,h5', '[class*="title"]', '[class*="heading"]', 'label', 'p']) {
|
|
628
1097
|
const hit = node.querySelector(sel)
|
|
@@ -632,7 +1101,7 @@ window.__ModuleLoader__.load({
|
|
|
632
1101
|
if (first) {
|
|
633
1102
|
for (const child of first.children) { const text = pick(child); if (text !== '') return text }
|
|
634
1103
|
}
|
|
635
|
-
return pick(node).split('·')[0].trim()
|
|
1104
|
+
return trimDescription(pick(node).split('·')[0].trim())
|
|
636
1105
|
}
|
|
637
1106
|
|
|
638
1107
|
const readSections = () => sortEntries(entriesOf('settings.section'))
|
|
@@ -650,10 +1119,14 @@ window.__ModuleLoader__.load({
|
|
|
650
1119
|
const parentLabel = (parentId) =>
|
|
651
1120
|
sections.find((s) => s.id === parentId)?.label ?? parentId
|
|
652
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()
|
|
653
1126
|
for (const cfg of DEEP_SLOTS) {
|
|
654
1127
|
const mapped = harvest.get(cfg.slot)
|
|
655
1128
|
sortEntries(entriesOf(cfg.slot)).forEach((entry, i) => {
|
|
656
|
-
const id = String(entry.options?.id ?? '')
|
|
1129
|
+
const id = String(entry.options?.id ?? entry.options?.key ?? '')
|
|
657
1130
|
if (id === '') return
|
|
658
1131
|
// static label first, then id-paired harvest, then loose index-keyed harvest
|
|
659
1132
|
const hitId = mapped?.get(id)
|
|
@@ -662,7 +1135,10 @@ window.__ModuleLoader__.load({
|
|
|
662
1135
|
const label = staticLabel(entry, cfg) || hitObj?.title || ''
|
|
663
1136
|
if (label === '') return // unmapped, unharvested — invisible until it renders once
|
|
664
1137
|
const key = cfg.slot + '#' + id
|
|
665
|
-
if (
|
|
1138
|
+
if (seenKeys.has(key)) return
|
|
1139
|
+
const pageLabel = parentLabel(cfg.parent)
|
|
1140
|
+
seenKeys.add(key)
|
|
1141
|
+
seenPairs.add(pageLabel + '\u0000' + label)
|
|
666
1142
|
out.push({
|
|
667
1143
|
kind: 'option',
|
|
668
1144
|
key,
|
|
@@ -673,24 +1149,90 @@ window.__ModuleLoader__.load({
|
|
|
673
1149
|
hay: label + ' ' + (hitObj?.text ?? ''),
|
|
674
1150
|
rowIndex: hitObj?.idx ?? i,
|
|
675
1151
|
parentId: cfg.parent,
|
|
676
|
-
parentLabel:
|
|
1152
|
+
parentLabel: pageLabel,
|
|
677
1153
|
})
|
|
1154
|
+
if (cfg.slot === 'settings.plugin.item' && Array.isArray(hitObj?.inner)) {
|
|
1155
|
+
hitObj.inner.forEach((op, oi) => {
|
|
1156
|
+
if (op.label === '' || op.label === label) return
|
|
1157
|
+
const innerKey = key + '#o' + oi
|
|
1158
|
+
if (seenKeys.has(innerKey)) return
|
|
1159
|
+
seenKeys.add(innerKey)
|
|
1160
|
+
seenPairs.add(label + '\u0000' + op.label)
|
|
1161
|
+
out.push({
|
|
1162
|
+
kind: 'option',
|
|
1163
|
+
key: innerKey,
|
|
1164
|
+
slot: cfg.slot,
|
|
1165
|
+
id: id + '#o' + oi,
|
|
1166
|
+
order: (entry.options?.order ?? 0) + oi / 1000,
|
|
1167
|
+
label: op.label,
|
|
1168
|
+
hay: `${op.text} ${label} ${pageLabel}`.toLowerCase(),
|
|
1169
|
+
rowIndex: hitObj?.idx ?? i,
|
|
1170
|
+
parentId: cfg.parent,
|
|
1171
|
+
parentLabel: label,
|
|
1172
|
+
pageLabel,
|
|
1173
|
+
inner: true,
|
|
1174
|
+
cardLabel: label,
|
|
1175
|
+
})
|
|
1176
|
+
})
|
|
1177
|
+
}
|
|
678
1178
|
})
|
|
679
1179
|
}
|
|
1180
|
+
// Expose inline options mined from active first-level sections.
|
|
1181
|
+
{
|
|
1182
|
+
const mapped = harvest.get('settings.section')
|
|
1183
|
+
if (mapped) {
|
|
1184
|
+
sections.forEach((sec) => {
|
|
1185
|
+
const id = sec.id
|
|
1186
|
+
if (id === '') return
|
|
1187
|
+
const rec = mapped.get(id)
|
|
1188
|
+
if (!rec || !Array.isArray(rec.inner)) return
|
|
1189
|
+
const label = sec.label
|
|
1190
|
+
if (label === '') return
|
|
1191
|
+
rec.inner.forEach((op, oi) => {
|
|
1192
|
+
if (op.label === '' || op.label === label) return
|
|
1193
|
+
const innerKey = 'settings.section#' + id + '#o' + oi
|
|
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)
|
|
1199
|
+
out.push({
|
|
1200
|
+
kind: 'option',
|
|
1201
|
+
key: innerKey,
|
|
1202
|
+
slot: 'settings.section',
|
|
1203
|
+
id: id + '#o' + oi,
|
|
1204
|
+
order: sec.order + oi / 1000,
|
|
1205
|
+
label: op.label,
|
|
1206
|
+
hay: `${op.text} ${label}`.toLowerCase(),
|
|
1207
|
+
rowIndex: rec.idx ?? 0,
|
|
1208
|
+
parentId: id,
|
|
1209
|
+
parentLabel: label,
|
|
1210
|
+
pageLabel: label,
|
|
1211
|
+
inner: true,
|
|
1212
|
+
cardLabel: label,
|
|
1213
|
+
})
|
|
1214
|
+
})
|
|
1215
|
+
})
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
680
1218
|
return out.sort((a, b) => a.parentLabel !== b.parentLabel
|
|
681
1219
|
? a.parentLabel.localeCompare(b.parentLabel)
|
|
682
1220
|
: a.order - b.order)
|
|
683
1221
|
}
|
|
684
1222
|
|
|
685
|
-
return { readSections, readDeep, runHarvest
|
|
1223
|
+
return { readSections, readDeep, runHarvest }
|
|
686
1224
|
}
|
|
687
1225
|
|
|
688
1226
|
/* ---------- DOM-level navigation into the settings shell ---------- */
|
|
689
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
|
+
}
|
|
690
1233
|
const panelButtons = () => {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return [...root.querySelectorAll('button')]
|
|
1234
|
+
const root = panelRoot()
|
|
1235
|
+
return root === null ? [] : [...root.querySelectorAll('button')]
|
|
694
1236
|
}
|
|
695
1237
|
const clickNavButton = (wantText) => {
|
|
696
1238
|
const want = String(wantText ?? '').trim()
|
|
@@ -703,16 +1245,56 @@ window.__ModuleLoader__.load({
|
|
|
703
1245
|
return true
|
|
704
1246
|
}
|
|
705
1247
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
1248
|
+
/** Label of the currently-active first-level settings nav item. */
|
|
1249
|
+
const activeSectionLabel = () => {
|
|
1250
|
+
const root = panelRoot()
|
|
1251
|
+
if (root === null) return ''
|
|
1252
|
+
const isTab = (b) => b.getAttribute('role') === 'tab'
|
|
1253
|
+
const buttons = [...root.querySelectorAll('button')].filter((b) => !isTab(b))
|
|
1254
|
+
const byCurrent = buttons.find((b) => b.getAttribute('aria-current') === 'true')
|
|
1255
|
+
const byClass = buttons.find((b) => /_navCell/.test(b.className || '') && /_active/.test(b.className || ''))
|
|
1256
|
+
const hit = byCurrent ?? byClass
|
|
1257
|
+
return (hit?.textContent ?? '').replace(/\s+/g, ' ').trim()
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
let warmed = false
|
|
1261
|
+
/**
|
|
1262
|
+
* One-shot background pass: visit every first-level settings section once,
|
|
1263
|
+
* let each mount, harvest its options, then restore the previously-active one.
|
|
1264
|
+
*/
|
|
1265
|
+
const warmAllSections = async (model, onStart, onDone) => {
|
|
1266
|
+
if (warmed || typeof document === 'undefined') return true
|
|
1267
|
+
const sections = model.readSections()
|
|
1268
|
+
if (sections.length === 0) return true
|
|
1269
|
+
const original = activeSectionLabel()
|
|
1270
|
+
if (original === '') return false // settings panel not open yet; retry on next mount
|
|
1271
|
+
warmed = true
|
|
1272
|
+
if (typeof onStart === 'function') onStart()
|
|
1273
|
+
let visited = 0
|
|
1274
|
+
try {
|
|
1275
|
+
for (const sec of sections.slice(0, 40)) {
|
|
1276
|
+
if (!clickNavButton(sec.label)) continue
|
|
1277
|
+
await delay(180)
|
|
1278
|
+
model.runHarvest()
|
|
1279
|
+
visited += 1
|
|
1280
|
+
}
|
|
1281
|
+
if (clickNavButton(original)) {
|
|
1282
|
+
await delay(160)
|
|
1283
|
+
model.runHarvest()
|
|
1284
|
+
}
|
|
1285
|
+
} finally {
|
|
1286
|
+
if (typeof onDone === 'function') onDone()
|
|
1287
|
+
}
|
|
1288
|
+
log('info', '预载设置索引', { total: sections.length, visited })
|
|
1289
|
+
return true
|
|
1290
|
+
}
|
|
706
1291
|
|
|
707
1292
|
function createNavigator(ctx) {
|
|
708
1293
|
/** Jump to a page result; falls back to showing a manual hint. */
|
|
709
|
-
const jumpToPage = async (item) =>
|
|
710
|
-
const ok = await Promise.resolve(clickNavButton(item.label))
|
|
711
|
-
return ok
|
|
712
|
-
}
|
|
1294
|
+
const jumpToPage = async (item) => clickNavButton(item.label)
|
|
713
1295
|
/** Jump to an in-page option: open its page, select its tab, flash its row. */
|
|
714
1296
|
const jumpToOption = async (item) => {
|
|
715
|
-
const opened =
|
|
1297
|
+
const opened = clickNavButton(item.pageLabel ?? item.parentLabel)
|
|
716
1298
|
if (opened !== true) { flashOptionRow(item); return false }
|
|
717
1299
|
await delay(160)
|
|
718
1300
|
const label = item.label
|
|
@@ -727,21 +1309,34 @@ window.__ModuleLoader__.load({
|
|
|
727
1309
|
const flashOptionRow = (item) => {
|
|
728
1310
|
try {
|
|
729
1311
|
let target = null
|
|
1312
|
+
let card = null
|
|
730
1313
|
if (typeof item.rowIndex === 'number') {
|
|
731
|
-
// wrapper shape: pick the exact rendered row by index
|
|
732
1314
|
const container = document.querySelector(`[data-slot="${item.slot}"]`)
|
|
733
|
-
|
|
734
|
-
if (
|
|
735
|
-
// index drift — fall through to textual match below
|
|
736
|
-
target = null
|
|
737
|
-
}
|
|
1315
|
+
card = container?.children?.[item.rowIndex] ?? null
|
|
1316
|
+
if (card && !(card.textContent ?? '').includes(item.label)) card = null
|
|
738
1317
|
}
|
|
739
|
-
if (!
|
|
1318
|
+
if (!card) {
|
|
740
1319
|
const nodes = [...document.querySelectorAll(`[data-slot="${item.slot}"]`)]
|
|
741
1320
|
const rows = nodes.length === 1 && nodes[0].childElementCount > 0
|
|
742
1321
|
? [...nodes[0].children]
|
|
743
1322
|
: nodes
|
|
744
|
-
|
|
1323
|
+
card = rows.find((n) => (n.textContent ?? '').includes(item.label)) ?? null
|
|
1324
|
+
}
|
|
1325
|
+
if (card) {
|
|
1326
|
+
if (item.inner) {
|
|
1327
|
+
let best = null
|
|
1328
|
+
let bestLen = Infinity
|
|
1329
|
+
for (const el of card.querySelectorAll('*')) {
|
|
1330
|
+
const t = (el.textContent ?? '')
|
|
1331
|
+
if (t.includes(item.label) && t.length < bestLen) {
|
|
1332
|
+
best = el
|
|
1333
|
+
bestLen = t.length
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
target = best ?? card
|
|
1337
|
+
} else {
|
|
1338
|
+
target = card
|
|
1339
|
+
}
|
|
745
1340
|
}
|
|
746
1341
|
if (!target) return
|
|
747
1342
|
target.scrollIntoView({ block: 'nearest' })
|
|
@@ -767,6 +1362,36 @@ window.__ModuleLoader__.load({
|
|
|
767
1362
|
const model = createModel(ctx)
|
|
768
1363
|
const navigator = createNavigator(ctx)
|
|
769
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
|
+
|
|
770
1395
|
const SearchIcon = React.createElement('svg', {
|
|
771
1396
|
className: 'sss-icon',
|
|
772
1397
|
viewBox: '0 0 16.5 16.5',
|
|
@@ -785,10 +1410,76 @@ window.__ModuleLoader__.load({
|
|
|
785
1410
|
}),
|
|
786
1411
|
])
|
|
787
1412
|
|
|
1413
|
+
/** Feature cards shown in the first-run welcome modal. */
|
|
1414
|
+
const WELCOME_FEATURES = [
|
|
1415
|
+
{ icon: '🔍', titleKey: 'wlFeature1Title', descKey: 'wlFeature1Desc' },
|
|
1416
|
+
{ icon: '🔤', titleKey: 'wlFeature2Title', descKey: 'wlFeature2Desc' },
|
|
1417
|
+
{ icon: '🤖', titleKey: 'wlFeature3Title', descKey: 'wlFeature3Desc' },
|
|
1418
|
+
{ icon: '🧭', titleKey: 'wlFeature4Title', descKey: 'wlFeature4Desc' },
|
|
1419
|
+
{ icon: '🧾', titleKey: 'wlFeature5Title', descKey: 'wlFeature5Desc' },
|
|
1420
|
+
{ icon: '⚡', titleKey: 'wlFeature6Title', descKey: 'wlFeature6Desc' },
|
|
1421
|
+
]
|
|
1422
|
+
|
|
1423
|
+
/** First-run onboarding modal rendered as a fixed overlay. */
|
|
1424
|
+
function Welcome({ t, onClose, onOpenPanel }) {
|
|
1425
|
+
return React.createElement('div', {
|
|
1426
|
+
className: 'sss-wl-overlay',
|
|
1427
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) onClose() },
|
|
1428
|
+
}, [
|
|
1429
|
+
React.createElement('div', { className: 'sss-wl', role: 'dialog', 'aria-modal': 'true' }, [
|
|
1430
|
+
React.createElement('div', { className: 'sss-wl-header' }, [
|
|
1431
|
+
React.createElement('div', { className: 'sss-wl-headText' }, [
|
|
1432
|
+
React.createElement('h2', { className: 'sss-wl-title' }, t('wlTitle')),
|
|
1433
|
+
React.createElement('p', { className: 'sss-wl-subtitle' }, t('wlSubtitle')),
|
|
1434
|
+
]),
|
|
1435
|
+
React.createElement('button', { className: 'sss-wl-close', type: 'button', 'aria-label': 'Close', onClick: onClose }, '✕'),
|
|
1436
|
+
]),
|
|
1437
|
+
React.createElement('ul', { className: 'sss-wl-features' },
|
|
1438
|
+
WELCOME_FEATURES.map((f) => React.createElement('li', { className: 'sss-wl-feature', key: f.titleKey }, [
|
|
1439
|
+
React.createElement('span', { className: 'sss-wl-feature-icon', 'aria-hidden': 'true' }, f.icon),
|
|
1440
|
+
React.createElement('span', { className: 'sss-wl-feature-body' }, [
|
|
1441
|
+
React.createElement('span', { className: 'sss-wl-feature-title' }, t(f.titleKey)),
|
|
1442
|
+
React.createElement('p', { className: 'sss-wl-feature-desc' }, t(f.descKey)),
|
|
1443
|
+
]),
|
|
1444
|
+
]))),
|
|
1445
|
+
React.createElement('div', { className: 'sss-wl-footer' }, [
|
|
1446
|
+
React.createElement('button', { className: 'sss-wl-btn sss-wl-primary', type: 'button', onClick: onClose }, t('wlStart')),
|
|
1447
|
+
React.createElement('button', { className: 'sss-wl-btn', type: 'button', onClick: onOpenPanel }, t('wlOpenPanel')),
|
|
1448
|
+
]),
|
|
1449
|
+
]),
|
|
1450
|
+
])
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/** Destructive-action confirmation modal (danger-styled confirm button). */
|
|
1454
|
+
function ConfirmModal({ t, title, desc, confirmLabel, cancelLabel, onConfirm, onCancel }) {
|
|
1455
|
+
return React.createElement('div', {
|
|
1456
|
+
className: 'sss-cf-overlay',
|
|
1457
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) onCancel() },
|
|
1458
|
+
}, [
|
|
1459
|
+
React.createElement('div', { className: 'sss-cf', role: 'dialog', 'aria-modal': 'true' }, [
|
|
1460
|
+
React.createElement('div', { className: 'sss-cf-header' }, [
|
|
1461
|
+
React.createElement('div', { className: 'sss-cf-headText' }, [
|
|
1462
|
+
React.createElement('h2', { className: 'sss-cf-title' }, title),
|
|
1463
|
+
React.createElement('p', { className: 'sss-cf-desc' }, desc),
|
|
1464
|
+
]),
|
|
1465
|
+
React.createElement('button', { className: 'sss-cf-close', type: 'button', 'aria-label': 'Close', onClick: onCancel }, '✕'),
|
|
1466
|
+
]),
|
|
1467
|
+
React.createElement('div', { className: 'sss-cf-actions' }, [
|
|
1468
|
+
React.createElement('button', { className: 'sss-cf-btn sss-cf-danger', type: 'button', onClick: onConfirm }, confirmLabel),
|
|
1469
|
+
React.createElement('button', { className: 'sss-cf-btn', type: 'button', onClick: onCancel }, cancelLabel),
|
|
1470
|
+
]),
|
|
1471
|
+
]),
|
|
1472
|
+
])
|
|
1473
|
+
}
|
|
1474
|
+
|
|
788
1475
|
function SettingsHeaderSearch() {
|
|
789
1476
|
const [query, setQuery] = React.useState('')
|
|
790
1477
|
const [open, setOpen] = React.useState(false)
|
|
1478
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
1479
|
+
const [pageOpen, setPageOpen] = React.useState(false)
|
|
1480
|
+
const [showWelcome, setShowWelcome] = React.useState(() => !welcomed())
|
|
791
1481
|
const [tick, setTick] = React.useState(0)
|
|
1482
|
+
const [warming, setWarming] = React.useState(false)
|
|
792
1483
|
const [, forceTick] = React.useReducer((x) => x + 1, 0)
|
|
793
1484
|
void tick
|
|
794
1485
|
const [aiEnabled, setAiEnabled] = React.useState(aiSearchEnabled)
|
|
@@ -803,20 +1494,67 @@ window.__ModuleLoader__.load({
|
|
|
803
1494
|
ctx.locale.subscribe(forceTick),
|
|
804
1495
|
]
|
|
805
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).
|
|
1499
|
+
let warmAlive = true
|
|
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
|
+
}
|
|
806
1513
|
const observer = typeof MutationObserver === 'function'
|
|
807
1514
|
? new MutationObserver(() => {
|
|
808
|
-
if (
|
|
1515
|
+
if (throttleTimer !== null) return
|
|
1516
|
+
throttleTimer = setTimeout(() => {
|
|
1517
|
+
throttleTimer = null
|
|
1518
|
+
onMutate()
|
|
1519
|
+
}, 120)
|
|
809
1520
|
})
|
|
810
1521
|
: null
|
|
811
1522
|
observer?.observe(document.body, { childList: true, subtree: true })
|
|
812
|
-
const
|
|
1523
|
+
const tryWarm = () => {
|
|
1524
|
+
void warmAllSections(model, () => setWarming(true), () => setWarming(false)).then((done) => {
|
|
1525
|
+
if (!warmAlive) return
|
|
1526
|
+
if (done) { setTick((x) => x + 1); return }
|
|
1527
|
+
warmAttempts += 1
|
|
1528
|
+
if (warmAttempts < 4) setTimeout(tryWarm, 700)
|
|
1529
|
+
})
|
|
1530
|
+
}
|
|
1531
|
+
const warm = setTimeout(() => {
|
|
1532
|
+
model.runHarvest()
|
|
1533
|
+
setTick((x) => x + 1)
|
|
1534
|
+
tryWarm()
|
|
1535
|
+
}, 800)
|
|
813
1536
|
return () => {
|
|
1537
|
+
warmAlive = false
|
|
1538
|
+
mutatorAlive = false
|
|
814
1539
|
for (const off of offs) off()
|
|
815
1540
|
observer?.disconnect()
|
|
816
1541
|
clearTimeout(warm)
|
|
1542
|
+
if (throttleTimer !== null) clearTimeout(throttleTimer)
|
|
817
1543
|
}
|
|
818
1544
|
}, [])
|
|
819
1545
|
|
|
1546
|
+
React.useEffect(() => {
|
|
1547
|
+
const onWelcome = (event) => { if (event?.detail === true) setShowWelcome(true) }
|
|
1548
|
+
window.addEventListener(WELCOME_EVENT, onWelcome)
|
|
1549
|
+
return () => window.removeEventListener(WELCOME_EVENT, onWelcome)
|
|
1550
|
+
}, [])
|
|
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
|
+
|
|
820
1558
|
React.useEffect(() => {
|
|
821
1559
|
const sync = () => setAiEnabled(aiSearchEnabled())
|
|
822
1560
|
const onConfig = () => forceTick()
|
|
@@ -839,7 +1577,7 @@ window.__ModuleLoader__.load({
|
|
|
839
1577
|
tokens.every((tok) => tokenMatch(haystackLower, item.keys, tok) !== null)
|
|
840
1578
|
const kwMatch = (item, haystackLower, kw) => {
|
|
841
1579
|
if (/^[\x00-\x7f]+$/.test(kw)) {
|
|
842
|
-
if (
|
|
1580
|
+
if (wordBoundaryRegex(kw).test(haystackLower)) return true
|
|
843
1581
|
} else if (haystackLower.includes(kw)) return true
|
|
844
1582
|
if (item.keys === null) return false
|
|
845
1583
|
const kwKeys = pinyinKeys(kw)
|
|
@@ -930,15 +1668,25 @@ window.__ModuleLoader__.load({
|
|
|
930
1668
|
const [hint, setHint] = React.useState(null)
|
|
931
1669
|
const [active, setActive] = React.useState(0)
|
|
932
1670
|
const popRef = React.useRef(null)
|
|
1671
|
+
const inputRef = React.useRef(null)
|
|
1672
|
+
const pageMode = searchStyle === 'page'
|
|
1673
|
+
const mixedMode = searchStyle === 'mixed'
|
|
1674
|
+
const topMode = searchStyle === 'top' || mixedMode
|
|
933
1675
|
const activeIndex = Math.min(active, Math.max(results.length - 1, 0))
|
|
934
1676
|
|
|
935
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])
|
|
936
1683
|
React.useEffect(() => {
|
|
937
1684
|
const el = popRef.current?.querySelector('.sss-item.is-active')
|
|
938
1685
|
el?.scrollIntoView?.({ block: 'nearest' })
|
|
939
1686
|
}, [activeIndex, q])
|
|
940
1687
|
const pick = (item) => {
|
|
941
1688
|
setOpen(false)
|
|
1689
|
+
setPageOpen(false)
|
|
942
1690
|
log('info', '打开设置', { kind: item.kind, id: item.id || item.key || '', label: String(item.label).slice(0, 60) })
|
|
943
1691
|
const jumped = item.kind === 'page'
|
|
944
1692
|
? navigator.jumpToPage(item)
|
|
@@ -948,6 +1696,15 @@ window.__ModuleLoader__.load({
|
|
|
948
1696
|
})
|
|
949
1697
|
}
|
|
950
1698
|
|
|
1699
|
+
const dismissWelcome = () => {
|
|
1700
|
+
setWelcomed()
|
|
1701
|
+
setShowWelcome(false)
|
|
1702
|
+
}
|
|
1703
|
+
const openPanelFromWelcome = () => {
|
|
1704
|
+
dismissWelcome()
|
|
1705
|
+
clickNavButton(t('updn'))
|
|
1706
|
+
}
|
|
1707
|
+
|
|
951
1708
|
const mark = (text) => React.createElement('mark', { className: 'sss-hit' }, text)
|
|
952
1709
|
|
|
953
1710
|
/** Highlight the matched substring, or the pinyin-covered prefix. */
|
|
@@ -1010,71 +1767,213 @@ window.__ModuleLoader__.load({
|
|
|
1010
1767
|
kindBadge(via, t('option')),
|
|
1011
1768
|
])
|
|
1012
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
|
+
|
|
1013
1956
|
return React.createElement('div', { className: 'sss-wrap' }, [
|
|
1014
1957
|
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
1015
1958
|
React.createElement('div', { className: 'sss-box' }, [
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
className: 'sss-input',
|
|
1019
|
-
value: query,
|
|
1020
|
-
placeholder: t('placeholder'),
|
|
1021
|
-
spellCheck: false,
|
|
1022
|
-
onKeyDown: (e) => {
|
|
1023
|
-
if (e.key === 'ArrowDown') {
|
|
1024
|
-
e.preventDefault()
|
|
1025
|
-
if (results.length > 0) setActive(Math.min(activeIndex + 1, results.length - 1))
|
|
1026
|
-
} else if (e.key === 'ArrowUp') {
|
|
1027
|
-
e.preventDefault()
|
|
1028
|
-
setActive(Math.max(activeIndex - 1, 0))
|
|
1029
|
-
} else if (e.key === 'Enter') {
|
|
1030
|
-
const item = results[activeIndex]
|
|
1031
|
-
if (item) { e.preventDefault(); pick(item) }
|
|
1032
|
-
} else if (e.key === 'Escape') {
|
|
1033
|
-
setOpen(false)
|
|
1034
|
-
}
|
|
1035
|
-
},
|
|
1036
|
-
onChange: (e) => { setQuery(e.target.value); setHint(null); setOpen(true) },
|
|
1037
|
-
onFocus: () => setOpen(true),
|
|
1038
|
-
onBlur: () => setOpen(false),
|
|
1039
|
-
}),
|
|
1040
|
-
query !== '' ? React.createElement('button', {
|
|
1041
|
-
className: 'sss-clear',
|
|
1959
|
+
React.createElement('button', {
|
|
1960
|
+
className: 'sss-icon-btn',
|
|
1042
1961
|
type: 'button',
|
|
1043
|
-
|
|
1044
|
-
|
|
1962
|
+
'aria-label': t('searchBtn'),
|
|
1963
|
+
title: t('searchBtn'),
|
|
1964
|
+
onClick: () => inputRef.current?.focus(),
|
|
1965
|
+
}, [SearchIcon]),
|
|
1966
|
+
searchInput,
|
|
1967
|
+
clearButton,
|
|
1045
1968
|
]),
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
? (aiStatus === 'loading'
|
|
1049
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiThinking'))
|
|
1050
|
-
: aiStatus === 'error'
|
|
1051
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiUnavailable'))
|
|
1052
|
-
: (aiEnabled && !aiConfigured)
|
|
1053
|
-
? React.createElement('p', { className: 'sss-ai-note' }, t('aiNeedConfig'))
|
|
1054
|
-
: React.createElement('p', { className: 'sss-empty' }, t('empty')))
|
|
1055
|
-
: React.createElement('div', { className: 'sss-groups' }, [
|
|
1056
|
-
shownPages.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1057
|
-
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
1058
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1059
|
-
shownPages.map((item, i) => renderPage(item, i))),
|
|
1060
|
-
]) : null,
|
|
1061
|
-
shownOptions.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1062
|
-
React.createElement('div', { className: 'sss-group-title' }, t('options')),
|
|
1063
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1064
|
-
shownOptions.map((item, i) => renderOption(item, shownPages.length + i))),
|
|
1065
|
-
]) : null,
|
|
1066
|
-
related.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
1067
|
-
React.createElement('div', { className: 'sss-group-title' }, t('guess')),
|
|
1068
|
-
React.createElement('ul', { className: 'sss-list' },
|
|
1069
|
-
related.map((r, i) => r.kind === 'page'
|
|
1070
|
-
? renderPage(r.item, shownPages.length + shownOptions.length + i, r.via)
|
|
1071
|
-
: renderOption(r.item, shownPages.length + shownOptions.length + i, r.via))),
|
|
1072
|
-
]) : null,
|
|
1073
|
-
]),
|
|
1074
|
-
]) : null,
|
|
1969
|
+
warming ? React.createElement('p', { className: 'sss-warm-note' }, t('warming')) : null,
|
|
1970
|
+
open && q !== '' ? React.createElement('div', { className: 'sss-pop', ref: popRef }, [resultsNode]) : null,
|
|
1075
1971
|
hint !== null
|
|
1076
1972
|
? React.createElement('div', { className: 'sss-guide' }, t('jumpFail').replace('{path}', hint))
|
|
1077
1973
|
: null,
|
|
1974
|
+
showWelcome
|
|
1975
|
+
? React.createElement(Welcome, { t, onClose: dismissWelcome, onOpenPanel: openPanelFromWelcome })
|
|
1976
|
+
: null,
|
|
1078
1977
|
])
|
|
1079
1978
|
}
|
|
1080
1979
|
|
|
@@ -1087,8 +1986,18 @@ window.__ModuleLoader__.load({
|
|
|
1087
1986
|
return 0
|
|
1088
1987
|
}
|
|
1089
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
|
+
|
|
1090
1998
|
function UpdatePanel() {
|
|
1091
1999
|
const [open, setOpen] = React.useState(true)
|
|
2000
|
+
const [searchStyle, setSearchStyleState] = React.useState(readSearchStyle)
|
|
1092
2001
|
const [state, setState] = React.useState('idle') // idle | loading | newest | outdated | error
|
|
1093
2002
|
const [latest, setLatest] = React.useState('')
|
|
1094
2003
|
const [copied, setCopied] = React.useState(false)
|
|
@@ -1102,11 +2011,17 @@ window.__ModuleLoader__.load({
|
|
|
1102
2011
|
const [model, setModel] = React.useState(cfg.model)
|
|
1103
2012
|
const [apiKey, setApiKey] = React.useState(cfg.apiKey)
|
|
1104
2013
|
const [aiSaved, setAiSaved] = React.useState(false)
|
|
2014
|
+
const [testState, setTestState] = React.useState('idle') // idle | loading | ok | fail
|
|
2015
|
+
const [testMsg, setTestMsg] = React.useState('')
|
|
2016
|
+
const [dshImportState, setDshImportState] = React.useState('idle') // idle | loading | ok | fail
|
|
2017
|
+
const [dshImportMsg, setDshImportMsg] = React.useState('')
|
|
1105
2018
|
const [, setLogVersion] = React.useState(0)
|
|
1106
2019
|
const [logCopied, setLogCopied] = React.useState(false)
|
|
1107
2020
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
1108
2021
|
const [logExportFail, setLogExportFail] = React.useState(false)
|
|
1109
2022
|
const [logCleared, setLogCleared] = React.useState(false)
|
|
2023
|
+
const [confirmClear, setConfirmClear] = React.useState(false)
|
|
2024
|
+
const [confirmClearAi, setConfirmClearAi] = React.useState(false)
|
|
1110
2025
|
|
|
1111
2026
|
React.useEffect(() => {
|
|
1112
2027
|
const sync = () => setAiOn(aiSearchEnabled())
|
|
@@ -1115,16 +2030,13 @@ window.__ModuleLoader__.load({
|
|
|
1115
2030
|
}, [])
|
|
1116
2031
|
|
|
1117
2032
|
React.useEffect(() => {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
return () =>
|
|
1121
|
-
}, [
|
|
2033
|
+
const sync = () => setSearchStyleState(readSearchStyle())
|
|
2034
|
+
window.addEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2035
|
+
return () => window.removeEventListener(SEARCH_STYLE_EVENT, sync)
|
|
2036
|
+
}, [])
|
|
1122
2037
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const off = setTimeout(() => setAiSaved(false), 1600)
|
|
1126
|
-
return () => clearTimeout(off)
|
|
1127
|
-
}, [aiSaved])
|
|
2038
|
+
useAutoReset(copied, () => { setCopied(false); setCopyFailed(false) })
|
|
2039
|
+
useAutoReset(aiSaved, () => setAiSaved(false))
|
|
1128
2040
|
|
|
1129
2041
|
React.useEffect(() => {
|
|
1130
2042
|
const sync = () => setLogVersion((v) => v + 1)
|
|
@@ -1132,23 +2044,9 @@ window.__ModuleLoader__.load({
|
|
|
1132
2044
|
return () => window.removeEventListener(LOG_EVENT, sync)
|
|
1133
2045
|
}, [])
|
|
1134
2046
|
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
return () => clearTimeout(off)
|
|
1139
|
-
}, [logCopied])
|
|
1140
|
-
|
|
1141
|
-
React.useEffect(() => {
|
|
1142
|
-
if (!logCleared) return
|
|
1143
|
-
const off = setTimeout(() => setLogCleared(false), 1600)
|
|
1144
|
-
return () => clearTimeout(off)
|
|
1145
|
-
}, [logCleared])
|
|
1146
|
-
|
|
1147
|
-
React.useEffect(() => {
|
|
1148
|
-
if (!logExportFail) return
|
|
1149
|
-
const off = setTimeout(() => setLogExportFail(false), 1600)
|
|
1150
|
-
return () => clearTimeout(off)
|
|
1151
|
-
}, [logExportFail])
|
|
2047
|
+
useAutoReset(logCopied, () => setLogCopied(false))
|
|
2048
|
+
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2049
|
+
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
1152
2050
|
|
|
1153
2051
|
const saveAi = () => {
|
|
1154
2052
|
setAiConfig({
|
|
@@ -1161,6 +2059,79 @@ window.__ModuleLoader__.load({
|
|
|
1161
2059
|
log('info', '保存 AI 配置', { provider, model: model.trim() })
|
|
1162
2060
|
}
|
|
1163
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
|
+
|
|
2089
|
+
const requestClearAi = () => setConfirmClearAi(true)
|
|
2090
|
+
const cancelClearAi = () => setConfirmClearAi(false)
|
|
2091
|
+
const clearAiConfig = () => {
|
|
2092
|
+
setConfirmClearAi(false)
|
|
2093
|
+
setAiConfig({ provider: 'openai', baseUrl: '', model: '', apiKey: '' })
|
|
2094
|
+
setProvider('openai')
|
|
2095
|
+
setBaseUrl('')
|
|
2096
|
+
setModel('')
|
|
2097
|
+
setApiKey('')
|
|
2098
|
+
setAiSaved(false)
|
|
2099
|
+
log('info', '清除 API 配置', {})
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
const testService = async () => {
|
|
2103
|
+
if (testState === 'loading') return
|
|
2104
|
+
if (!baseUrl.trim() || !model.trim() || !apiKey.trim()) {
|
|
2105
|
+
setTestState('fail')
|
|
2106
|
+
setTestMsg(t('aiNeedConfig'))
|
|
2107
|
+
return
|
|
2108
|
+
}
|
|
2109
|
+
setTestState('loading')
|
|
2110
|
+
setTestMsg('')
|
|
2111
|
+
const result = await testAiService({ provider, baseUrl, model, apiKey })
|
|
2112
|
+
if (result.ok) {
|
|
2113
|
+
setTestState('ok')
|
|
2114
|
+
setTestMsg(t('aiTestOk'))
|
|
2115
|
+
} else {
|
|
2116
|
+
setTestState('fail')
|
|
2117
|
+
const hintKey = {
|
|
2118
|
+
auth: 'aiTestErrAuth',
|
|
2119
|
+
forbidden: 'aiTestErrForbidden',
|
|
2120
|
+
'not-found': 'aiTestErrNotFound',
|
|
2121
|
+
'rate-limit': 'aiTestErrRateLimit',
|
|
2122
|
+
'bad-request': 'aiTestErrBadRequest',
|
|
2123
|
+
server: 'aiTestErrServer',
|
|
2124
|
+
timeout: 'aiTestErrTimeout',
|
|
2125
|
+
network: 'aiTestErrNetwork',
|
|
2126
|
+
'bad-response': 'aiTestErrBadResponse',
|
|
2127
|
+
'unsafe-url': 'aiTestErrUnsafeUrl',
|
|
2128
|
+
}[result.code] ?? 'aiTestErrHttp'
|
|
2129
|
+
const hint = t(hintKey)
|
|
2130
|
+
const detail = result.detail ? `(${result.detail})` : ''
|
|
2131
|
+
setTestMsg(`${t('aiTestFail')}:${hint}${detail}`)
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
|
|
1164
2135
|
const exportLogs = () => {
|
|
1165
2136
|
setLogExportFail(false)
|
|
1166
2137
|
try {
|
|
@@ -1188,7 +2159,10 @@ window.__ModuleLoader__.load({
|
|
|
1188
2159
|
}
|
|
1189
2160
|
}
|
|
1190
2161
|
|
|
2162
|
+
const requestClear = () => setConfirmClear(true)
|
|
2163
|
+
const cancelClear = () => setConfirmClear(false)
|
|
1191
2164
|
const clearLogs = () => {
|
|
2165
|
+
setConfirmClear(false)
|
|
1192
2166
|
LOGS.length = 0
|
|
1193
2167
|
setLogVersion((v) => v + 1)
|
|
1194
2168
|
setLogCleared(true)
|
|
@@ -1204,6 +2178,11 @@ window.__ModuleLoader__.load({
|
|
|
1204
2178
|
const data = await res.json()
|
|
1205
2179
|
const ver = typeof data?.version === 'string' ? data.version : ''
|
|
1206
2180
|
if (ver === '') throw new Error('no version in registry response')
|
|
2181
|
+
// Only accept a plain semver string so it can never be interpreted
|
|
2182
|
+
// as shell syntax or injected into a URL opened via location.href.
|
|
2183
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(ver)) {
|
|
2184
|
+
throw new Error('invalid semver in registry response')
|
|
2185
|
+
}
|
|
1207
2186
|
setLatest(ver)
|
|
1208
2187
|
const newer = compareSemver(ver, PKG_VERSION) > 0
|
|
1209
2188
|
setState(newer ? 'outdated' : 'newest')
|
|
@@ -1268,6 +2247,34 @@ window.__ModuleLoader__.load({
|
|
|
1268
2247
|
React.createElement('span', { className: 'sss-vr-chevron' + (open ? ' sss-vr-chevron-open' : '') }, '▾'),
|
|
1269
2248
|
]),
|
|
1270
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
|
+
]),
|
|
1271
2278
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
1272
2279
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1273
2280
|
React.createElement('span', { className: 'sss-vr-label' }, t('aiSearch')),
|
|
@@ -1291,7 +2298,11 @@ window.__ModuleLoader__.load({
|
|
|
1291
2298
|
]),
|
|
1292
2299
|
React.createElement('select', {
|
|
1293
2300
|
className: 'sss-vr-input sss-vr-select',
|
|
2301
|
+
name: 'dsh-settings-search-provider',
|
|
1294
2302
|
value: provider,
|
|
2303
|
+
autoComplete: 'off',
|
|
2304
|
+
'data-lpignore': 'true',
|
|
2305
|
+
'data-form-type': 'other',
|
|
1295
2306
|
onChange: (event) => setProvider(event.target.value),
|
|
1296
2307
|
}, [
|
|
1297
2308
|
React.createElement('option', { value: 'openai' }, t('aiOpenAI')),
|
|
@@ -1303,37 +2314,64 @@ window.__ModuleLoader__.load({
|
|
|
1303
2314
|
React.createElement('input', {
|
|
1304
2315
|
className: 'sss-vr-input',
|
|
1305
2316
|
type: 'url',
|
|
2317
|
+
name: 'dsh-settings-search-base-url',
|
|
1306
2318
|
value: baseUrl,
|
|
1307
2319
|
placeholder: provider === 'anthropic' ? 'https://api.anthropic.com' : 'https://api.openai.com/v1',
|
|
1308
2320
|
spellCheck: false,
|
|
2321
|
+
autoComplete: 'off',
|
|
2322
|
+
'data-lpignore': 'true',
|
|
2323
|
+
'data-1p-ignore': 'true',
|
|
2324
|
+
'data-form-type': 'other',
|
|
1309
2325
|
onChange: (event) => setBaseUrl(event.target.value),
|
|
1310
2326
|
}),
|
|
2327
|
+
React.createElement('p', { className: 'sss-vr-hint' }, t('aiBaseUrlHint')),
|
|
1311
2328
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1312
2329
|
React.createElement('label', { className: 'sss-vr-label' }, t('aiModel')),
|
|
1313
2330
|
]),
|
|
1314
2331
|
React.createElement('input', {
|
|
1315
2332
|
className: 'sss-vr-input',
|
|
1316
2333
|
type: 'text',
|
|
2334
|
+
name: 'dsh-settings-search-model',
|
|
1317
2335
|
value: model,
|
|
1318
2336
|
placeholder: provider === 'anthropic' ? 'claude-sonnet-4-20250514' : 'gpt-4o-mini',
|
|
1319
2337
|
spellCheck: false,
|
|
2338
|
+
autoComplete: 'off',
|
|
2339
|
+
'data-lpignore': 'true',
|
|
2340
|
+
'data-1p-ignore': 'true',
|
|
2341
|
+
'data-form-type': 'other',
|
|
1320
2342
|
onChange: (event) => setModel(event.target.value),
|
|
1321
2343
|
}),
|
|
1322
2344
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
1323
2345
|
React.createElement('label', { className: 'sss-vr-label' }, t('aiApiKey')),
|
|
1324
2346
|
]),
|
|
1325
2347
|
React.createElement('input', {
|
|
1326
|
-
className: 'sss-vr-input',
|
|
1327
|
-
type: '
|
|
2348
|
+
className: 'sss-vr-input sss-vr-mask',
|
|
2349
|
+
type: 'text',
|
|
2350
|
+
name: 'dsh-settings-search-api-key',
|
|
1328
2351
|
value: apiKey,
|
|
1329
2352
|
placeholder: 'sk-…',
|
|
1330
2353
|
spellCheck: false,
|
|
1331
2354
|
autoComplete: 'off',
|
|
2355
|
+
'data-lpignore': 'true',
|
|
2356
|
+
'data-1p-ignore': 'true',
|
|
2357
|
+
'data-form-type': 'other',
|
|
1332
2358
|
onChange: (event) => setApiKey(event.target.value),
|
|
1333
2359
|
}),
|
|
1334
|
-
React.createElement('div', { className: 'sss-vr-actions' },
|
|
2360
|
+
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
1335
2361
|
React.createElement('button', { className: 'sss-vr-btn sss-vr-primary', type: 'button', onClick: () => { void saveAi() } },
|
|
1336
|
-
aiSaved ? t('aiConfigSaved') : t('aiConfigSave'))
|
|
2362
|
+
aiSaved ? t('aiConfigSaved') : t('aiConfigSave')),
|
|
2363
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: testState === 'loading', onClick: () => { void testService() } },
|
|
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')),
|
|
2367
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => requestClearAi() }, t('aiClear')),
|
|
2368
|
+
]),
|
|
2369
|
+
dshImportMsg
|
|
2370
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, dshImportMsg)
|
|
2371
|
+
: null,
|
|
2372
|
+
testMsg
|
|
2373
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? '#1e8e3e' : 'var(--dsw-alias-label-danger)' } }, testMsg)
|
|
2374
|
+
: null,
|
|
1337
2375
|
]),
|
|
1338
2376
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
1339
2377
|
React.createElement('div', { className: 'sss-vr-row' }, [
|
|
@@ -1384,13 +2422,35 @@ window.__ModuleLoader__.load({
|
|
|
1384
2422
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void exportLogs() } }, t('logExport')),
|
|
1385
2423
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void copyLogs() } },
|
|
1386
2424
|
logCopied ? t('logCopied') : t('logCopy')),
|
|
1387
|
-
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () =>
|
|
2425
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: LOGS.length === 0, onClick: () => requestClear() },
|
|
1388
2426
|
logCleared ? t('logCleared') : t('logClear')),
|
|
1389
2427
|
]),
|
|
1390
2428
|
logCopyFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-label-danger)' } }, t('logCopyFail')) : null,
|
|
1391
2429
|
logExportFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-label-danger)' } }, t('logExportFail')) : null,
|
|
1392
2430
|
]),
|
|
1393
2431
|
]) : null,
|
|
2432
|
+
confirmClear
|
|
2433
|
+
? React.createElement(ConfirmModal, {
|
|
2434
|
+
t,
|
|
2435
|
+
title: t('logConfirmTitle'),
|
|
2436
|
+
desc: t('logConfirm'),
|
|
2437
|
+
confirmLabel: t('logConfirmYes'),
|
|
2438
|
+
cancelLabel: t('logConfirmCancel'),
|
|
2439
|
+
onConfirm: () => { void clearLogs() },
|
|
2440
|
+
onCancel: () => cancelClear(),
|
|
2441
|
+
})
|
|
2442
|
+
: null,
|
|
2443
|
+
confirmClearAi
|
|
2444
|
+
? React.createElement(ConfirmModal, {
|
|
2445
|
+
t,
|
|
2446
|
+
title: t('aiClearTitle'),
|
|
2447
|
+
desc: t('aiClearConfirm'),
|
|
2448
|
+
confirmLabel: t('aiClearYes'),
|
|
2449
|
+
cancelLabel: t('logConfirmCancel'),
|
|
2450
|
+
onConfirm: () => { void clearAiConfig() },
|
|
2451
|
+
onCancel: () => cancelClearAi(),
|
|
2452
|
+
})
|
|
2453
|
+
: null,
|
|
1394
2454
|
])
|
|
1395
2455
|
}
|
|
1396
2456
|
|
|
@@ -1410,7 +2470,7 @@ window.__ModuleLoader__.load({
|
|
|
1410
2470
|
}
|
|
1411
2471
|
|
|
1412
2472
|
exports.apply = apply
|
|
1413
|
-
exports.inject = ['slots', 'locale']
|
|
2473
|
+
exports.inject = ['slots', 'locale', 'connection']
|
|
1414
2474
|
return module.exports
|
|
1415
2475
|
},
|
|
1416
2476
|
})
|