@objectivex666/dsh-settings-search 1.12.0 → 1.13.1
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 +9 -7
- package/README.md +9 -7
- package/lib/client.js +598 -91
- package/lib/index.js +448 -11
- package/package.json +5 -4
- package/scripts/ai.test.mjs +311 -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.1'
|
|
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,9 @@ window.__ModuleLoader__.load({
|
|
|
107
107
|
aiDshImportOk: '已导入 Base URL 与模型名;API Key 未改动 ✓',
|
|
108
108
|
aiDshImportFail: '导入失败:{reason}',
|
|
109
109
|
aiDshImportEmpty: '未找到 DSH 当前默认模型配置',
|
|
110
|
+
aiDshImportBuiltin: '已导入 DeepSeek 内置地址与模型名;API Key 未改动,插件内使用需填写自己的 DeepSeek API Key',
|
|
111
|
+
aiDshImportApi: '当前 DSH 版本未提供模型配置接口(remote.settings)',
|
|
112
|
+
aiDshImportNoBaseUrl: '默认模型「{model}」在 DSH 设置中没有可导入的 Base URL',
|
|
110
113
|
aiConfigSave: '保存配置',
|
|
111
114
|
aiConfigSaved: '已保存 ✓',
|
|
112
115
|
aiClear: '清除配置',
|
|
@@ -132,9 +135,14 @@ window.__ModuleLoader__.load({
|
|
|
132
135
|
aiOllamaErrForbidden: 'Ollama 拒绝访问,请检查 OLLAMA_ORIGINS 是否允许当前 DSH 页面来源',
|
|
133
136
|
aiOllamaErrTimeout: 'Ollama 响应超时,请检查本机服务及模型加载状态',
|
|
134
137
|
logTitle: '日志',
|
|
135
|
-
|
|
138
|
+
logCountHint: '磁盘日志文件中的条目数',
|
|
139
|
+
logDesc: '记录插件运行、搜索与 AI 请求的关键动作,便于排查问题;日志按天写入 DSH 的日志文件夹,文件名带日期。',
|
|
136
140
|
logEmpty: '暂无日志',
|
|
137
141
|
logExport: '导出日志',
|
|
142
|
+
logView: '查看日志',
|
|
143
|
+
logOpenDir: '打开日志文件夹',
|
|
144
|
+
logOpenOk: '已在文件管理器中打开日志文件夹 ✓',
|
|
145
|
+
logOpenFail: '打开日志文件夹失败',
|
|
138
146
|
logIssues: '反馈问题',
|
|
139
147
|
logCopy: '复制日志',
|
|
140
148
|
logCopied: '已复制 ✓',
|
|
@@ -142,10 +150,20 @@ window.__ModuleLoader__.load({
|
|
|
142
150
|
logClear: '清空日志',
|
|
143
151
|
logCleared: '已清空',
|
|
144
152
|
logConfirmTitle: '清空日志',
|
|
145
|
-
logConfirm: '
|
|
153
|
+
logConfirm: '确定要清空全部日志吗?磁盘上的日志文件也会一并清空。',
|
|
146
154
|
logConfirmYes: '确认清空',
|
|
147
155
|
logConfirmCancel: '取消',
|
|
148
156
|
logExportFail: '导出失败,请重试。',
|
|
157
|
+
logViewerTitle: '运行日志',
|
|
158
|
+
logViewerRefresh: '刷新',
|
|
159
|
+
logViewerLoading: '正在读取…',
|
|
160
|
+
logViewerCopy: '复制',
|
|
161
|
+
logViewerCopied: '已复制 ✓',
|
|
162
|
+
logViewerClose: '关闭',
|
|
163
|
+
logViewerEmpty: '暂无日志。',
|
|
164
|
+
logViewerFile: '文件:{file}',
|
|
165
|
+
logViewerMemory: '宿主日志通道不可用,这里显示本次会话的记录,重启后不再保留。',
|
|
166
|
+
logViewerTruncated: '仅显示文件末尾部分,更早的内容请打开日志文件夹查看。',
|
|
149
167
|
wlTitle: '欢迎使用设置搜索',
|
|
150
168
|
wlSubtitle: '为 DSH 设置面板提供即时搜索与导航,几秒钟就能找到任何设置。',
|
|
151
169
|
wlFeature1Title: '实时搜索',
|
|
@@ -236,6 +254,9 @@ window.__ModuleLoader__.load({
|
|
|
236
254
|
aiDshImportOk: 'Imported Base URL and model; API key unchanged ✓',
|
|
237
255
|
aiDshImportFail: 'Import failed: {reason}',
|
|
238
256
|
aiDshImportEmpty: 'No current DSH default model config found',
|
|
257
|
+
aiDshImportBuiltin: 'Imported the built-in DeepSeek address and model; the API key is unchanged, so fill in your own DeepSeek key to use it here',
|
|
258
|
+
aiDshImportApi: 'This DSH build exposes no model config API (remote.settings)',
|
|
259
|
+
aiDshImportNoBaseUrl: 'The default model "{model}" has no Base URL in DSH settings to import',
|
|
239
260
|
aiConfigSave: 'Save config',
|
|
240
261
|
aiConfigSaved: 'Saved ✓',
|
|
241
262
|
aiClear: 'Clear config',
|
|
@@ -261,9 +282,14 @@ window.__ModuleLoader__.load({
|
|
|
261
282
|
aiOllamaErrForbidden: 'Ollama denied access. Check that OLLAMA_ORIGINS allows this DSH page origin',
|
|
262
283
|
aiOllamaErrTimeout: 'Ollama timed out. Check the local service and model loading status',
|
|
263
284
|
logTitle: 'Logs',
|
|
264
|
-
|
|
285
|
+
logCountHint: 'Entries in the log file on disk',
|
|
286
|
+
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
287
|
logEmpty: 'No logs yet',
|
|
266
288
|
logExport: 'Export logs',
|
|
289
|
+
logView: 'View logs',
|
|
290
|
+
logOpenDir: 'Open log folder',
|
|
291
|
+
logOpenOk: 'Log folder opened in your file manager ✓',
|
|
292
|
+
logOpenFail: 'Could not open the log folder',
|
|
267
293
|
logIssues: 'Report issue',
|
|
268
294
|
logCopy: 'Copy logs',
|
|
269
295
|
logCopied: 'Copied ✓',
|
|
@@ -271,10 +297,20 @@ window.__ModuleLoader__.load({
|
|
|
271
297
|
logClear: 'Clear logs',
|
|
272
298
|
logCleared: 'Cleared',
|
|
273
299
|
logConfirmTitle: 'Clear logs',
|
|
274
|
-
logConfirm: 'Clear all logs?
|
|
300
|
+
logConfirm: 'Clear all logs? The log file on disk is truncated as well.',
|
|
275
301
|
logConfirmYes: 'Clear logs',
|
|
276
302
|
logConfirmCancel: 'Cancel',
|
|
277
303
|
logExportFail: 'Export failed. Please try again.',
|
|
304
|
+
logViewerTitle: 'Run log',
|
|
305
|
+
logViewerRefresh: 'Refresh',
|
|
306
|
+
logViewerLoading: 'Loading…',
|
|
307
|
+
logViewerCopy: 'Copy',
|
|
308
|
+
logViewerCopied: 'Copied ✓',
|
|
309
|
+
logViewerClose: 'Close',
|
|
310
|
+
logViewerEmpty: 'No logs yet.',
|
|
311
|
+
logViewerFile: 'File: {file}',
|
|
312
|
+
logViewerMemory: 'The host log channel is unavailable, so this shows the current session only; it is not kept after a reload.',
|
|
313
|
+
logViewerTruncated: 'Showing the end of the file only — open the log folder for earlier lines.',
|
|
278
314
|
wlTitle: 'Welcome to Settings Search',
|
|
279
315
|
wlSubtitle: 'Instant search and navigation for the DSH settings panel — find any setting in seconds.',
|
|
280
316
|
wlFeature1Title: 'Instant search',
|
|
@@ -293,7 +329,14 @@ window.__ModuleLoader__.load({
|
|
|
293
329
|
wlFeature6Desc: 'On first open it automatically passes through every settings section, so in-page options are searchable without opening each page manually.',
|
|
294
330
|
}
|
|
295
331
|
|
|
296
|
-
/**
|
|
332
|
+
/**
|
|
333
|
+
* Inner-row slots that represent specific options inside one page.
|
|
334
|
+
* DSH 0.2 keeps the plugin cards in its standalone Plugins panel
|
|
335
|
+
* (`plugins.item`) and renders the Settings → Plugins inventory tab itself,
|
|
336
|
+
* so only the tab seat is inside the settings panel the search reaches.
|
|
337
|
+
* `settings.plugin.item` and `web-ui.plugin.item` are the pre-0.2 card
|
|
338
|
+
* seats — 0.2 declares neither, and they stay indexed for older builds.
|
|
339
|
+
*/
|
|
297
340
|
const DEEP_SLOTS = [
|
|
298
341
|
{ slot: 'settings.plugins.tab', parent: 'plugins' },
|
|
299
342
|
{ slot: 'settings.plugin.item', parent: 'plugins' },
|
|
@@ -472,16 +515,192 @@ window.__ModuleLoader__.load({
|
|
|
472
515
|
|
|
473
516
|
const AI_TOGGLE_EVENT = 'dsh-settings-search:ai-toggle'
|
|
474
517
|
const AI_CONFIG_EVENT = 'dsh-settings-search:ai-config'
|
|
475
|
-
/**
|
|
518
|
+
/**
|
|
519
|
+
* Log recording has two sinks: the in-memory ring that feeds the panel
|
|
520
|
+
* instantly, and the host log file the panel can view and open. Every entry
|
|
521
|
+
* is formatted once ({@link logLine}), so the file, the viewer, and the
|
|
522
|
+
* exported text all read the same. Lines are handed to the host in small
|
|
523
|
+
* batches — a settings panel produces bursts, and one request per line
|
|
524
|
+
* would be wasteful — and the transport stays deliberately silent about its
|
|
525
|
+
* own failures so a broken channel can never log about itself.
|
|
526
|
+
*/
|
|
476
527
|
const LOG_EVENT = 'dsh-settings-search:log'
|
|
528
|
+
/** Fires when the on-disk entry count changes, so the panel's badge follows it. */
|
|
529
|
+
const LOG_COUNT_EVENT = 'dsh-settings-search:log-count'
|
|
477
530
|
const LOG_MAX = 500
|
|
531
|
+
const LOG_LINE_MAX = 800
|
|
532
|
+
const LOG_BATCH_MAX = 40
|
|
533
|
+
const LOG_FLUSH_MS = 1200
|
|
534
|
+
/** Document-relative form of the host half's `/api/settings-search-log`. */
|
|
535
|
+
const LOG_ROUTE = 'api/settings-search-log'
|
|
536
|
+
/** Per page-load run id, so one file can tell concurrent runs apart. */
|
|
537
|
+
const LOG_RUN = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, '0')
|
|
478
538
|
const LOGS = []
|
|
539
|
+
let logSeq = 0
|
|
540
|
+
|
|
541
|
+
const safeJson = (value) => {
|
|
542
|
+
try {
|
|
543
|
+
const text = JSON.stringify(value)
|
|
544
|
+
return typeof text === 'string' ? text : ''
|
|
545
|
+
} catch {
|
|
546
|
+
return '"<unserializable>"'
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** One line per entry: `time [level] [run:seq] message {meta}`. */
|
|
551
|
+
const logLine = (entry) => {
|
|
552
|
+
const meta = entry.meta === undefined ? '' : ` ${safeJson(entry.meta)}`
|
|
553
|
+
const seq = Number.isFinite(entry.seq) ? entry.seq : 0
|
|
554
|
+
return `${entry.time} [${entry.level}] [${LOG_RUN}:${seq}] ${entry.msg}${meta}`.slice(0, LOG_LINE_MAX)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** Host-log transport state: pending lines, its timer, and the last outcome. */
|
|
558
|
+
const logTransport = { pending: [], timer: null, state: 'idle', dir: '', file: '', detail: '', count: null }
|
|
559
|
+
|
|
560
|
+
const setLogOutcome = (failed, payload, detail) => {
|
|
561
|
+
logTransport.state = failed ? 'fail' : 'ok'
|
|
562
|
+
logTransport.detail = failed ? detail : ''
|
|
563
|
+
if (typeof payload?.dir === 'string') logTransport.dir = payload.dir
|
|
564
|
+
if (typeof payload?.file === 'string') logTransport.file = payload.file
|
|
565
|
+
// The host answers the on-disk entry count on every call, so the panel's
|
|
566
|
+
// badge reflects the log that exists rather than this page's ring.
|
|
567
|
+
if (Number.isFinite(payload?.count) && payload.count !== logTransport.count) {
|
|
568
|
+
logTransport.count = payload.count
|
|
569
|
+
try { window.dispatchEvent(new CustomEvent(LOG_COUNT_EVENT)) } catch {}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/** One request to the host log route; never throws, never logs. */
|
|
574
|
+
const postLog = async (body) => {
|
|
575
|
+
try {
|
|
576
|
+
const response = await fetch(LOG_ROUTE, {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'content-type': 'application/json' },
|
|
579
|
+
body: JSON.stringify(body),
|
|
580
|
+
})
|
|
581
|
+
const payload = await response.json().catch(() => null)
|
|
582
|
+
if (!response.ok || payload?.ok !== true) {
|
|
583
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
584
|
+
return null
|
|
585
|
+
}
|
|
586
|
+
setLogOutcome(false, payload, '')
|
|
587
|
+
return payload
|
|
588
|
+
} catch (error) {
|
|
589
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
590
|
+
return null
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Ship the queued lines now; the host stops serving once its batch is full. */
|
|
595
|
+
const flushLogs = () => {
|
|
596
|
+
if (logTransport.timer !== null) {
|
|
597
|
+
clearTimeout(logTransport.timer)
|
|
598
|
+
logTransport.timer = null
|
|
599
|
+
}
|
|
600
|
+
if (logTransport.pending.length === 0) return
|
|
601
|
+
const lines = logTransport.pending.splice(0, logTransport.pending.length)
|
|
602
|
+
void postLog({ action: 'append', lines })
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** Drop queued lines without shipping them (used by 清空日志). */
|
|
606
|
+
const discardPendingLogs = () => {
|
|
607
|
+
if (logTransport.timer !== null) {
|
|
608
|
+
clearTimeout(logTransport.timer)
|
|
609
|
+
logTransport.timer = null
|
|
610
|
+
}
|
|
611
|
+
logTransport.pending.length = 0
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const queueLogLine = (line) => {
|
|
615
|
+
logTransport.pending.push(line)
|
|
616
|
+
if (logTransport.pending.length >= LOG_BATCH_MAX) {
|
|
617
|
+
flushLogs()
|
|
618
|
+
return
|
|
619
|
+
}
|
|
620
|
+
if (logTransport.timer === null) logTransport.timer = setTimeout(flushLogs, LOG_FLUSH_MS)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/** Read the host log tail (viewer, and the folder path the panel offers). */
|
|
624
|
+
const readLogFile = async () => {
|
|
625
|
+
try {
|
|
626
|
+
const response = await fetch(LOG_ROUTE, { method: 'GET' })
|
|
627
|
+
const payload = await response.json().catch(() => null)
|
|
628
|
+
if (!response.ok || payload?.ok !== true) {
|
|
629
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
630
|
+
return null
|
|
631
|
+
}
|
|
632
|
+
setLogOutcome(false, payload, '')
|
|
633
|
+
return {
|
|
634
|
+
text: String(payload.text ?? ''),
|
|
635
|
+
size: Number(payload.size ?? 0),
|
|
636
|
+
truncated: payload.truncated === true,
|
|
637
|
+
}
|
|
638
|
+
} catch (error) {
|
|
639
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
640
|
+
return null
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* Refresh the on-disk entry count without transferring the file tail.
|
|
646
|
+
* @returns the count, or null when the host channel is unavailable.
|
|
647
|
+
*/
|
|
648
|
+
const readLogCount = async () => {
|
|
649
|
+
try {
|
|
650
|
+
const response = await fetch(`${LOG_ROUTE}?count=1`, { method: 'GET' })
|
|
651
|
+
const payload = await response.json().catch(() => null)
|
|
652
|
+
if (!response.ok || payload?.ok !== true) {
|
|
653
|
+
setLogOutcome(true, null, String(payload?.detail ?? payload?.reason ?? `HTTP ${response.status}`))
|
|
654
|
+
return null
|
|
655
|
+
}
|
|
656
|
+
setLogOutcome(false, payload, '')
|
|
657
|
+
return Number.isFinite(payload.count) ? payload.count : null
|
|
658
|
+
} catch (error) {
|
|
659
|
+
setLogOutcome(true, null, String(error?.message ?? error))
|
|
660
|
+
return null
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/** Open the log folder in the OS file manager. */
|
|
665
|
+
const openLogFolder = async () => {
|
|
666
|
+
const payload = await postLog({ action: 'open' })
|
|
667
|
+
return { ok: payload !== null, dir: logTransport.dir, file: logTransport.file, detail: logTransport.detail }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** Truncate the on-disk log. */
|
|
671
|
+
const clearLogFile = async () => { await postLog({ action: 'clear' }) }
|
|
672
|
+
|
|
673
|
+
/** Current host-log target and last transport outcome, for the panel. */
|
|
674
|
+
const logTarget = () => ({
|
|
675
|
+
dir: logTransport.dir,
|
|
676
|
+
file: logTransport.file,
|
|
677
|
+
state: logTransport.state,
|
|
678
|
+
detail: logTransport.detail,
|
|
679
|
+
count: logTransport.count,
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
/** This session's entries in the same one-line format the file uses. */
|
|
683
|
+
const sessionLogText = () => LOGS.map(logLine).join('\n')
|
|
684
|
+
|
|
479
685
|
const log = (level, msg, meta) => {
|
|
480
686
|
if (typeof window === 'undefined') return
|
|
481
|
-
|
|
687
|
+
const entry = { time: new Date().toISOString(), level, msg: String(msg), meta, seq: ++logSeq }
|
|
688
|
+
LOGS.push(entry)
|
|
482
689
|
if (LOGS.length > LOG_MAX) LOGS.splice(0, LOGS.length - LOG_MAX)
|
|
690
|
+
queueLogLine(logLine(entry))
|
|
483
691
|
try { window.dispatchEvent(new CustomEvent(LOG_EVENT, { detail: LOGS.length })) } catch {}
|
|
484
692
|
}
|
|
693
|
+
|
|
694
|
+
if (typeof window !== 'undefined') {
|
|
695
|
+
// A page leaving the document is the last chance to ship queued lines.
|
|
696
|
+
try { window.addEventListener('pagehide', flushLogs) } catch {}
|
|
697
|
+
try {
|
|
698
|
+
window.addEventListener('visibilitychange', () => {
|
|
699
|
+
if (typeof document !== 'undefined' && document.visibilityState === 'hidden') flushLogs()
|
|
700
|
+
})
|
|
701
|
+
} catch {}
|
|
702
|
+
}
|
|
703
|
+
|
|
485
704
|
const formatLogs = (logs) => {
|
|
486
705
|
const lines = [
|
|
487
706
|
'dsh-settings-search log',
|
|
@@ -490,9 +709,7 @@ window.__ModuleLoader__.load({
|
|
|
490
709
|
`count: ${logs.length}`,
|
|
491
710
|
'',
|
|
492
711
|
]
|
|
493
|
-
for (const entry of logs)
|
|
494
|
-
lines.push(`[${entry.time}] [${entry.level}] ${entry.msg}${entry.meta ? ' ' + JSON.stringify(entry.meta) : ''}`)
|
|
495
|
-
}
|
|
712
|
+
for (const entry of logs) lines.push(logLine(entry))
|
|
496
713
|
return lines.join('\n')
|
|
497
714
|
}
|
|
498
715
|
const AI_CACHE = new Map()
|
|
@@ -959,7 +1176,7 @@ window.__ModuleLoader__.load({
|
|
|
959
1176
|
.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
1177
|
.sss-trigger:hover { border-color: var(--dsw-alias-label-dimmed); color: var(--dsw-alias-label-secondary); }
|
|
961
1178
|
.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%)); }
|
|
1179
|
+
.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
1180
|
.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
1181
|
.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
1182
|
.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 +1237,7 @@ window.__ModuleLoader__.load({
|
|
|
1020
1237
|
.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
1238
|
.sss-vr-link:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
1022
1239
|
.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; }
|
|
1240
|
+
.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
1241
|
.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
1242
|
.sss-wl-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
1026
1243
|
.sss-wl-headText { flex: 1; min-width: 0; }
|
|
@@ -1041,7 +1258,7 @@ window.__ModuleLoader__.load({
|
|
|
1041
1258
|
.sss-wl-primary:hover { color: var(--dsw-alias-bg-layer-3); border-color: transparent; }
|
|
1042
1259
|
.sss-wl-skip { margin-left: auto; border: none; background: transparent; color: var(--dsw-alias-label-tertiary); padding: 7px 4px; }
|
|
1043
1260
|
.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; }
|
|
1261
|
+
.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
1262
|
.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
1263
|
.sss-cf-header { display: flex; align-items: flex-start; gap: 12px; }
|
|
1047
1264
|
.sss-cf-headText { flex: 1; min-width: 0; }
|
|
@@ -1052,6 +1269,15 @@ window.__ModuleLoader__.load({
|
|
|
1052
1269
|
.sss-cf-actions { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 18px; }
|
|
1053
1270
|
.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
1271
|
.sss-cf-btn:hover { color: var(--dsw-alias-label-primary); border-color: var(--dsw-alias-label-dimmed); }
|
|
1272
|
+
.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; }
|
|
1273
|
+
.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; }
|
|
1274
|
+
.sss-lv-header { flex: none; display: flex; align-items: center; gap: 10px; }
|
|
1275
|
+
.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; }
|
|
1276
|
+
.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; }
|
|
1277
|
+
.sss-lv-close:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
|
|
1278
|
+
.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; }
|
|
1279
|
+
.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; }
|
|
1280
|
+
.sss-lv-actions { flex: none; display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
|
|
1055
1281
|
.sss-cf-danger { background: #c0392b; color: #fff; border-color: transparent; }
|
|
1056
1282
|
.sss-cf-danger:hover { background: #a93226; color: #fff; border-color: transparent; }
|
|
1057
1283
|
`
|
|
@@ -1134,6 +1360,7 @@ window.__ModuleLoader__.load({
|
|
|
1134
1360
|
String(row.id ?? row.name ?? ''),
|
|
1135
1361
|
String(row.npm ?? row.package ?? row.packageName ?? ''),
|
|
1136
1362
|
String(row.version ?? ''),
|
|
1363
|
+
row.enabled === undefined ? '' : String(row.enabled),
|
|
1137
1364
|
].sort()).sort())
|
|
1138
1365
|
}
|
|
1139
1366
|
|
|
@@ -1453,18 +1680,27 @@ window.__ModuleLoader__.load({
|
|
|
1453
1680
|
: a.order - b.order)
|
|
1454
1681
|
}
|
|
1455
1682
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1683
|
+
/**
|
|
1684
|
+
* Fingerprint of the seats the index is built from: every settings page
|
|
1685
|
+
* plus each in-page seat this build declares. It is the change signal for
|
|
1686
|
+
* compositions that expose no plugin inventory at all (see
|
|
1687
|
+
* hasPluginInventory), so pages and rows appearing or disappearing still
|
|
1688
|
+
* invalidate a stored index. The pre-0.2 card seats alone would be empty
|
|
1689
|
+
* on DSH 0.2, which declares neither.
|
|
1690
|
+
*/
|
|
1691
|
+
const slotsFingerprint = () => {
|
|
1692
|
+
const rows = []
|
|
1693
|
+
for (const slot of ['settings.section', ...DEEP_SLOTS.map((config) => config.slot)]) {
|
|
1694
|
+
for (const entry of sortEntries(entriesOf(slot))) {
|
|
1695
|
+
rows.push([
|
|
1696
|
+
slot,
|
|
1697
|
+
String(entry.options?.id ?? entry.options?.key ?? ''),
|
|
1698
|
+
optionLabel(entry.options?.label),
|
|
1699
|
+
])
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
return JSON.stringify(rows.sort())
|
|
1703
|
+
}
|
|
1468
1704
|
|
|
1469
1705
|
const resetHarvest = () => { harvest.clear(); warmed = false }
|
|
1470
1706
|
|
|
@@ -1638,6 +1874,130 @@ window.__ModuleLoader__.load({
|
|
|
1638
1874
|
return { jumpToPage, jumpToOption }
|
|
1639
1875
|
}
|
|
1640
1876
|
|
|
1877
|
+
/**
|
|
1878
|
+
* Resolve the running DSH build's settings/LLM read faces.
|
|
1879
|
+
*
|
|
1880
|
+
* DSH 0.2 (the official desktop and `dsh web` release) exposes host
|
|
1881
|
+
* capabilities as Typert Remote namespaces: `remote.settings.describe()`
|
|
1882
|
+
* and `remote.llm.listConfigurableProviders()` each resolve to
|
|
1883
|
+
* `{ ok: true, value } | { ok: false, error }`. Older builds exposed the
|
|
1884
|
+
* same data through the unary client carried by the connection service
|
|
1885
|
+
* (`connection.api.settings.describe({})`, resolving to
|
|
1886
|
+
* `{ result: { ok, value } }`). Both generations answer with the same
|
|
1887
|
+
* namespace views, so one small face is resolved per call.
|
|
1888
|
+
*
|
|
1889
|
+
* @param ctx - the client plugin context.
|
|
1890
|
+
* @returns the read face, or null when this build offers neither generation.
|
|
1891
|
+
*/
|
|
1892
|
+
const resolveDshModelFace = (ctx) => {
|
|
1893
|
+
const remoteSettings = ctx.get('remote.settings')
|
|
1894
|
+
if (remoteSettings !== undefined && remoteSettings !== null) {
|
|
1895
|
+
const remoteLlm = ctx.get('remote.llm')
|
|
1896
|
+
return {
|
|
1897
|
+
describe: async () => {
|
|
1898
|
+
const response = await remoteSettings.describe()
|
|
1899
|
+
if (!response?.ok) throw new Error(response?.error?.message || 'settings.describe failed')
|
|
1900
|
+
return response.value
|
|
1901
|
+
},
|
|
1902
|
+
configuredProviders: async () => {
|
|
1903
|
+
if (typeof remoteLlm?.listConfigurableProviders !== 'function') return null
|
|
1904
|
+
const response = await remoteLlm.listConfigurableProviders().catch(() => null)
|
|
1905
|
+
return response?.ok ? response.value : null
|
|
1906
|
+
},
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
const legacyApi = ctx.get('connection')?.api
|
|
1910
|
+
if (legacyApi?.settings?.describe) {
|
|
1911
|
+
return {
|
|
1912
|
+
describe: async () => {
|
|
1913
|
+
const response = await legacyApi.settings.describe({})
|
|
1914
|
+
if (!response?.result?.ok) throw new Error(response?.result?.error?.message || 'settings.describe failed')
|
|
1915
|
+
return response.result.value
|
|
1916
|
+
},
|
|
1917
|
+
configuredProviders: async () => {
|
|
1918
|
+
if (typeof legacyApi.llm?.providers !== 'function') return null
|
|
1919
|
+
const response = await legacyApi.llm.providers({}).catch(() => null)
|
|
1920
|
+
return response?.result?.ok ? response.result.value?.providers ?? null : null
|
|
1921
|
+
},
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
return null
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
/** DSH's built-in DeepSeek routes and the namespaces that own them. */
|
|
1928
|
+
const DEEPSEEK_PROVIDERS = new Set(['deepseek-official', 'deepseek-account'])
|
|
1929
|
+
const DEEPSEEK_SETTINGS = new Set(['llm-deepseek', 'llm-deepseek-api-key', 'llm-deepseek-account'])
|
|
1930
|
+
/** The endpoint the shipped DeepSeek adapter falls back to (`PUBLIC_BASE_URL`). */
|
|
1931
|
+
const DEEPSEEK_PUBLIC_BASE_URL = 'https://api.deepseek.com/anthropic'
|
|
1932
|
+
/**
|
|
1933
|
+
* Whether one provider route is a built-in DeepSeek one.
|
|
1934
|
+
* @param providerId - the route's provider id.
|
|
1935
|
+
* @param settingsNs - the settings namespace the route reads, when known.
|
|
1936
|
+
* @returns true for the shipped DeepSeek routes.
|
|
1937
|
+
*/
|
|
1938
|
+
const isBuiltInDeepseekRoute = (providerId, settingsNs) =>
|
|
1939
|
+
DEEPSEEK_PROVIDERS.has(String(providerId ?? '')) || DEEPSEEK_SETTINGS.has(String(settingsNs ?? ''))
|
|
1940
|
+
|
|
1941
|
+
/**
|
|
1942
|
+
* Read the model fields of DSH's current default model; credentials are
|
|
1943
|
+
* never requested. `agent-default-model` names the provider and the model,
|
|
1944
|
+
* the provider's own settings namespace carries the Base URL, and the model
|
|
1945
|
+
* profile inside that namespace may override the wire protocol.
|
|
1946
|
+
*
|
|
1947
|
+
* A built-in DeepSeek route keeps its endpoint in the adapter rather than in
|
|
1948
|
+
* settings, so nothing is importable from the namespace there. Those routes
|
|
1949
|
+
* fall back to the endpoint DSH itself calls — the adapter resolves
|
|
1950
|
+
* `config.baseURL ?? $DEEPSEEK_BASE_URL ?? https://api.deepseek.com/anthropic`,
|
|
1951
|
+
* and only the last of those is visible to a browser — so importing the
|
|
1952
|
+
* default DeepSeek model still yields a usable address.
|
|
1953
|
+
*
|
|
1954
|
+
* @param ctx - the client plugin context.
|
|
1955
|
+
* @param t - translate function for the failure messages.
|
|
1956
|
+
* @returns `{ provider, baseUrl, model, builtin? }`, or null when DSH has no default model.
|
|
1957
|
+
* @throws when this build exposes no settings API, or the default model has no Base URL.
|
|
1958
|
+
*/
|
|
1959
|
+
const readDshModelConfig = async (ctx, t) => {
|
|
1960
|
+
const face = resolveDshModelFace(ctx)
|
|
1961
|
+
if (face === null) throw new Error(t('aiDshImportApi'))
|
|
1962
|
+
const described = await face.describe()
|
|
1963
|
+
const namespaces = new Map((described?.namespaces ?? []).map((view) => [view.ns, view]))
|
|
1964
|
+
const defaultModel = namespaces.get('agent-default-model')?.value ?? {}
|
|
1965
|
+
const providerId = String(defaultModel.provider ?? '').trim()
|
|
1966
|
+
const modelId = String(defaultModel.model ?? '').trim()
|
|
1967
|
+
if (!modelId) return null
|
|
1968
|
+
const readPath = (value, path) => (path ?? []).reduce((current, key) => current?.[key], value)
|
|
1969
|
+
const providers = await face.configuredProviders()
|
|
1970
|
+
const providerRow = Array.isArray(providers) ? providers.find((entry) => entry.provider === providerId) : undefined
|
|
1971
|
+
const modelNamespace = namespaces.get(providerRow?.settingsNs || 'llm-pi-ai')
|
|
1972
|
+
const providerProfile = providerRow
|
|
1973
|
+
? readPath(modelNamespace?.value, providerRow.settingsPath) ?? {}
|
|
1974
|
+
: modelNamespace?.value?.providers?.[providerId] ?? {}
|
|
1975
|
+
const configuredModels = Array.isArray(providerProfile.models) ? providerProfile.models : []
|
|
1976
|
+
const modelProfile = configuredModels.find((entry) => entry?.id === modelId) ?? {}
|
|
1977
|
+
// The built-in DeepSeek routes fix their protocol in the adapter: whatever
|
|
1978
|
+
// address they use, they speak Messages.
|
|
1979
|
+
const deepseekRoute = isBuiltInDeepseekRoute(providerId, providerRow?.settingsNs)
|
|
1980
|
+
let protocol = String(modelProfile.api ?? providerProfile.api ?? '').trim()
|
|
1981
|
+
let baseUrl = String(providerProfile.baseURL ?? '').trim()
|
|
1982
|
+
if (deepseekRoute && protocol === '') protocol = 'anthropic-messages'
|
|
1983
|
+
let builtin = false
|
|
1984
|
+
if (baseUrl === '' && deepseekRoute) {
|
|
1985
|
+
// Nothing is importable from settings, so mirror the endpoint the
|
|
1986
|
+
// adapter itself falls back to (`PUBLIC_BASE_URL`).
|
|
1987
|
+
baseUrl = DEEPSEEK_PUBLIC_BASE_URL
|
|
1988
|
+
builtin = true
|
|
1989
|
+
}
|
|
1990
|
+
if (!baseUrl) throw new Error(t('aiDshImportNoBaseUrl').replace('{model}', modelId))
|
|
1991
|
+
return {
|
|
1992
|
+
provider: providerId.toLowerCase() === 'ollama' || protocol === 'ollama'
|
|
1993
|
+
? 'ollama'
|
|
1994
|
+
: protocol === 'anthropic-messages' ? 'anthropic' : 'openai',
|
|
1995
|
+
baseUrl,
|
|
1996
|
+
model: modelId,
|
|
1997
|
+
...builtin ? { builtin: true } : {},
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1641
2001
|
function apply(ctx) {
|
|
1642
2002
|
log('info', '应用装载', { version: PKG_VERSION })
|
|
1643
2003
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'settings-search: dictionaries')
|
|
@@ -1650,65 +2010,101 @@ window.__ModuleLoader__.load({
|
|
|
1650
2010
|
const model = createModel(ctx)
|
|
1651
2011
|
const indexControls = { resetHarvest: model.resetHarvest, persistIndex: model.persistIndex }
|
|
1652
2012
|
const navigator = createNavigator(ctx)
|
|
1653
|
-
let
|
|
2013
|
+
let pluginInventoryToken = 0
|
|
2014
|
+
|
|
2015
|
+
/** Apply one plugin inventory as the index fingerprint, invalidating on change. */
|
|
2016
|
+
const applyPluginFingerprint = (rows) => {
|
|
2017
|
+
if (!Array.isArray(rows) || rows.length === 0) return
|
|
2018
|
+
if (model.applyIndexFingerprint('manager:' + otherPluginFingerprint(rows))) {
|
|
2019
|
+
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
2020
|
+
log('info', '检测到其它插件变化,将重建设置索引', {})
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1654
2023
|
|
|
1655
|
-
|
|
2024
|
+
/** Legacy (pre-0.2) client plugin manager service: `list()` plus change events. */
|
|
2025
|
+
const syncLegacyPluginManagerIndex = async (face) => {
|
|
1656
2026
|
if (face === null || !face.isLoopback) return
|
|
1657
|
-
const token = ++
|
|
2027
|
+
const token = ++pluginInventoryToken
|
|
1658
2028
|
try {
|
|
1659
2029
|
const rows = await face.list()
|
|
1660
|
-
if (token !==
|
|
1661
|
-
|
|
1662
|
-
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1663
|
-
log('info', '检测到其它插件变化,将重建设置索引', {})
|
|
1664
|
-
}
|
|
2030
|
+
if (token !== pluginInventoryToken) return
|
|
2031
|
+
applyPluginFingerprint(rows)
|
|
1665
2032
|
} catch {
|
|
1666
2033
|
// Keep the last trusted fingerprint when manager inspection fails.
|
|
1667
2034
|
}
|
|
1668
2035
|
}
|
|
1669
2036
|
|
|
2037
|
+
/**
|
|
2038
|
+
* Read the installed plugin inventory from DSH 0.2's `remote.pluginManager`
|
|
2039
|
+
* namespace. Both calls answer `{ ok, value }`: bundles carry `name` and
|
|
2040
|
+
* `version`, plugin rows carry `entryId`/`moduleName` and a fiber phase.
|
|
2041
|
+
*
|
|
2042
|
+
* @param face - the `remote.pluginManager` service.
|
|
2043
|
+
* @returns inventory rows for otherPluginFingerprint().
|
|
2044
|
+
*/
|
|
2045
|
+
const readRemotePluginInventory = async (face) => {
|
|
2046
|
+
const [bundles, plugins] = await Promise.all([
|
|
2047
|
+
typeof face?.listBundles === 'function' ? face.listBundles().catch(() => null) : null,
|
|
2048
|
+
typeof face?.listPlugins === 'function' ? face.listPlugins().catch(() => null) : null,
|
|
2049
|
+
])
|
|
2050
|
+
const rows = []
|
|
2051
|
+
if (bundles?.ok === true) {
|
|
2052
|
+
for (const row of bundles.value ?? []) rows.push({ id: row?.name, npm: row?.name, version: row?.version, enabled: row?.enabled })
|
|
2053
|
+
}
|
|
2054
|
+
if (plugins?.ok === true) {
|
|
2055
|
+
for (const row of plugins.value ?? []) rows.push({ id: row?.entryId, npm: row?.moduleName, enabled: row?.enabled })
|
|
2056
|
+
}
|
|
2057
|
+
return rows
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
const syncRemotePluginInventory = async (face) => {
|
|
2061
|
+
const token = ++pluginInventoryToken
|
|
2062
|
+
try {
|
|
2063
|
+
const rows = await readRemotePluginInventory(face)
|
|
2064
|
+
if (token !== pluginInventoryToken) return
|
|
2065
|
+
applyPluginFingerprint(rows)
|
|
2066
|
+
} catch {
|
|
2067
|
+
// Keep the last trusted fingerprint when the inventory read fails.
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
/**
|
|
2072
|
+
* Whether this build exposes a plugin inventory to watch: the 0.2 Remote
|
|
2073
|
+
* namespace or the legacy client service. Without one the seat fingerprint
|
|
2074
|
+
* is the only change signal.
|
|
2075
|
+
*/
|
|
2076
|
+
const hasPluginInventory = () => {
|
|
2077
|
+
const remote = ctx.get('remote.pluginManager')
|
|
2078
|
+
if (remote !== undefined && remote !== null) return true
|
|
2079
|
+
const legacy = ctx.get('pluginManager')
|
|
2080
|
+
return legacy !== undefined && legacy !== null
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
/**
|
|
2084
|
+
* Re-check the DSH 0.2 inventory. The Remote namespace has no change event,
|
|
2085
|
+
* so the check runs when the namespace arrives and whenever the settings
|
|
2086
|
+
* panel mounts (installs and updates land between the two).
|
|
2087
|
+
*/
|
|
2088
|
+
const refreshPluginInventory = () => {
|
|
2089
|
+
const face = ctx.get('remote.pluginManager')
|
|
2090
|
+
if (face !== undefined && face !== null) void syncRemotePluginInventory(face)
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
ctx.inject(['remote.pluginManager'], (inner) => {
|
|
2094
|
+
void syncRemotePluginInventory(inner.get('remote.pluginManager'))
|
|
2095
|
+
return () => { pluginInventoryToken += 1 }
|
|
2096
|
+
}, 'settings-search: remote plugin inventory fingerprint')
|
|
2097
|
+
|
|
1670
2098
|
ctx.inject(['pluginManager'], (inner) => {
|
|
1671
2099
|
const face = inner.pluginManager ?? null
|
|
1672
|
-
const sync = () => { void
|
|
2100
|
+
const sync = () => { void syncLegacyPluginManagerIndex(face) }
|
|
1673
2101
|
sync()
|
|
1674
2102
|
const unsubscribe = typeof face?.onChange === 'function' ? face.onChange(sync) : null
|
|
1675
2103
|
return () => {
|
|
1676
|
-
|
|
2104
|
+
pluginInventoryToken += 1
|
|
1677
2105
|
if (typeof unsubscribe === 'function') unsubscribe()
|
|
1678
2106
|
}
|
|
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
|
-
}
|
|
2107
|
+
}, 'settings-search: plugin manager index fingerprint (legacy)')
|
|
1712
2108
|
|
|
1713
2109
|
const SearchIcon = React.createElement('svg', {
|
|
1714
2110
|
className: 'sss-icon',
|
|
@@ -1790,6 +2186,36 @@ window.__ModuleLoader__.load({
|
|
|
1790
2186
|
])
|
|
1791
2187
|
}
|
|
1792
2188
|
|
|
2189
|
+
/**
|
|
2190
|
+
* Read-only log viewer. Shows the host log tail when the host answered and
|
|
2191
|
+
* the current session's ring otherwise, with the file path (or the reason
|
|
2192
|
+
* the file is unavailable) spelled out above the text.
|
|
2193
|
+
*/
|
|
2194
|
+
function LogViewer({ t, view }) {
|
|
2195
|
+
return React.createElement('div', {
|
|
2196
|
+
className: 'sss-lv-overlay',
|
|
2197
|
+
onMouseDown: (event) => { if (event.target === event.currentTarget) view.onClose() },
|
|
2198
|
+
}, [
|
|
2199
|
+
React.createElement('div', { className: 'sss-lv', role: 'dialog', 'aria-modal': 'true' }, [
|
|
2200
|
+
React.createElement('div', { className: 'sss-lv-header' }, [
|
|
2201
|
+
React.createElement('span', { className: 'sss-lv-title' }, t('logViewerTitle')),
|
|
2202
|
+
React.createElement('button', { className: 'sss-lv-close', type: 'button', 'aria-label': t('logViewerClose'), onClick: () => view.onClose() }, '✕'),
|
|
2203
|
+
]),
|
|
2204
|
+
React.createElement('p', { className: 'sss-lv-meta' }, view.metaText),
|
|
2205
|
+
view.truncated ? React.createElement('p', { className: 'sss-lv-meta' }, t('logViewerTruncated')) : null,
|
|
2206
|
+
React.createElement('pre', { className: 'sss-lv-body' }, view.text === '' ? t('logViewerEmpty') : view.text),
|
|
2207
|
+
React.createElement('div', { className: 'sss-lv-actions' }, [
|
|
2208
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: view.loading, onClick: () => view.onRefresh() },
|
|
2209
|
+
view.loading ? t('logViewerLoading') : t('logViewerRefresh')),
|
|
2210
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => view.onOpenFolder() }, t('logOpenDir')),
|
|
2211
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => view.onCopy() },
|
|
2212
|
+
view.copied ? t('logViewerCopied') : t('logViewerCopy')),
|
|
2213
|
+
React.createElement('button', { className: 'sss-vr-btn sss-vr-primary', type: 'button', onClick: () => view.onClose() }, t('logViewerClose')),
|
|
2214
|
+
]),
|
|
2215
|
+
]),
|
|
2216
|
+
])
|
|
2217
|
+
}
|
|
2218
|
+
|
|
1793
2219
|
function SettingsHeaderSearch() {
|
|
1794
2220
|
const [query, setQuery] = React.useState('')
|
|
1795
2221
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1863,11 +2289,14 @@ window.__ModuleLoader__.load({
|
|
|
1863
2289
|
}, [])
|
|
1864
2290
|
|
|
1865
2291
|
React.useEffect(() => {
|
|
1866
|
-
//
|
|
1867
|
-
//
|
|
1868
|
-
|
|
2292
|
+
// DSH 0.2 answers the plugin inventory through `remote.pluginManager`
|
|
2293
|
+
// with no change event, so re-check it every time the panel mounts.
|
|
2294
|
+
refreshPluginInventory()
|
|
2295
|
+
// Without any plugin inventory, the seats the index was built from are
|
|
2296
|
+
// the only available change signal: re-apply them on every mount, so a
|
|
2297
|
+
// page or row that appeared since the last visit invalidates the index.
|
|
1869
2298
|
const fallback = setTimeout(() => {
|
|
1870
|
-
if (
|
|
2299
|
+
if (hasPluginInventory()) return
|
|
1871
2300
|
if (model.applyIndexFingerprint(model.slotsFingerprint())) {
|
|
1872
2301
|
window.dispatchEvent(new CustomEvent(INDEX_INVALID_EVENT))
|
|
1873
2302
|
}
|
|
@@ -2361,6 +2790,10 @@ window.__ModuleLoader__.load({
|
|
|
2361
2790
|
const [logCopyFail, setLogCopyFail] = React.useState(false)
|
|
2362
2791
|
const [logExportFail, setLogExportFail] = React.useState(false)
|
|
2363
2792
|
const [logCleared, setLogCleared] = React.useState(false)
|
|
2793
|
+
const [logViewer, setLogViewer] = React.useState(false)
|
|
2794
|
+
const [logViewerState, setLogViewerState] = React.useState({ status: 'idle', text: '', size: 0, truncated: false })
|
|
2795
|
+
const [logViewerCopied, setLogViewerCopied] = React.useState(false)
|
|
2796
|
+
const [logFolderMsg, setLogFolderMsg] = React.useState('')
|
|
2364
2797
|
const [indexState, setIndexState] = React.useState('idle')
|
|
2365
2798
|
const [warming, setWarming] = React.useState(false)
|
|
2366
2799
|
const [confirmClear, setConfirmClear] = React.useState(false)
|
|
@@ -2399,12 +2832,19 @@ window.__ModuleLoader__.load({
|
|
|
2399
2832
|
React.useEffect(() => {
|
|
2400
2833
|
const sync = () => setLogVersion((v) => v + 1)
|
|
2401
2834
|
window.addEventListener(LOG_EVENT, sync)
|
|
2402
|
-
|
|
2835
|
+
window.addEventListener(LOG_COUNT_EVENT, sync)
|
|
2836
|
+
// The badge counts what exists on disk, including earlier runs.
|
|
2837
|
+
void readLogCount()
|
|
2838
|
+
return () => {
|
|
2839
|
+
window.removeEventListener(LOG_EVENT, sync)
|
|
2840
|
+
window.removeEventListener(LOG_COUNT_EVENT, sync)
|
|
2841
|
+
}
|
|
2403
2842
|
}, [])
|
|
2404
2843
|
|
|
2405
2844
|
useAutoReset(logCopied, () => setLogCopied(false))
|
|
2406
2845
|
useAutoReset(logCleared, () => setLogCleared(false))
|
|
2407
2846
|
useAutoReset(logExportFail, () => setLogExportFail(false))
|
|
2847
|
+
useAutoReset(logViewerCopied, () => setLogViewerCopied(false))
|
|
2408
2848
|
|
|
2409
2849
|
const changeProvider = (next) => {
|
|
2410
2850
|
providerDrafts.current[provider] = { baseUrl, model }
|
|
@@ -2493,17 +2933,20 @@ window.__ModuleLoader__.load({
|
|
|
2493
2933
|
setDshImportState('loading')
|
|
2494
2934
|
setDshImportMsg('')
|
|
2495
2935
|
try {
|
|
2496
|
-
const imported = await readDshModelConfig()
|
|
2936
|
+
const imported = await readDshModelConfig(ctx, t)
|
|
2497
2937
|
if (!imported) throw new Error(t('aiDshImportEmpty'))
|
|
2938
|
+
const { builtin, ...config } = imported
|
|
2498
2939
|
providerDrafts.current[provider] = { baseUrl, model }
|
|
2499
|
-
setAiConfig({ ...
|
|
2500
|
-
setProvider(
|
|
2501
|
-
setBaseUrl(
|
|
2502
|
-
setModel(
|
|
2940
|
+
setAiConfig({ ...config, apiKey })
|
|
2941
|
+
setProvider(config.provider)
|
|
2942
|
+
setBaseUrl(config.baseUrl)
|
|
2943
|
+
setModel(config.model)
|
|
2503
2944
|
resetModelList()
|
|
2504
2945
|
setDshImportState('ok')
|
|
2505
|
-
|
|
2506
|
-
|
|
2946
|
+
// A built-in DeepSeek route has no stored address and no API key of
|
|
2947
|
+
// its own, so the note says where the address came from.
|
|
2948
|
+
setDshImportMsg(builtin === true ? t('aiDshImportBuiltin') : t('aiDshImportOk'))
|
|
2949
|
+
log('info', '导入 DSH 模型配置', { provider: config.provider, model: config.model, builtin: builtin === true })
|
|
2507
2950
|
} catch (error) {
|
|
2508
2951
|
const reason = String(error?.message ?? error).slice(0, 160)
|
|
2509
2952
|
setDshImportState('fail')
|
|
@@ -2632,8 +3075,64 @@ window.__ModuleLoader__.load({
|
|
|
2632
3075
|
const clearLogs = () => {
|
|
2633
3076
|
setConfirmClear(false)
|
|
2634
3077
|
LOGS.length = 0
|
|
3078
|
+
// Drop queued lines too: a late flush would resurrect what was cleared.
|
|
3079
|
+
discardPendingLogs()
|
|
2635
3080
|
setLogVersion((v) => v + 1)
|
|
2636
3081
|
setLogCleared(true)
|
|
3082
|
+
void clearLogFile()
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/** Read the on-disk tail for the viewer; fall back to this session's ring. */
|
|
3086
|
+
const refreshLogView = async () => {
|
|
3087
|
+
setLogViewerState((current) => ({ ...current, status: 'loading' }))
|
|
3088
|
+
const read = await readLogFile()
|
|
3089
|
+
if (read === null) {
|
|
3090
|
+
setLogViewerState({ status: 'fail', text: sessionLogText(), size: 0, truncated: false })
|
|
3091
|
+
return
|
|
3092
|
+
}
|
|
3093
|
+
setLogViewerState({ status: 'ok', text: read.text, size: read.size, truncated: read.truncated })
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
const openLogViewer = () => {
|
|
3097
|
+
flushLogs()
|
|
3098
|
+
setLogViewer(true)
|
|
3099
|
+
void refreshLogView()
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
const copyLogView = async () => {
|
|
3103
|
+
try {
|
|
3104
|
+
await navigator.clipboard.writeText(logViewerState.text)
|
|
3105
|
+
setLogViewerCopied(true)
|
|
3106
|
+
} catch {
|
|
3107
|
+
setLogCopyFail(true)
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
/** Open the on-disk log folder through the host half. */
|
|
3112
|
+
const openLogDir = async () => {
|
|
3113
|
+
flushLogs()
|
|
3114
|
+
setLogFolderMsg('')
|
|
3115
|
+
const result = await openLogFolder()
|
|
3116
|
+
setLogFolderMsg(result.ok
|
|
3117
|
+
? t('logOpenOk')
|
|
3118
|
+
: `${t('logOpenFail')}${result.detail === '' ? '' : `(${result.detail})`}`)
|
|
3119
|
+
}
|
|
3120
|
+
|
|
3121
|
+
/** Badge value: entries that exist on disk, or this session's ring when the host is unavailable. */
|
|
3122
|
+
const logBadgeCount = logTarget().count ?? LOGS.length
|
|
3123
|
+
|
|
3124
|
+
const logView = {
|
|
3125
|
+
text: logViewerState.text,
|
|
3126
|
+
truncated: logViewerState.truncated,
|
|
3127
|
+
loading: logViewerState.status === 'loading',
|
|
3128
|
+
copied: logViewerCopied,
|
|
3129
|
+
metaText: logViewerState.status === 'fail' || logTarget().file === ''
|
|
3130
|
+
? t('logViewerMemory')
|
|
3131
|
+
: t('logViewerFile').replace('{file}', logTarget().file),
|
|
3132
|
+
onClose: () => setLogViewer(false),
|
|
3133
|
+
onRefresh: () => { void refreshLogView() },
|
|
3134
|
+
onOpenFolder: () => { void openLogDir() },
|
|
3135
|
+
onCopy: () => { void copyLogView() },
|
|
2637
3136
|
}
|
|
2638
3137
|
|
|
2639
3138
|
const check = async () => {
|
|
@@ -2781,7 +3280,7 @@ window.__ModuleLoader__.load({
|
|
|
2781
3280
|
}, indexState === 'loading' || warming ? t('indexRebuilding') : t('indexRebuild')),
|
|
2782
3281
|
]),
|
|
2783
3282
|
indexState === 'done'
|
|
2784
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: '
|
|
3283
|
+
? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-success-primary)' } }, t('indexDone'))
|
|
2785
3284
|
: null,
|
|
2786
3285
|
]),
|
|
2787
3286
|
React.createElement('div', { className: 'sss-vr-field sss-vr-form' }, [
|
|
@@ -2848,7 +3347,7 @@ window.__ModuleLoader__.load({
|
|
|
2848
3347
|
modelsMsg
|
|
2849
3348
|
? React.createElement('p', {
|
|
2850
3349
|
className: modelsState === 'fail' ? 'sss-vr-note' : 'sss-vr-hint',
|
|
2851
|
-
style: modelsState === 'fail' ? { color: 'var(--dsw-alias-
|
|
3350
|
+
style: modelsState === 'fail' ? { color: 'var(--dsw-alias-state-error-primary)' } : undefined,
|
|
2852
3351
|
}, modelsMsg)
|
|
2853
3352
|
: null,
|
|
2854
3353
|
provider !== 'ollama' ? React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
@@ -2878,10 +3377,10 @@ window.__ModuleLoader__.load({
|
|
|
2878
3377
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => requestClearAi() }, t('aiClear')),
|
|
2879
3378
|
]),
|
|
2880
3379
|
dshImportMsg
|
|
2881
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: dshImportState === 'ok' ? '
|
|
3380
|
+
? 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
3381
|
: null,
|
|
2883
3382
|
testMsg
|
|
2884
|
-
? React.createElement('p', { className: 'sss-vr-note', style: { color: testState === 'ok' ? '
|
|
3383
|
+
? 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
3384
|
: null,
|
|
2886
3385
|
]),
|
|
2887
3386
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
@@ -2907,7 +3406,7 @@ window.__ModuleLoader__.load({
|
|
|
2907
3406
|
copied ? t('updCopied') : t('updCopy')),
|
|
2908
3407
|
React.createElement('a', { className: 'sss-vr-link', href: releasesUrl, target: '_blank', rel: 'noopener noreferrer' }, t('updRelease')),
|
|
2909
3408
|
]),
|
|
2910
|
-
copyFailed ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-
|
|
3409
|
+
copyFailed ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('updCopyFail')) : null,
|
|
2911
3410
|
])
|
|
2912
3411
|
: null,
|
|
2913
3412
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
@@ -2925,23 +3424,28 @@ window.__ModuleLoader__.load({
|
|
|
2925
3424
|
React.createElement('div', { className: 'sss-vr-field' }, [
|
|
2926
3425
|
React.createElement('div', { className: 'sss-vr-field-head' }, [
|
|
2927
3426
|
React.createElement('span', { className: 'sss-vr-label' }, t('logTitle')),
|
|
2928
|
-
|
|
3427
|
+
logBadgeCount > 0
|
|
3428
|
+
? React.createElement('span', { className: 'sss-vr-badge', title: t('logCountHint') }, String(logBadgeCount))
|
|
3429
|
+
: null,
|
|
2929
3430
|
]),
|
|
2930
3431
|
React.createElement('p', { className: 'sss-vr-hint' }, t('logDesc')),
|
|
2931
|
-
LOGS.length === 0
|
|
3432
|
+
LOGS.length === 0 && logBadgeCount === 0
|
|
2932
3433
|
? React.createElement('p', { className: 'sss-vr-note' }, t('logEmpty'))
|
|
2933
3434
|
: null,
|
|
2934
3435
|
React.createElement('div', { className: 'sss-vr-actions' }, [
|
|
3436
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => openLogViewer() }, t('logView')),
|
|
3437
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void openLogDir() } }, t('logOpenDir')),
|
|
2935
3438
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void exportLogs() } }, t('logExport')),
|
|
2936
3439
|
React.createElement('a', { className: 'sss-vr-link', href: ISSUES_URL, target: '_blank', rel: 'noopener noreferrer' },
|
|
2937
3440
|
t('logIssues')),
|
|
2938
3441
|
React.createElement('button', { className: 'sss-vr-btn', type: 'button', onClick: () => { void copyLogs() } },
|
|
2939
3442
|
logCopied ? t('logCopied') : t('logCopy')),
|
|
2940
|
-
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled:
|
|
3443
|
+
React.createElement('button', { className: 'sss-vr-btn', type: 'button', disabled: logBadgeCount === 0, onClick: () => requestClear() },
|
|
2941
3444
|
logCleared ? t('logCleared') : t('logClear')),
|
|
2942
3445
|
]),
|
|
2943
|
-
|
|
2944
|
-
|
|
3446
|
+
logFolderMsg ? React.createElement('p', { className: 'sss-vr-note' }, logFolderMsg) : null,
|
|
3447
|
+
logCopyFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('logCopyFail')) : null,
|
|
3448
|
+
logExportFail ? React.createElement('p', { className: 'sss-vr-note', style: { color: 'var(--dsw-alias-state-error-primary)' } }, t('logExportFail')) : null,
|
|
2945
3449
|
]),
|
|
2946
3450
|
]) : null,
|
|
2947
3451
|
confirmClear
|
|
@@ -2955,6 +3459,9 @@ window.__ModuleLoader__.load({
|
|
|
2955
3459
|
onCancel: () => cancelClear(),
|
|
2956
3460
|
})
|
|
2957
3461
|
: null,
|
|
3462
|
+
logViewer
|
|
3463
|
+
? React.createElement(LogViewer, { t, view: logView })
|
|
3464
|
+
: null,
|
|
2958
3465
|
confirmClearAi
|
|
2959
3466
|
? React.createElement(ConfirmModal, {
|
|
2960
3467
|
t,
|