@objectivex666/dsh-settings-search 1.12.0 → 1.13.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 -5
- package/README.md +6 -5
- package/lib/client.js +554 -85
- package/lib/index.js +448 -11
- package/package.json +5 -4
- package/scripts/ai.test.mjs +273 -2
- package/scripts/check.mjs +28 -0
- package/scripts/log.test.mjs +345 -0
package/lib/client.js
CHANGED
|
@@ -30,7 +30,7 @@ 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.13.0'
|
|
34
34
|
const NPM_NAME = '@objectivex666/dsh-settings-search'
|
|
35
35
|
const REPO_URL = 'https://github.com/objectivex666/dsh-settings-search'
|
|
36
36
|
const ISSUES_URL = 'https://github.com/objectivex666/dsh-settings-search/issues/new/choose'
|
|
@@ -107,6 +107,8 @@ window.__ModuleLoader__.load({
|
|
|
107
107
|
aiDshImportOk: '已导入 Base URL 与模型名;API Key 未改动 ✓',
|
|
108
108
|
aiDshImportFail: '导入失败:{reason}',
|
|
109
109
|
aiDshImportEmpty: '未找到 DSH 当前默认模型配置',
|
|
110
|
+
aiDshImportApi: '当前 DSH 版本未提供模型配置接口(remote.settings)',
|
|
111
|
+
aiDshImportNoBaseUrl: '默认模型「{model}」在 DSH 设置中没有可导入的 Base URL',
|
|
110
112
|
aiConfigSave: '保存配置',
|
|
111
113
|
aiConfigSaved: '已保存 ✓',
|
|
112
114
|
aiClear: '清除配置',
|
|
@@ -132,9 +134,14 @@ window.__ModuleLoader__.load({
|
|
|
132
134
|
aiOllamaErrForbidden: 'Ollama 拒绝访问,请检查 OLLAMA_ORIGINS 是否允许当前 DSH 页面来源',
|
|
133
135
|
aiOllamaErrTimeout: 'Ollama 响应超时,请检查本机服务及模型加载状态',
|
|
134
136
|
logTitle: '日志',
|
|
135
|
-
|
|
137
|
+
logCountHint: '磁盘日志文件中的条目数',
|
|
138
|
+
logDesc: '记录插件运行、搜索与 AI 请求的关键动作,便于排查问题;日志按天写入 DSH 的日志文件夹,文件名带日期。',
|
|
136
139
|
logEmpty: '暂无日志',
|
|
137
140
|
logExport: '导出日志',
|
|
141
|
+
logView: '查看日志',
|
|
142
|
+
logOpenDir: '打开日志文件夹',
|
|
143
|
+
logOpenOk: '已在文件管理器中打开日志文件夹 ✓',
|
|
144
|
+
logOpenFail: '打开日志文件夹失败',
|
|
138
145
|
logIssues: '反馈问题',
|
|
139
146
|
logCopy: '复制日志',
|
|
140
147
|
logCopied: '已复制 ✓',
|
|
@@ -142,10 +149,20 @@ window.__ModuleLoader__.load({
|
|
|
142
149
|
logClear: '清空日志',
|
|
143
150
|
logCleared: '已清空',
|
|
144
151
|
logConfirmTitle: '清空日志',
|
|
145
|
-
logConfirm: '
|
|
152
|
+
logConfirm: '确定要清空全部日志吗?磁盘上的日志文件也会一并清空。',
|
|
146
153
|
logConfirmYes: '确认清空',
|
|
147
154
|
logConfirmCancel: '取消',
|
|
148
155
|
logExportFail: '导出失败,请重试。',
|
|
156
|
+
logViewerTitle: '运行日志',
|
|
157
|
+
logViewerRefresh: '刷新',
|
|
158
|
+
logViewerLoading: '正在读取…',
|
|
159
|
+
logViewerCopy: '复制',
|
|
160
|
+
logViewerCopied: '已复制 ✓',
|
|
161
|
+
logViewerClose: '关闭',
|
|
162
|
+
logViewerEmpty: '暂无日志。',
|
|
163
|
+
logViewerFile: '文件:{file}',
|
|
164
|
+
logViewerMemory: '宿主日志通道不可用,这里显示本次会话的记录,重启后不再保留。',
|
|
165
|
+
logViewerTruncated: '仅显示文件末尾部分,更早的内容请打开日志文件夹查看。',
|
|
149
166
|
wlTitle: '欢迎使用设置搜索',
|
|
150
167
|
wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
|
|
151
168
|
wlFeature1Title: '实时搜索',
|
|
@@ -236,6 +253,8 @@ window.__ModuleLoader__.load({
|
|
|
236
253
|
aiDshImportOk: 'Imported Base URL and model; API key unchanged ✓',
|
|
237
254
|
aiDshImportFail: 'Import failed: {reason}',
|
|
238
255
|
aiDshImportEmpty: 'No current DSH default model config found',
|
|
256
|
+
aiDshImportApi: 'This DSH build exposes no model config API (remote.settings)',
|
|
257
|
+
aiDshImportNoBaseUrl: 'The default model "{model}" has no Base URL in DSH settings to import',
|
|
239
258
|
aiConfigSave: 'Save config',
|
|
240
259
|
aiConfigSaved: 'Saved ✓',
|
|
241
260
|
aiClear: 'Clear config',
|
|
@@ -261,9 +280,14 @@ window.__ModuleLoader__.load({
|
|
|
261
280
|
aiOllamaErrForbidden: 'Ollama denied access. Check that OLLAMA_ORIGINS allows this DSH page origin',
|
|
262
281
|
aiOllamaErrTimeout: 'Ollama timed out. Check the local service and model loading status',
|
|
263
282
|
logTitle: 'Logs',
|
|
264
|
-
|
|
283
|
+
logCountHint: 'Entries in the log file on disk',
|
|
284
|
+
logDesc: 'Logs key plugin actions, searches, and AI requests to help troubleshoot; one dated file per day is written to DSH\u2019s log folder.',
|
|
265
285
|
logEmpty: 'No logs yet',
|
|
266
286
|
logExport: 'Export logs',
|
|
287
|
+
logView: 'View logs',
|
|
288
|
+
logOpenDir: 'Open log folder',
|
|
289
|
+
logOpenOk: 'Log folder opened in your file manager ✓',
|
|
290
|
+
logOpenFail: 'Could not open the log folder',
|
|
267
291
|
logIssues: 'Report issue',
|
|
268
292
|
logCopy: 'Copy logs',
|
|
269
293
|
logCopied: 'Copied ✓',
|
|
@@ -271,10 +295,20 @@ window.__ModuleLoader__.load({
|
|
|
271
295
|
logClear: 'Clear logs',
|
|
272
296
|
logCleared: 'Cleared',
|
|
273
297
|
logConfirmTitle: 'Clear logs',
|
|
274
|
-
logConfirm: 'Clear all logs?
|
|
298
|
+
logConfirm: 'Clear all logs? The log file on disk is truncated as well.',
|
|
275
299
|
logConfirmYes: 'Clear logs',
|
|
276
300
|
logConfirmCancel: 'Cancel',
|
|
277
301
|
logExportFail: 'Export failed. Please try again.',
|
|
302
|
+
logViewerTitle: 'Run log',
|
|
303
|
+
logViewerRefresh: 'Refresh',
|
|
304
|
+
logViewerLoading: 'Loading…',
|
|
305
|
+
logViewerCopy: 'Copy',
|
|
306
|
+
logViewerCopied: 'Copied ✓',
|
|
307
|
+
logViewerClose: 'Close',
|
|
308
|
+
logViewerEmpty: 'No logs yet.',
|
|
309
|
+
logViewerFile: 'File: {file}',
|
|
310
|
+
logViewerMemory: 'The host log channel is unavailable, so this shows the current session only; it is not kept after a reload.',
|
|
311
|
+
logViewerTruncated: 'Showing the end of the file only — open the log folder for earlier lines.',
|
|
278
312
|
wlTitle: 'Welcome to Settings Search',
|
|
279
313
|
wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
|
|
280
314
|
wlFeature1Title: 'Instant search',
|
|
@@ -293,7 +327,14 @@ window.__ModuleLoader__.load({
|
|
|
293
327
|
wlFeature6Desc: 'On first open it automatically passes through every settings section, so in-page options are searchable without opening each page manually.',
|
|
294
328
|
}
|
|
295
329
|
|
|
296
|
-
/**
|
|
330
|
+
/**
|
|
331
|
+
* Inner-row slots that represent specific options inside one page.
|
|
332
|
+
* DSH 0.2 keeps the plugin cards in its standalone Plugins panel
|
|
333
|
+
* (`plugins.item`) and renders the Settings → Plugins inventory tab itself,
|
|
334
|
+
* so only the tab seat is inside the settings panel the search reaches.
|
|
335
|
+
* `settings.plugin.item` and `web-ui.plugin.item` are the pre-0.2 card
|
|
336
|
+
* seats — 0.2 declares neither, and they stay indexed for older builds.
|
|
337
|
+
*/
|
|
297
338
|
const DEEP_SLOTS = [
|
|
298
339
|
{ slot: 'settings.plugins.tab', parent: 'plugins' },
|
|
299
340
|
{ slot: 'settings.plugin.item', parent: 'plugins' },
|
|
@@ -472,16 +513,192 @@ window.__ModuleLoader__.load({
|
|
|
472
513
|
|
|
473
514
|
const AI_TOGGLE_EVENT = 'dsh-settings-search:ai-toggle'
|
|
474
515
|
const AI_CONFIG_EVENT = 'dsh-settings-search:ai-config'
|
|
475
|
-
/**
|
|
516
|
+
/**
|
|
517
|
+
* Log recording has two sinks: the in-memory ring that feeds the panel
|
|
518
|
+
* instantly, and the host log file the panel can view and open. Every entry
|
|
519
|
+
* is formatted once ({@link logLine}), so the file, the viewer, and the
|
|
520
|
+
* exported text all read the same. Lines are handed to the host in small
|
|
521
|
+
* batches — a settings panel produces bursts, and one request per line
|
|
522
|
+
* would be wasteful — and the transport stays deliberately silent about its
|
|
523
|
+
* own failures so a broken channel can never log about itself.
|
|
524
|
+
*/
|
|
476
525
|
const LOG_EVENT = 'dsh-settings-search:log'
|
|
526
|
+
/** Fires when the on-disk entry count changes, so the panel's badge follows it. */
|
|
527
|
+
const LOG_COUNT_EVENT = 'dsh-settings-search:log-count'
|
|
477
528
|
const LOG_MAX = 500
|
|
529
|
+
const LOG_LINE_MAX = 800
|
|
530
|
+
const LOG_BATCH_MAX = 40
|
|
531
|
+
const LOG_FLUSH_MS = 1200
|
|
532
|
+
/** Document-relative form of the host half's `/api/settings-search-log`. */
|
|
533
|
+
const LOG_ROUTE = 'api/settings-search-log'
|
|
534
|
+
/** Per page-load run id, so one file can tell concurrent runs apart. */
|
|
535
|
+
const LOG_RUN = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, '0')
|
|
478
536
|
const LOGS = []
|
|
537
|
+
let logSeq = 0
|
|
538
|
+
|
|
539
|
+
const safeJson = (value) => {
|
|
540
|
+
try {
|
|
541
|
+
const text = JSON.stringify(value)
|
|
542
|
+
return typeof text === 'string' ? text : ''
|
|
543
|
+
} catch {
|
|
544
|
+
return '"<unserializable>"'
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/** One line per entry: `time [level] [run:seq] message {meta}`. */
|
|
549
|
+
const logLine = (entry) => {
|
|
550
|
+
const meta = entry.meta === undefined ? '' : ` ${safeJson(entry.meta)}`
|
|
551
|
+
const seq = Number.isFinite(entry.seq) ? entry.seq : 0
|
|
552
|
+
return `${entry.time} [${entry.level}] [${LOG_RUN}:${seq}] ${entry.msg}${meta}`.slice(0, LOG_LINE_MAX)
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/** Host-log transport state: pending lines, its timer, and the last outcome. */
|
|
556
|
+
const logTransport = { pending: [], timer: null, state: 'idle', dir: '', file: '', detail: '', count: null }
|
|
557
|
+
|
|
558
|
+
const setLogOutcome = (failed, payload, detail) => {
|
|
559
|
+
logTransport.state = failed ? 'fail' : 'ok'
|
|
560
|
+
logTransport.detail = failed ? detail : ''
|
|
561
|
+
if (typeof payload?.dir === 'string') logTransport.dir = payload.dir
|
|
562
|
+
if (typeof payload?.file === 'string') logTransport.file = payload.file
|
|
563
|
+
// The host answers the on-disk entry count on every call, so the panel's
|
|
564
|
+
// badge reflects the log that exists rather than this page's ring.
|
|
565
|
+
if (Number.isFinite(payload?.count) && payload.count !== logTransport.count) {
|
|
566
|
+
logTransport.count = payload.count
|
|
567
|
+
try { window.dispatchEvent(new CustomEvent(LOG_COUNT_EVENT)) } catch {}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** One request to the host log route; never throws, never logs. */
|
|
572
|
+
const postLog = async (body) => {
|
|
573
|
+
try {
|
|
574
|
+
const response = await fetch(LOG_ROUTE, {
|
|
575
|
+
method: 'POST',
|
|
576
|
+
headers: { 'content-type': 'application/json' },
|
|
577
|
+
body: JSON.stringify(body),
|
|
578
|
+
})
|
|
579
|
+
const payload = await response.json().catch(() => null)
|
|
580
|
+
if (!response.ok || payload?.ok !== true) {
|
|
581
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
582
|
+
return null
|
|
583
|
+
}
|
|
584
|
+
setLogOutcome(false, payload, '')
|
|
585
|
+
return payload
|
|
586
|
+
} catch (error) {
|
|
587
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
588
|
+
return null
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/** Ship the queued lines now; the host stops serving once its batch is full. */
|
|
593
|
+
const flushLogs = () => {
|
|
594
|
+
if (logTransport.timer !== null) {
|
|
595
|
+
clearTimeout(logTransport.timer)
|
|
596
|
+
logTransport.timer = null
|
|
597
|
+
}
|
|
598
|
+
if (logTransport.pending.length === 0) return
|
|
599
|
+
const lines = logTransport.pending.splice(0, logTransport.pending.length)
|
|
600
|
+
void postLog({ action: 'append', lines })
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Drop queued lines without shipping them (used by 清空日志). */
|
|
604
|
+
const discardPendingLogs = () => {
|
|
605
|
+
if (logTransport.timer !== null) {
|
|
606
|
+
clearTimeout(logTransport.timer)
|
|
607
|
+
logTransport.timer = null
|
|
608
|
+
}
|
|
609
|
+
logTransport.pending.length = 0
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const queueLogLine = (line) => {
|
|
613
|
+
logTransport.pending.push(line)
|
|
614
|
+
if (logTransport.pending.length >= LOG_BATCH_MAX) {
|
|
615
|
+
flushLogs()
|
|
616
|
+
return
|
|
617
|
+
}
|
|
618
|
+
if (logTransport.timer === null) logTransport.timer = setTimeout(flushLogs, LOG_FLUSH_MS)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/** Read the host log tail (viewer, and the folder path the panel offers). */
|
|
622
|
+
const readLogFile = async () => {
|
|
623
|
+
try {
|
|
624
|
+
const response = await fetch(LOG_ROUTE, { method: 'GET' })
|
|
625
|
+
const payload = await response.json().catch(() => null)
|
|
626
|
+
if (!response.ok || payload?.ok !== true) {
|
|
627
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
setLogOutcome(false, payload, '')
|
|
631
|
+
return {
|
|
632
|
+
text: String(payload.text ?? ''),
|
|
633
|
+
size: Number(payload.size ?? 0),
|
|
634
|
+
truncated: payload.truncated === true,
|
|
635
|
+
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
638
|
+
return null
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Refresh the on-disk entry count without transferring the file tail.
|
|
644
|
+
* @returns the count, or null when the host channel is unavailable.
|
|
645
|
+
*/
|
|
646
|
+
const readLogCount = async () => {
|
|
647
|
+
try {
|
|
648
|
+
const response = await fetch(`${LOG_ROUTE}?count=1`, { method: 'GET' })
|
|
649
|
+
const payload = await response.json().catch(() => null)
|
|
650
|
+
if (!response.ok || payload?.ok !== true) {
|
|
651
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
652
|
+
return null
|
|
653
|
+
}
|
|
654
|
+
setLogOutcome(false, payload, '')
|
|
655
|
+
return Number.isFinite(payload.count) ? payload.count : null
|
|
656
|
+
} catch (error) {
|
|
657
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
658
|
+
return null
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Open the log folder in the OS file manager. */
|
|
663
|
+
const openLogFolder = async () => {
|
|
664
|
+
const payload = await postLog({ action: 'open' })
|
|
665
|
+
return { ok: payload !== null, dir: logTransport.dir, file: logTransport.file, detail: logTransport.detail }
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** Truncate the on-disk log. */
|
|
669
|
+
const clearLogFile = async () => { await postLog({ action: 'clear' }) }
|
|
670
|
+
|
|
671
|
+
/** Current host-log target and last transport outcome, for the panel. */
|
|
672
|
+
const logTarget = () => ({
|
|
673
|
+
dir: logTransport.dir,
|
|
674
|
+
file: logTransport.file,
|
|
675
|
+
state: logTransport.state,
|
|
676
|
+
detail: logTransport.detail,
|
|
677
|
+
count: logTransport.count,
|
|
678
|
+
})
|
|
679
|
+
|
|
680
|
+
/** This session's entries in the same one-line format the file uses. */
|
|
681
|
+
const sessionLogText = () => LOGS.map(logLine).join('\n')
|
|
682
|
+
|
|
479
683
|
const log = (level, msg, meta) => {
|
|
480
684
|
if (typeof window === 'undefined') return
|
|
481
|
-
|
|
685
|
+
const entry = { time: new Date().toISOString(), level, msg: String(msg), meta, seq: ++logSeq }
|
|
686
|
+
LOGS.push(entry)
|
|
482
687
|
if (LOGS.length > LOG_MAX) LOGS.splice(0, LOGS.length - LOG_MAX)
|
|
688
|
+
queueLogLine(logLine(entry))
|
|
483
689
|
try { window.dispatchEvent(new CustomEvent(LOG_EVENT, { detail: LOGS.length })) } catch {}
|
|
484
690
|
}
|
|
691
|
+
|
|
692
|
+
if (typeof window !== 'undefined') {
|
|
693
|
+
// A page leaving the document is the last chance to ship queued lines.
|
|
694
|
+
try { window.addEventListener('pagehide', flushLogs) } catch {}
|
|
695
|
+
try {
|
|
696
|
+
window.addEventListener('visibilitychange', () => {
|
|
697
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') flushLogs()
|
|
698
|
+
})
|
|
699
|
+
} catch {}
|
|
700
|
+
}
|
|
701
|
+
|
|
485
702
|
const formatLogs = (logs) => {
|
|
486
703
|
const lines = [
|
|
487
704
|
'dsh-settings-search log',
|
|
@@ -490,9 +707,7 @@ window.__ModuleLoader__.load({
|
|
|
490
707
|
`count: ${logs.length}`,
|
|
491
708
|
'',
|
|
492
709
|
]
|
|
493
|
-
for (const entry of logs)
|
|
494
|
-
lines.push(`[${entry.time}] [${entry.level}] ${entry.msg}${entry.meta ? ' ' + JSON.stringify(entry.meta) : ''}`)
|
|
495
|
-
}
|
|
710
|
+
for (const entry of logs) lines.push(logLine(entry))
|
|
496
711
|
return lines.join('\n')
|
|
497
712
|
}
|
|
498
713
|
const AI_CACHE = new Map()
|
|
@@ -959,7 +1174,7 @@ window.__ModuleLoader__.load({
|
|
|
959
1174
|
.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; }
|
|
960
1175
|
.sss-trigger:hover { border-color: var(--dsw-alias-label-dimmed); color: var(--dsw-alias-label-secondary); }
|
|
961
1176
|
.sss-trigger:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: -2px; }
|
|
962
|
-
.sss-page-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: flex-start; justify-content: center; padding: 72px 20px 32px; background: var(--dsw-alias-bg-mask, rgb(0 0 0 / 36%)); }
|
|
1177
|
+
.sss-page-overlay { position: fixed; inset: 0; z-index: 1000; display: flex; align-items: flex-start; justify-content: center; padding: 72px 20px 32px; background: var(--dsw-alias-bg-mask-1, rgb(0 0 0 / 36%)); }
|
|
963
1178
|
.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); }
|
|
964
1179
|
.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); }
|
|
965
1180
|
.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); }
|
|
@@ -1020,7 +1235,7 @@ window.__ModuleLoader__.load({
|
|
|
1020
1235
|
.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; }
|
|
1021
1236
|
.sss-vr-link:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
1022
1237
|
.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; }
|
|
1023
|
-
.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; }
|
|
1238
|
+
.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-1, rgb(0 0 0 / 36%)); box-sizing: border-box; }
|
|
1024
1239
|
.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; }
|
|
1025
1240
|
.sss-wl-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
1026
1241
|
.sss-wl-headText { flex: 1; min-width: 0; }
|
|
@@ -1041,7 +1256,7 @@ window.__ModuleLoader__.load({
|
|
|
1041
1256
|
.sss-wl-primary:hover { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
1042
1257
|
.sss-wl-skip { margin-left: auto; border: none; background: transparent; color: var(--dsw-alias-label-tertiary); padding: 7px 4px; }
|
|
1043
1258
|
.sss-wl-skip:hover { color: var(--dsw-alias-label-primary); border-color: transparent; }
|
|
1044
|
-
.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; }
|
|
1259
|
+
.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-1, rgb(0 0 0 / 36%)); box-sizing: border-box; }
|
|
1045
1260
|
.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; }
|
|
1046
1261
|
.sss-cf-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
1047
1262
|
.sss-cf-headText { flex: 1; min-width: 0; }
|
|
@@ -1052,6 +1267,15 @@ window.__ModuleLoader__.load({
|
|
|
1052
1267
|
.sss-cf-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
1053
1268
|
.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; }
|
|
1054
1269
|
.sss-cf-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
1270
|
+
.sss-lv-overlay { position: fixed; inset: 0; z-index: 1002; display: flex; align-items: center; justify-content: center; padding: 20px; background: var(--dsw-alias-bg-mask-1, rgb(0 0 0 / 36%)); box-sizing: border-box; }
|
|
1271
|
+
.sss-lv { display: flex; flex-direction: column; width: min(880px, 100%); max-height: min(760px, calc(100vh - 40px)); 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: 16px 18px 14px; }
|
|
1272
|
+
.sss-lv-header { flex: none; display: flex; align-items: center; gap: 10px; }
|
|
1273
|
+
.sss-lv-title { flex: 1; min-width: 0; margin: 0; color: var(--dsw-alias-label-primary); font-size: 16px; font-weight: 650; line-height: 1.4; }
|
|
1274
|
+
.sss-lv-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; }
|
|
1275
|
+
.sss-lv-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
1276
|
+
.sss-lv-meta { flex: none; margin: 6px 0 0; color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 1.5; word-break: break-all; }
|
|
1277
|
+
.sss-lv-body { flex: 1; min-height: 120px; overflow: auto; margin: 10px 0 0; padding: 10px 12px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 11px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; }
|
|
1278
|
+
.sss-lv-actions { flex: none; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
1055
1279
|
.sss-cf-danger { background: #c0392b; color: #fff; border-color: transparent; }
|
|
1056
1280
|
.sss-cf-danger:hover { background: #a93226; color: #fff; border-color: transparent; }
|
|
1057
1281
|
`
|
|
@@ -1134,6 +1358,7 @@ window.__ModuleLoader__.load({
|
|
|
1134
1358
|
String(row.id ?? row.name ?? ''),
|
|
1135
1359
|
String(row.npm ?? row.package ?? row.packageName ?? ''),
|
|
1136
1360
|
String(row.version ?? ''),
|
|
1361
|
+
row.enabled === undefined ? '' : String(row.enabled),
|
|
1137
1362
|
].sort()).sort())
|
|
1138
1363
|
}
|
|
1139
1364
|
|
|
@@ -1453,18 +1678,27 @@ window.__ModuleLoader__.load({
|
|
|
1453
1678
|
: a.order - b.order)
|
|
1454
1679
|
}
|
|
1455
1680
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1681
|
+
/**
|
|
1682
|
+
* Fingerprint of the seats the index is built from: every settings page
|
|
1683
|
+
* plus each in-page seat this build declares. It is the change signal for
|
|
1684
|
+
* compositions that expose no plugin inventory at all (see
|
|
1685
|
+
* hasPluginInventory), so pages and rows appearing or disappearing still
|
|
1686
|
+
* invalidate a stored index. The pre-0.2 card seats alone would be empty
|
|
1687
|
+
* on DSH 0.2, which declares neither.
|
|
1688
|
+
*/
|
|
1689
|
+
const slotsFingerprint = () => {
|
|
1690
|
+
const rows = []
|
|
1691
|
+
for (const slot of ['settings.section', ...DEEP_SLOTS.map((config) => config.slot)]) {
|
|
1692
|
+
for (const entry of sortEntries(entriesOf(slot))) {
|
|
1693
|
+
rows.push([
|
|
1694
|
+
slot,
|
|
1695
|
+
String(entry.options?.id ?? entry.options?.key ?? ''),
|
|
1696
|
+
optionLabel(entry.options?.label),
|
|
1697
|
+
])
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
return JSON.stringify(rows.sort())
|
|
1701
|
+
}
|
|
1468
1702
|
|
|
1469
1703
|
const resetHarvest = () => { harvest.clear(); warmed = false }
|
|
1470
1704
|
|
|
@@ -1638,6 +1872,97 @@ window.__ModuleLoader__.load({
|
|
|
1638
1872
|
return { jumpToPage, jumpToOption }
|
|
1639
1873
|
}
|
|
1640
1874
|
|
|
1875
|
+
/**
|
|
1876
|
+
* Resolve the running DSH build's settings/LLM read faces.
|
|
1877
|
+
*
|
|
1878
|
+
* DSH 0.2 (the official desktop and `dsh web` release) exposes host
|
|
1879
|
+
* capabilities as Typert Remote namespaces: `remote.settings.describe()`
|
|
1880
|
+
* and `remote.llm.listConfigurableProviders()` each resolve to
|
|
1881
|
+
* `{ ok: true, value } | { ok: false, error }`. Older builds exposed the
|
|
1882
|
+
* same data through the unary client carried by the connection service
|
|
1883
|
+
* (`connection.api.settings.describe({})`, resolving to
|
|
1884
|
+
* `{ result: { ok, value } }`). Both generations answer with the same
|
|
1885
|
+
* namespace views, so one small face is resolved per call.
|
|
1886
|
+
*
|
|
1887
|
+
* @param ctx - the client plugin context.
|
|
1888
|
+
* @returns the read face, or null when this build offers neither generation.
|
|
1889
|
+
*/
|
|
1890
|
+
const resolveDshModelFace = (ctx) => {
|
|
1891
|
+
const remoteSettings = ctx.get('remote.settings')
|
|
1892
|
+
if (remoteSettings !== undefined && remoteSettings !== null) {
|
|
1893
|
+
const remoteLlm = ctx.get('remote.llm')
|
|
1894
|
+
return {
|
|
1895
|
+
describe: async () => {
|
|
1896
|
+
const response = await remoteSettings.describe()
|
|
1897
|
+
if (!response?.ok) throw new Error(response?.error?.message || 'settings.describe failed')
|
|
1898
|
+
return response.value
|
|
1899
|
+
},
|
|
1900
|
+
configuredProviders: async () => {
|
|
1901
|
+
if (typeof remoteLlm?.listConfigurableProviders !== 'function') return null
|
|
1902
|
+
const response = await remoteLlm.listConfigurableProviders().catch(() => null)
|
|
1903
|
+
return response?.ok ? response.value : null
|
|
1904
|
+
},
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
const legacyApi = ctx.get('connection')?.api
|
|
1908
|
+
if (legacyApi?.settings?.describe) {
|
|
1909
|
+
return {
|
|
1910
|
+
describe: async () => {
|
|
1911
|
+
const response = await legacyApi.settings.describe({})
|
|
1912
|
+
if (!response?.result?.ok) throw new Error(response?.result?.error?.message || 'settings.describe failed')
|
|
1913
|
+
return response.result.value
|
|
1914
|
+
},
|
|
1915
|
+
configuredProviders: async () => {
|
|
1916
|
+
if (typeof legacyApi.llm?.providers !== 'function') return null
|
|
1917
|
+
const response = await legacyApi.llm.providers({}).catch(() => null)
|
|
1918
|
+
return response?.result?.ok ? response.result.value?.providers ?? null : null
|
|
1919
|
+
},
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
return null
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
/**
|
|
1926
|
+
* Read the model fields of DSH's current default model; credentials are
|
|
1927
|
+
* never requested. `agent-default-model` names the provider and the model,
|
|
1928
|
+
* the provider's own settings namespace carries the Base URL, and the model
|
|
1929
|
+
* profile inside that namespace may override the wire protocol.
|
|
1930
|
+
*
|
|
1931
|
+
* @param ctx - the client plugin context.
|
|
1932
|
+
* @param t - translate function for the failure messages.
|
|
1933
|
+
* @returns `{ provider, baseUrl, model }`, or null when DSH has no default model.
|
|
1934
|
+
* @throws when this build exposes no settings API, or the default model has no Base URL.
|
|
1935
|
+
*/
|
|
1936
|
+
const readDshModelConfig = async (ctx, t) => {
|
|
1937
|
+
const face = resolveDshModelFace(ctx)
|
|
1938
|
+
if (face === null) throw new Error(t('aiDshImportApi'))
|
|
1939
|
+
const described = await face.describe()
|
|
1940
|
+
const namespaces = new Map((described?.namespaces ?? []).map((view) => [view.ns, view]))
|
|
1941
|
+
const defaultModel = namespaces.get('agent-default-model')?.value ?? {}
|
|
1942
|
+
const providerId = String(defaultModel.provider ?? '').trim()
|
|
1943
|
+
const modelId = String(defaultModel.model ?? '').trim()
|
|
1944
|
+
if (!modelId) return null
|
|
1945
|
+
const readPath = (value, path) => (path ?? []).reduce((current, key) => current?.[key], value)
|
|
1946
|
+
const providers = await face.configuredProviders()
|
|
1947
|
+
const providerRow = Array.isArray(providers) ? providers.find((entry) => entry.provider === providerId) : undefined
|
|
1948
|
+
const modelNamespace = namespaces.get(providerRow?.settingsNs || 'llm-pi-ai')
|
|
1949
|
+
const providerProfile = providerRow
|
|
1950
|
+
? readPath(modelNamespace?.value, providerRow.settingsPath) ?? {}
|
|
1951
|
+
: modelNamespace?.value?.providers?.[providerId] ?? {}
|
|
1952
|
+
const configuredModels = Array.isArray(providerProfile.models) ? providerProfile.models : []
|
|
1953
|
+
const modelProfile = configuredModels.find((entry) => entry?.id === modelId) ?? {}
|
|
1954
|
+
const protocol = String(modelProfile.api ?? providerProfile.api ?? '').trim()
|
|
1955
|
+
const baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1956
|
+
if (!baseUrl) throw new Error(t('aiDshImportNoBaseUrl').replace('{model}', modelId))
|
|
1957
|
+
return {
|
|
1958
|
+
provider: providerId.toLowerCase() === 'ollama' || protocol === 'ollama'
|
|
1959
|
+
? 'ollama'
|
|
1960
|
+
: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1961
|
+
baseUrl,
|
|
1962
|
+
model: modelId,
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1641
1966
|
function apply(ctx) {
|
|
1642
1967
|
log('info', '应用装载', { version: PKG_VERSION })
|
|
1643
1968
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'settings-search: dictionaries')
|
|
@@ -1650,65 +1975,101 @@ window.__ModuleLoader__.load({
|
|
|
1650
1975
|
const model = createModel(ctx)
|
|
1651
1976
|
const indexControls = { resetHarvest: model.resetHarvest, persistIndex: model.persistIndex }
|
|
1652
1977
|
const navigator = createNavigator(ctx)
|
|
1653
|
-
let
|
|
1978
|
+
let pluginInventoryToken = 0
|
|
1979
|
+
|
|
1980
|
+
/** Apply one plugin inventory as the index fingerprint, invalidating on change. */
|
|
1981
|
+
const applyPluginFingerprint = (rows) => {
|
|
1982
|
+
if (!Array.isArray(rows) || rows.length === 0) return
|
|
1983
|
+
if (model.applyIndexFingerprint('manager:' + otherPluginFingerprint(rows))) {
|
|
1984
|
+
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1985
|
+
log('info', '检测到其它插件变化,将重建设置索引', {})
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1654
1988
|
|
|
1655
|
-
|
|
1989
|
+
/** Legacy (pre-0.2) client plugin manager service: `list()` plus change events. */
|
|
1990
|
+
const syncLegacyPluginManagerIndex = async (face) => {
|
|
1656
1991
|
if (face === null || !face.isLoopback) return
|
|
1657
|
-
const token = ++
|
|
1992
|
+
const token = ++pluginInventoryToken
|
|
1658
1993
|
try {
|
|
1659
1994
|
const rows = await face.list()
|
|
1660
|
-
if (token !==
|
|
1661
|
-
|
|
1662
|
-
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1663
|
-
log('info', '检测到其它插件变化,将重建设置索引', {})
|
|
1664
|
-
}
|
|
1995
|
+
if (token !== pluginInventoryToken) return
|
|
1996
|
+
applyPluginFingerprint(rows)
|
|
1665
1997
|
} catch {
|
|
1666
1998
|
// Keep the last trusted fingerprint when manager inspection fails.
|
|
1667
1999
|
}
|
|
1668
2000
|
}
|
|
1669
2001
|
|
|
2002
|
+
/**
|
|
2003
|
+
* Read the installed plugin inventory from DSH 0.2's `remote.pluginManager`
|
|
2004
|
+
* namespace. Both calls answer `{ ok, value }`: bundles carry `name` and
|
|
2005
|
+
* `version`, plugin rows carry `entryId`/`moduleName` and a fiber phase.
|
|
2006
|
+
*
|
|
2007
|
+
* @param face - the `remote.pluginManager` service.
|
|
2008
|
+
* @returns inventory rows for otherPluginFingerprint().
|
|
2009
|
+
*/
|
|
2010
|
+
const readRemotePluginInventory = async (face) => {
|
|
2011
|
+
const [bundles, plugins] = await Promise.all([
|
|
2012
|
+
typeof face?.listBundles === 'function' ? face.listBundles().catch(() => null) : null,
|
|
2013
|
+
typeof face?.listPlugins === 'function' ? face.listPlugins().catch(() => null) : null,
|
|
2014
|
+
])
|
|
2015
|
+
const rows = []
|
|
2016
|
+
if (bundles?.ok === true) {
|
|
2017
|
+
for (const row of bundles.value ?? []) rows.push({ id: row?.name, npm: row?.name, version: row?.version, enabled: row?.enabled })
|
|
2018
|
+
}
|
|
2019
|
+
if (plugins?.ok === true) {
|
|
2020
|
+
for (const row of plugins.value ?? []) rows.push({ id: row?.entryId, npm: row?.moduleName, enabled: row?.enabled })
|
|
2021
|
+
}
|
|
2022
|
+
return rows
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
const syncRemotePluginInventory = async (face) => {
|
|
2026
|
+
const token = ++pluginInventoryToken
|
|
2027
|
+
try {
|
|
2028
|
+
const rows = await readRemotePluginInventory(face)
|
|
2029
|
+
if (token !== pluginInventoryToken) return
|
|
2030
|
+
applyPluginFingerprint(rows)
|
|
2031
|
+
} catch {
|
|
2032
|
+
// Keep the last trusted fingerprint when the inventory read fails.
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* Whether this build exposes a plugin inventory to watch: the 0.2 Remote
|
|
2038
|
+
* namespace or the legacy client service. Without one the seat fingerprint
|
|
2039
|
+
* is the only change signal.
|
|
2040
|
+
*/
|
|
2041
|
+
const hasPluginInventory = () => {
|
|
2042
|
+
const remote = ctx.get('remote.pluginManager')
|
|
2043
|
+
if (remote !== undefined && remote !== null) return true
|
|
2044
|
+
const legacy = ctx.get('pluginManager')
|
|
2045
|
+
return legacy !== undefined && legacy !== null
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/**
|
|
2049
|
+
* Re-check the DSH 0.2 inventory. The Remote namespace has no change event,
|
|
2050
|
+
* so the check runs when the namespace arrives and whenever the settings
|
|
2051
|
+
* panel mounts (installs and updates land between the two).
|
|
2052
|
+
*/
|
|
2053
|
+
const refreshPluginInventory = () => {
|
|
2054
|
+
const face = ctx.get('remote.pluginManager')
|
|
2055
|
+
if (face !== undefined && face !== null) void syncRemotePluginInventory(face)
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
ctx.inject(['remote.pluginManager'], (inner) => {
|
|
2059
|
+
void syncRemotePluginInventory(inner.get('remote.pluginManager'))
|
|
2060
|
+
return () => { pluginInventoryToken += 1 }
|
|
2061
|
+
}, 'settings-search: remote plugin inventory fingerprint')
|
|
2062
|
+
|
|
1670
2063
|
ctx.inject(['pluginManager'], (inner) => {
|
|
1671
2064
|
const face = inner.pluginManager ?? null
|
|
1672
|
-
const sync = () => { void
|
|
2065
|
+
const sync = () => { void syncLegacyPluginManagerIndex(face) }
|
|
1673
2066
|
sync()
|
|
1674
2067
|
const unsubscribe = typeof face?.onChange === 'function' ? face.onChange(sync) : null
|
|
1675
2068
|
return () => {
|
|
1676
|
-
|
|
2069
|
+
pluginInventoryToken += 1
|
|
1677
2070
|
if (typeof unsubscribe === 'function') unsubscribe()
|
|
1678
2071
|
}
|
|
1679
|
-
}, 'settings-search: plugin manager index fingerprint')
|
|
1680
|
-
|
|
1681
|
-
/** Read the model fields from DSH; credentials are never requested. */
|
|
1682
|
-
const readDshModelConfig = async () => {
|
|
1683
|
-
const connection = ctx.get('connection')
|
|
1684
|
-
const response = await connection.api.settings.describe({})
|
|
1685
|
-
if (!response.result.ok) throw new Error(response.result.error?.message || 'settings.describe failed')
|
|
1686
|
-
const namespaces = new Map(response.result.value.namespaces.map((view) => [view.ns, view]))
|
|
1687
|
-
const defaultModel = namespaces.get('agent-default-model')?.value ?? {}
|
|
1688
|
-
const providerId = String(defaultModel.provider ?? '').trim()
|
|
1689
|
-
const modelId = String(defaultModel.model ?? '').trim()
|
|
1690
|
-
const readPath = (value, path) => (path ?? []).reduce((current, key) => current?.[key], value)
|
|
1691
|
-
const providersResponse = await connection.api.llm.providers({}).catch(() => null)
|
|
1692
|
-
const providerRow = providersResponse?.result?.ok
|
|
1693
|
-
? providersResponse.result.value.providers.find((entry) => entry.provider === providerId)
|
|
1694
|
-
: undefined
|
|
1695
|
-
const modelNamespace = namespaces.get(providerRow?.settingsNs || 'llm-pi-ai')
|
|
1696
|
-
const providerProfile = providerRow
|
|
1697
|
-
? readPath(modelNamespace?.value, providerRow.settingsPath) ?? {}
|
|
1698
|
-
: modelNamespace?.value?.providers?.[providerId] ?? {}
|
|
1699
|
-
const configuredModels = Array.isArray(providerProfile.models) ? providerProfile.models : []
|
|
1700
|
-
const modelProfile = configuredModels.find((entry) => entry?.id === modelId) ?? {}
|
|
1701
|
-
const protocol = String(modelProfile.api ?? providerProfile.api ?? '').trim()
|
|
1702
|
-
const baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1703
|
-
if (!modelId || !baseUrl) return null
|
|
1704
|
-
return {
|
|
1705
|
-
provider: providerId.toLowerCase() === 'ollama' || protocol === 'ollama'
|
|
1706
|
-
? 'ollama'
|
|
1707
|
-
: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1708
|
-
baseUrl,
|
|
1709
|
-
model: modelId,
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
2072
|
+
}, 'settings-search: plugin manager index fingerprint (legacy)')
|
|
1712
2073
|
|
|
1713
2074
|
const SearchIcon = React.createElement('svg', {
|
|
1714
2075
|
className: 'sss-icon',
|
|
@@ -1790,6 +2151,36 @@ window.__ModuleLoader__.load({
|
|
|
1790
2151
|
])
|
|
1791
2152
|
}
|
|
1792
2153
|
|
|
2154
|
+
/**
|
|
2155
|
+
* Read-only log viewer. Shows the host log tail when the host answered and
|
|
2156
|
+
* the current session's ring otherwise, with the file path (or the reason
|
|
2157
|
+
* the file is unavailable) spelled out above the text.
|
|
2158
|
+
*/
|
|
2159
|
+
function LogViewer({ t, view }) {
|
|
2160
|
+
return React.createElement('div', {
|
|
2161
|
+
className: 'sss-lv-overlay',
|
|
2162
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) view.onClose() },
|
|
2163
|
+
}, [
|
|
2164
|
+
React.createElement('div', { className: 'sss-lv', role: 'dialog', 'aria-modal': 'true' }, [
|
|
2165
|
+
React.createElement('div', { className: 'sss-lv-header' }, [
|
|
2166
|
+
React.createElement('span', { className: 'sss-lv-title' }, t('logViewerTitle')),
|
|
2167
|
+
React.createElement('button', { className: 'sss-lv-close', type: 'button', 'aria-label': t('logViewerClose'), onClick: () => view.onClose() }, '✕'),
|
|
2168
|
+
]),
|
|
2169
|
+
React.createElement('p', { className: 'sss-lv-meta' }, view.metaText),
|
|
2170
|
+
view.truncated ? React.createElement('p', { className: 'sss-lv-meta' }, t('logViewerTruncated')) : null,
|
|
2171
|
+
React.createElement('pre', { className: 'sss-lv-body' }, view.text === '' ? t('logViewerEmpty') : view.text),
|
|
2172
|
+
React.createElement('div', { className: 'sss-lv-actions' }, [
|
|
2173
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: view.loading, onClick: () => view.onRefresh() },
|
|
2174
|
+
view.loading ? t('logViewerLoading') : t('logViewerRefresh')),
|
|
2175
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => view.onOpenFolder() }, t('logOpenDir')),
|
|
2176
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => view.onCopy() },
|
|
2177
|
+
view.copied ? t('logViewerCopied') : t('logViewerCopy')),
|
|
2178
|
+
React.createElement('button', { className: 'sss-vr-btn sss-vr-primary', type: 'button', onClick: () => view.onClose() }, t('logViewerClose')),
|
|
2179
|
+
]),
|
|
2180
|
+
]),
|
|
2181
|
+
])
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1793
2184
|
function SettingsHeaderSearch() {
|
|
1794
2185
|
const [query, setQuery] = React.useState('')
|
|
1795
2186
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1863,11 +2254,14 @@ window.__ModuleLoader__.load({
|
|
|
1863
2254
|
}, [])
|
|
1864
2255
|
|
|
1865
2256
|
React.useEffect(() => {
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
|
|
2257
|
+
// DSH 0.2 answers the plugin inventory through `remote.pluginManager`
|
|
2258
|
+
// with no change event, so re-check it every time the panel mounts.
|
|
2259
|
+
refreshPluginInventory()
|
|
2260
|
+
// Without any plugin inventory, the seats the index was built from are
|
|
2261
|
+
// the only available change signal: re-apply them on every mount, so a
|
|
2262
|
+
// page or row that appeared since the last visit invalidates the index.
|
|
1869
2263
|
const fallback = setTimeout(() => {
|
|
1870
|
-
if (
|
|
2264
|
+
if (hasPluginInventory()) return
|
|
1871
2265
|
if (model.applyIndexFingerprint(model.slotsFingerprint())) {
|
|
1872
2266
|
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1873
2267
|
}
|
|
@@ -2361,6 +2755,10 @@ window.__ModuleLoader__.load({
|
|
|
2361
2755
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
2362
2756
|
const [logExportFail, setLogExportFail] = React.useState(false)
|
|
2363
2757
|
const [logCleared, setLogCleared] = React.useState(false)
|
|
2758
|
+
const [logViewer, setLogViewer] = React.useState(false)
|
|
2759
|
+
const [logViewerState, setLogViewerState] = React.useState({ status: 'idle', text: '', size: 0, truncated: false })
|
|
2760
|
+
const [logViewerCopied, setLogViewerCopied] = React.useState(false)
|
|
2761
|
+
const [logFolderMsg, setLogFolderMsg] = React.useState('')
|
|
2364
2762
|
const [indexState, setIndexState] = React.useState('idle')
|
|
2365
2763
|
const [warming, setWarming] = React.useState(false)
|
|
2366
2764
|
const [confirmClear, setConfirmClear] = React.useState(false)
|
|
@@ -2399,12 +2797,19 @@ window.__ModuleLoader__.load({
|
|
|
2399
2797
|
React.useEffect(() => {
|
|
2400
2798
|
const sync = () => setLogVersion((v) => v + 1)
|
|
2401
2799
|
window.addEventListener(LOG_EVENT, sync)
|
|
2402
|
-
|
|
2800
|
+
window.addEventListener(LOG_COUNT_EVENT, sync)
|
|
2801
|
+
// The badge counts what exists on disk, including earlier runs.
|
|
2802
|
+
void readLogCount()
|
|
2803
|
+
return () => {
|
|
2804
|
+
window.removeEventListener(LOG_EVENT, sync)
|
|
2805
|
+
window.removeEventListener(LOG_COUNT_EVENT, sync)
|
|
2806
|
+
}
|
|
2403
2807
|
}, [])
|
|
2404
2808
|
|
|
2405
2809
|
useAutoReset(logCopied, () => setLogCopied(false))
|
|
2406
2810
|
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2407
2811
|
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
2812
|
+
useAutoReset(logViewerCopied, () => setLogViewerCopied(false))
|
|
2408
2813
|
|
|
2409
2814
|
const changeProvider = (next) => {
|
|
2410
2815
|
providerDrafts.current[provider] = { baseUrl, model }
|
|
@@ -2493,7 +2898,7 @@ window.__ModuleLoader__.load({
|
|
|
2493
2898
|
setDshImportState('loading')
|
|
2494
2899
|
setDshImportMsg('')
|
|
2495
2900
|
try {
|
|
2496
|
-
const imported = await readDshModelConfig()
|
|
2901
|
+
const imported = await readDshModelConfig(ctx, t)
|
|
2497
2902
|
if (!imported) throw new Error(t('aiDshImportEmpty'))
|
|
2498
2903
|
providerDrafts.current[provider] = { baseUrl, model }
|
|
2499
2904
|
setAiConfig({ ...imported, apiKey })
|
|
@@ -2632,8 +3037,64 @@ window.__ModuleLoader__.load({
|
|
|
2632
3037
|
const clearLogs = () => {
|
|
2633
3038
|
setConfirmClear(false)
|
|
2634
3039
|
LOGS.length = 0
|
|
3040
|
+
// Drop queued lines too: a late flush would resurrect what was cleared.
|
|
3041
|
+
discardPendingLogs()
|
|
2635
3042
|
setLogVersion((v) => v + 1)
|
|
2636
3043
|
setLogCleared(true)
|
|
3044
|
+
void clearLogFile()
|
|
3045
|
+
}
|
|
3046
|
+
|
|
3047
|
+
/** Read the on-disk tail for the viewer; fall back to this session's ring. */
|
|
3048
|
+
const refreshLogView = async () => {
|
|
3049
|
+
setLogViewerState((current) => ({ ...current, status: 'loading' }))
|
|
3050
|
+
const read = await readLogFile()
|
|
3051
|
+
if (read === null) {
|
|
3052
|
+
setLogViewerState({ status: 'fail', text: sessionLogText(), size: 0, truncated: false })
|
|
3053
|
+
return
|
|
3054
|
+
}
|
|
3055
|
+
setLogViewerState({ status: 'ok', text: read.text, size: read.size, truncated: read.truncated })
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const openLogViewer = () => {
|
|
3059
|
+
flushLogs()
|
|
3060
|
+
setLogViewer(true)
|
|
3061
|
+
void refreshLogView()
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
const copyLogView = async () => {
|
|
3065
|
+
try {
|
|
3066
|
+
await navigator.clipboard.writeText(logViewerState.text)
|
|
3067
|
+
setLogViewerCopied(true)
|
|
3068
|
+
} catch {
|
|
3069
|
+
setLogCopyFail(true)
|
|
3070
|
+
}
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
/** Open the on-disk log folder through the host half. */
|
|
3074
|
+
const openLogDir = async () => {
|
|
3075
|
+
flushLogs()
|
|
3076
|
+
setLogFolderMsg('')
|
|
3077
|
+
const result = await openLogFolder()
|
|
3078
|
+
setLogFolderMsg(result.ok
|
|
3079
|
+
? t('logOpenOk')
|
|
3080
|
+
: `${t('logOpenFail')}${result.detail === '' ? '' : `(${result.detail})`}`)
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
/** Badge value: entries that exist on disk, or this session's ring when the host is unavailable. */
|
|
3084
|
+
const logBadgeCount = logTarget().count ?? LOGS.length
|
|
3085
|
+
|
|
3086
|
+
const logView = {
|
|
3087
|
+
text: logViewerState.text,
|
|
3088
|
+
truncated: logViewerState.truncated,
|
|
3089
|
+
loading: logViewerState.status === 'loading',
|
|
3090
|
+
copied: logViewerCopied,
|
|
3091
|
+
metaText: logViewerState.status === 'fail' || logTarget().file === ''
|
|
3092
|
+
? t('logViewerMemory')
|
|
3093
|
+
: t('logViewerFile').replace('{file}', logTarget().file),
|
|
3094
|
+
onClose: () => setLogViewer(false),
|
|
3095
|
+
onRefresh: () => { void refreshLogView() },
|
|
3096
|
+
onOpenFolder: () => { void openLogDir() },
|
|
3097
|
+
onCopy: () => { void copyLogView() },
|
|
2637
3098
|
}
|
|
2638
3099
|
|
|
2639
3100
|
const check = async () => {
|
|
@@ -2781,7 +3242,7 @@ window.__ModuleLoader__.load({
|
|
|
2781
3242
|
}, indexState === 'loading' || warming ? t('indexRebuilding') : t('indexRebuild')),
|
|
2782
3243
|
]),
|
|
2783
3244
|
indexState === 'done'
|
|
2784
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: '
|
|
3245
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-success-primary)' } }, t('indexDone'))
|
|
2785
3246
|
: null,
|
|
2786
3247
|
]),
|
|
2787
3248
|
React.createElement('div', { className: 'sss-vr-field sss-vr-form' }, [
|
|
@@ -2848,7 +3309,7 @@ window.__ModuleLoader__.load({
|
|
|
2848
3309
|
modelsMsg
|
|
2849
3310
|
? React.createElement('p', {
|
|
2850
3311
|
className: modelsState === 'fail' ? 'sss-vr-note' : 'sss-vr-hint',
|
|
2851
|
-
style: modelsState === 'fail' ? { color: 'var(--dsw-alias-
|
|
3312
|
+
style: modelsState === 'fail' ? { color: 'var(--dsw-alias-state-error-primary)' } : undefined,
|
|
2852
3313
|
}, modelsMsg)
|
|
2853
3314
|
: null,
|
|
2854
3315
|
provider !== 'ollama' ? React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
@@ -2878,10 +3339,10 @@ window.__ModuleLoader__.load({
|
|
|
2878
3339
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => requestClearAi() }, t('aiClear')),
|
|
2879
3340
|
]),
|
|
2880
3341
|
dshImportMsg
|
|
2881
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? '
|
|
3342
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, dshImportMsg)
|
|
2882
3343
|
: null,
|
|
2883
3344
|
testMsg
|
|
2884
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? '
|
|
3345
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? 'var(--dsw-alias-state-success-primary)' : 'var(--dsw-alias-state-error-primary)' } }, testMsg)
|
|
2885
3346
|
: null,
|
|
2886
3347
|
]),
|
|
2887
3348
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
@@ -2907,7 +3368,7 @@ window.__ModuleLoader__.load({
|
|
|
2907
3368
|
copied ? t('updCopied') : t('updCopy')),
|
|
2908
3369
|
React.createElement('a', { className: 'sss-vr-link', href: releasesUrl, target: '_blank', rel: 'noopener noreferrer' }, t('updRelease')),
|
|
2909
3370
|
]),
|
|
2910
|
-
copyFailed ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-
|
|
3371
|
+
copyFailed ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('updCopyFail')) : null,
|
|
2911
3372
|
])
|
|
2912
3373
|
: null,
|
|
2913
3374
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
@@ -2925,23 +3386,28 @@ window.__ModuleLoader__.load({
|
|
|
2925
3386
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2926
3387
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2927
3388
|
React.createElement('span', { className: 'sss-vr-label' }, t('logTitle')),
|
|
2928
|
-
|
|
3389
|
+
logBadgeCount > 0
|
|
3390
|
+
? React.createElement('span', { className: 'sss-vr-badge', title: t('logCountHint') }, String(logBadgeCount))
|
|
3391
|
+
: null,
|
|
2929
3392
|
]),
|
|
2930
3393
|
React.createElement('p', { className: 'sss-vr-hint' }, t('logDesc')),
|
|
2931
|
-
LOGS.length === 0
|
|
3394
|
+
LOGS.length === 0 && logBadgeCount === 0
|
|
2932
3395
|
? React.createElement('p', { className: 'sss-vr-note' }, t('logEmpty'))
|
|
2933
3396
|
: null,
|
|
2934
3397
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
3398
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => openLogViewer() }, t('logView')),
|
|
3399
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void openLogDir() } }, t('logOpenDir')),
|
|
2935
3400
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void exportLogs() } }, t('logExport')),
|
|
2936
3401
|
React.createElement('a', { className: 'sss-vr-link', href: ISSUES_URL, target: '_blank', rel: 'noopener noreferrer' },
|
|
2937
3402
|
t('logIssues')),
|
|
2938
3403
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void copyLogs() } },
|
|
2939
3404
|
logCopied ? t('logCopied') : t('logCopy')),
|
|
2940
|
-
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled:
|
|
3405
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: logBadgeCount === 0, onClick: () => requestClear() },
|
|
2941
3406
|
logCleared ? t('logCleared') : t('logClear')),
|
|
2942
3407
|
]),
|
|
2943
|
-
|
|
2944
|
-
|
|
3408
|
+
logFolderMsg ? React.createElement('p', { className: 'sss-vr-note' }, logFolderMsg) : null,
|
|
3409
|
+
logCopyFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('logCopyFail')) : null,
|
|
3410
|
+
logExportFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('logExportFail')) : null,
|
|
2945
3411
|
]),
|
|
2946
3412
|
]) : null,
|
|
2947
3413
|
confirmClear
|
|
@@ -2955,6 +3421,9 @@ window.__ModuleLoader__.load({
|
|
|
2955
3421
|
onCancel: () => cancelClear(),
|
|
2956
3422
|
})
|
|
2957
3423
|
: null,
|
|
3424
|
+
logViewer
|
|
3425
|
+
? React.createElement(LogViewer, { t, view: logView })
|
|
3426
|
+
: null,
|
|
2958
3427
|
confirmClearAi
|
|
2959
3428
|
? React.createElement(ConfirmModal, {
|
|
2960
3429
|
t,
|