@lemoncat7/dsh-knowledge 1.1.5 → 1.2.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.md +2 -2
- package/docs/architecture.md +3 -3
- package/lib/api.d.ts +2 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +41 -12
- package/lib/api.js.map +1 -1
- package/lib/atomic-file.d.ts +16 -0
- package/lib/atomic-file.d.ts.map +1 -0
- package/lib/atomic-file.js +93 -0
- package/lib/atomic-file.js.map +1 -0
- package/lib/client.js +1 -1
- package/lib/client.js.map +2 -2
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +2 -5
- package/lib/config.js.map +1 -1
- package/lib/connection.d.ts.map +1 -1
- package/lib/connection.js +5 -15
- package/lib/connection.js.map +1 -1
- package/lib/documents/projection-queue.d.ts +10 -0
- package/lib/documents/projection-queue.d.ts.map +1 -0
- package/lib/documents/projection-queue.js +21 -0
- package/lib/documents/projection-queue.js.map +1 -0
- package/lib/documents/store.d.ts +4 -2
- package/lib/documents/store.d.ts.map +1 -1
- package/lib/documents/store.js +54 -73
- package/lib/documents/store.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +10 -2
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +85 -31
- package/lib/index.js.map +1 -1
- package/lib/knowledge-merge.d.ts +8 -0
- package/lib/knowledge-merge.d.ts.map +1 -0
- package/lib/knowledge-merge.js +19 -0
- package/lib/knowledge-merge.js.map +1 -0
- package/lib/local-provider.d.ts +2 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +100 -61
- package/lib/local-provider.js.map +1 -1
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +34 -2
- package/lib/remote-provider.js.map +1 -1
- package/lib/remote-url.d.ts +2 -0
- package/lib/remote-url.d.ts.map +1 -0
- package/lib/remote-url.js +14 -0
- package/lib/remote-url.js.map +1 -0
- package/lib/service-settings.d.ts.map +1 -1
- package/lib/service-settings.js +3 -10
- package/lib/service-settings.js.map +1 -1
- package/lib/tools.js +44 -1
- package/lib/tools.js.map +1 -1
- package/lib/web-change-review.d.ts +34 -0
- package/lib/web-change-review.d.ts.map +1 -0
- package/lib/web-change-review.js +147 -0
- package/lib/web-change-review.js.map +1 -0
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +1 -0
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +287 -81
- package/web/change-review.js +5 -0
- package/web/index.html +1 -0
- package/web/styles.css +100 -23
package/web/app.js
CHANGED
|
@@ -82,6 +82,10 @@ const state = {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
let scrollRestoreFrame = 0
|
|
85
|
+
let navigationController = null
|
|
86
|
+
let navigationRequest = 0
|
|
87
|
+
let navigationSave = Promise.resolve(true)
|
|
88
|
+
let libraryDetailRequest = 0
|
|
85
89
|
|
|
86
90
|
function installHostThemeBridge() {
|
|
87
91
|
if (window.parent === window) return Promise.resolve()
|
|
@@ -242,9 +246,12 @@ function badge(label, variant = '') {
|
|
|
242
246
|
}
|
|
243
247
|
|
|
244
248
|
function showToast(message, kind = '') {
|
|
245
|
-
const toast = element('div', { class: `toast ${kind}`.trim(), role: kind === 'error' ? 'alert' : 'status' },
|
|
249
|
+
const toast = element('div', { class: `toast ${kind}`.trim(), role: kind === 'error' ? 'alert' : 'status' },
|
|
250
|
+
element('span', {}, message),
|
|
251
|
+
kind === 'error' ? actionButton('关闭', () => toast.remove(), 'ghost small toast-close', { 'aria-label': '关闭错误提示' }) : null,
|
|
252
|
+
)
|
|
246
253
|
toastRegion.append(toast)
|
|
247
|
-
window.setTimeout(() => toast.remove(), 4200)
|
|
254
|
+
if (kind !== 'error') window.setTimeout(() => toast.remove(), 4200)
|
|
248
255
|
}
|
|
249
256
|
|
|
250
257
|
async function api(path, options = {}) {
|
|
@@ -342,6 +349,12 @@ function signOut() {
|
|
|
342
349
|
}
|
|
343
350
|
|
|
344
351
|
async function navigate(view) {
|
|
352
|
+
const request = ++navigationRequest
|
|
353
|
+
navigationSave = navigationSave.then(() => saveBeforeLeavingDocument(), () => saveBeforeLeavingDocument())
|
|
354
|
+
if (!await navigationSave || request !== navigationRequest) return
|
|
355
|
+
navigationController?.abort()
|
|
356
|
+
const controller = new AbortController()
|
|
357
|
+
navigationController = controller
|
|
345
358
|
const previousView = state.view
|
|
346
359
|
if (view === 'bases' && previousView !== 'bases' && state.knowledgeBaseView === 'detail') {
|
|
347
360
|
state.knowledgeBaseView = 'libraries'
|
|
@@ -352,35 +365,51 @@ async function navigate(view) {
|
|
|
352
365
|
state.error = ''
|
|
353
366
|
renderShell()
|
|
354
367
|
try {
|
|
355
|
-
if (view === 'overview') await loadOverview()
|
|
356
|
-
if (view === 'bases') await loadKnowledgeBasesPage()
|
|
357
|
-
if (view === 'entries') await loadDocuments()
|
|
358
|
-
if (view === 'candidates') await loadCandidates()
|
|
359
|
-
if (view === 'tokens') await loadTokens()
|
|
368
|
+
if (view === 'overview') await loadOverview(controller.signal)
|
|
369
|
+
if (view === 'bases') await loadKnowledgeBasesPage(controller.signal)
|
|
370
|
+
if (view === 'entries') await loadDocuments(controller.signal)
|
|
371
|
+
if (view === 'candidates') await loadCandidates(controller.signal)
|
|
372
|
+
if (view === 'tokens') await loadTokens(controller.signal)
|
|
360
373
|
} catch (error) {
|
|
374
|
+
if (controller.signal.aborted) return
|
|
361
375
|
if (error.status === 401 && AUTH_MODE === 'bearer') return signOut()
|
|
362
376
|
state.error = friendlyError(error)
|
|
363
377
|
} finally {
|
|
378
|
+
if (navigationController !== controller) return
|
|
364
379
|
state.loading = false
|
|
365
380
|
renderShell()
|
|
366
381
|
}
|
|
367
382
|
}
|
|
368
383
|
|
|
369
|
-
async function
|
|
370
|
-
|
|
384
|
+
async function saveBeforeLeavingDocument() {
|
|
385
|
+
const workspace = activeDocumentWorkspace()
|
|
386
|
+
const editor = workspace?.view.editor
|
|
387
|
+
if (!workspace || !editor?.dirty) return true
|
|
388
|
+
const emptyDraft = editor.isNew && !editor.title.trim() && !editor.body.trim()
|
|
389
|
+
if (emptyDraft) {
|
|
390
|
+
workspace.view.editor = null
|
|
391
|
+
workspace.view.documentId = ''
|
|
392
|
+
return true
|
|
393
|
+
}
|
|
394
|
+
return saveDocumentEditor(workspace)
|
|
371
395
|
}
|
|
372
396
|
|
|
373
|
-
async function
|
|
374
|
-
|
|
397
|
+
async function refreshStats(signal) {
|
|
398
|
+
state.stats = await api('stats', { signal })
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async function ensureKnowledgeBases(force = false, signal) {
|
|
402
|
+
if (force || state.knowledgeBases.length === 0) state.knowledgeBases = await api('knowledge-bases', { signal })
|
|
375
403
|
return state.knowledgeBases
|
|
376
404
|
}
|
|
377
405
|
|
|
378
|
-
async function loadKnowledgeBasesPage() {
|
|
379
|
-
const
|
|
406
|
+
async function loadKnowledgeBasesPage(signal) {
|
|
407
|
+
const options = { signal }
|
|
408
|
+
const requests = [api('knowledge-bases', options), api('mounts', options), api('settings', options), api('service', options)]
|
|
380
409
|
if (state.mountContext.sessionId) {
|
|
381
410
|
const params = new URLSearchParams({ sessionId: state.mountContext.sessionId })
|
|
382
411
|
if (state.mountContext.projectId) params.set('projectId', state.mountContext.projectId)
|
|
383
|
-
requests.push(api(`mounts/resolve?${params}
|
|
412
|
+
requests.push(api(`mounts/resolve?${params}`, options))
|
|
384
413
|
}
|
|
385
414
|
const [bases, mounts, settings, service, resolved = []] = await Promise.all(requests)
|
|
386
415
|
state.knowledgeBases = bases
|
|
@@ -388,15 +417,16 @@ async function loadKnowledgeBasesPage() {
|
|
|
388
417
|
state.settings = settings
|
|
389
418
|
state.service = service
|
|
390
419
|
state.resolvedMounts = resolved
|
|
391
|
-
await refreshStats()
|
|
420
|
+
await refreshStats(signal)
|
|
392
421
|
}
|
|
393
422
|
|
|
394
|
-
async function loadOverview() {
|
|
423
|
+
async function loadOverview(signal) {
|
|
424
|
+
const options = { signal }
|
|
395
425
|
const [stats, recent, pending, bases] = await Promise.all([
|
|
396
|
-
api('stats'),
|
|
397
|
-
api('entries?status=active&limit=6'),
|
|
398
|
-
api('candidates?status=pending&limit=5'),
|
|
399
|
-
api('knowledge-bases'),
|
|
426
|
+
api('stats', options),
|
|
427
|
+
api('entries?status=active&limit=6', options),
|
|
428
|
+
api('candidates?status=pending&limit=5', options),
|
|
429
|
+
api('knowledge-bases', options),
|
|
400
430
|
])
|
|
401
431
|
state.stats = stats
|
|
402
432
|
state.knowledgeBases = bases
|
|
@@ -427,12 +457,13 @@ async function loadEntries(cursor = '') {
|
|
|
427
457
|
if (!state.stats) await refreshStats()
|
|
428
458
|
}
|
|
429
459
|
|
|
430
|
-
async function loadDocuments() {
|
|
431
|
-
const
|
|
460
|
+
async function loadDocuments(signal) {
|
|
461
|
+
const options = { signal }
|
|
462
|
+
const requests = [api('knowledge-bases', options), api('documents', options)]
|
|
432
463
|
if (state.mountContext.sessionId) {
|
|
433
464
|
const params = new URLSearchParams({ sessionId: state.mountContext.sessionId })
|
|
434
465
|
if (state.mountContext.projectId) params.set('projectId', state.mountContext.projectId)
|
|
435
|
-
requests.push(api(`mounts/resolve?${params}
|
|
466
|
+
requests.push(api(`mounts/resolve?${params}`, options))
|
|
436
467
|
}
|
|
437
468
|
const [bases, documents, resolved = []] = await Promise.all(requests)
|
|
438
469
|
state.knowledgeBases = bases
|
|
@@ -448,9 +479,9 @@ async function loadDocuments() {
|
|
|
448
479
|
: documentKnowledgeBases(bases)[0]?.id || ''
|
|
449
480
|
}
|
|
450
481
|
selectDefaultDocument(workspace)
|
|
451
|
-
if (view.documentId) await loadDocumentEditor(workspace, view.documentId)
|
|
482
|
+
if (view.documentId) await loadDocumentEditor(workspace, view.documentId, signal)
|
|
452
483
|
else view.editor = null
|
|
453
|
-
if (!state.stats) await refreshStats()
|
|
484
|
+
if (!state.stats) await refreshStats(signal)
|
|
454
485
|
}
|
|
455
486
|
|
|
456
487
|
function documentKnowledgeBases(bases = state.knowledgeBases) {
|
|
@@ -496,14 +527,19 @@ function documentWorkspaceReadOnly(workspace) {
|
|
|
496
527
|
}
|
|
497
528
|
|
|
498
529
|
async function reloadDocumentWorkspace(workspace) {
|
|
530
|
+
const libraryBaseId = workspace.kind === 'library' ? state.libraryDetail.knowledgeBaseId : ''
|
|
499
531
|
const documents = workspace.kind === 'library'
|
|
500
|
-
? await api(`documents?knowledgeBaseId=${encodeURIComponent(
|
|
532
|
+
? await api(`documents?knowledgeBaseId=${encodeURIComponent(libraryBaseId)}`)
|
|
501
533
|
: await api('documents')
|
|
502
|
-
if (workspace.kind === 'library')
|
|
534
|
+
if (workspace.kind === 'library') {
|
|
535
|
+
if (state.libraryDetail.knowledgeBaseId !== libraryBaseId) return false
|
|
536
|
+
setDocumentWorkspaceDocuments(workspace, documents)
|
|
537
|
+
}
|
|
503
538
|
else {
|
|
504
539
|
const visibleBaseIds = new Set(documentKnowledgeBases().map(base => base.id))
|
|
505
540
|
setDocumentWorkspaceDocuments(workspace, documents.filter(document => visibleBaseIds.has(document.knowledgeBaseId)))
|
|
506
541
|
}
|
|
542
|
+
return true
|
|
507
543
|
}
|
|
508
544
|
|
|
509
545
|
function selectDefaultDocument(workspace) {
|
|
@@ -515,8 +551,11 @@ function selectDefaultDocument(workspace) {
|
|
|
515
551
|
if (view.knowledgeBaseId) view.expandedBases.add(view.knowledgeBaseId)
|
|
516
552
|
}
|
|
517
553
|
|
|
518
|
-
async function loadDocumentEditor(workspace, id) {
|
|
519
|
-
const
|
|
554
|
+
async function loadDocumentEditor(workspace, id, signal) {
|
|
555
|
+
const revision = (workspace.view.loadRevision || 0) + 1
|
|
556
|
+
workspace.view.loadRevision = revision
|
|
557
|
+
const entry = await api(`entries/${encodeURIComponent(id)}`, { signal })
|
|
558
|
+
if (workspace.view.loadRevision !== revision || workspace.view.documentId !== id) return false
|
|
520
559
|
workspace.view.mode = 'preview'
|
|
521
560
|
workspace.view.editor = {
|
|
522
561
|
...entry,
|
|
@@ -525,6 +564,7 @@ async function loadDocumentEditor(workspace, id) {
|
|
|
525
564
|
isNew: false,
|
|
526
565
|
saveState: '已保存',
|
|
527
566
|
}
|
|
567
|
+
return true
|
|
528
568
|
}
|
|
529
569
|
|
|
530
570
|
function createBlankDocument(workspace, baseId) {
|
|
@@ -557,8 +597,7 @@ async function selectDocument(workspace, id) {
|
|
|
557
597
|
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor(workspace)) return
|
|
558
598
|
workspace.view.documentId = id
|
|
559
599
|
workspace.view.treeOpen = false
|
|
560
|
-
await loadDocumentEditor(workspace, id)
|
|
561
|
-
renderShell()
|
|
600
|
+
if (await loadDocumentEditor(workspace, id)) renderShell()
|
|
562
601
|
}
|
|
563
602
|
|
|
564
603
|
function editorDraft(editor) {
|
|
@@ -615,26 +654,29 @@ function updateEditorSaveState(label) {
|
|
|
615
654
|
if (node) node.textContent = label
|
|
616
655
|
}
|
|
617
656
|
|
|
618
|
-
async function loadCandidates() {
|
|
657
|
+
async function loadCandidates(signal) {
|
|
619
658
|
const [candidates] = await Promise.all([
|
|
620
|
-
api(`candidates?status=${state.candidateStatus}&limit=100
|
|
621
|
-
ensureKnowledgeBases(),
|
|
659
|
+
api(`candidates?status=${state.candidateStatus}&limit=100`, { signal }),
|
|
660
|
+
ensureKnowledgeBases(false, signal),
|
|
622
661
|
])
|
|
623
662
|
state.candidates = candidates
|
|
624
663
|
const targetIds = [...new Set(candidates.map(candidate => candidate.targetId).filter(Boolean))]
|
|
625
664
|
const targets = await Promise.all(targetIds.map(async id => {
|
|
626
|
-
try { return [id, await api(`entries/${encodeURIComponent(id)}
|
|
627
|
-
catch {
|
|
665
|
+
try { return [id, await api(`entries/${encodeURIComponent(id)}`, { signal })] }
|
|
666
|
+
catch (error) {
|
|
667
|
+
if (signal?.aborted) throw error
|
|
668
|
+
return [id, null]
|
|
669
|
+
}
|
|
628
670
|
}))
|
|
629
671
|
state.candidateTargets = new Map(targets)
|
|
630
|
-
if (!state.stats) await refreshStats()
|
|
672
|
+
if (!state.stats) await refreshStats(signal)
|
|
631
673
|
}
|
|
632
674
|
|
|
633
|
-
async function loadTokens() {
|
|
634
|
-
const [tokens, service] = await Promise.all([api('tokens'), api('service')])
|
|
675
|
+
async function loadTokens(signal) {
|
|
676
|
+
const [tokens, service] = await Promise.all([api('tokens', { signal }), api('service', { signal })])
|
|
635
677
|
state.tokens = tokens
|
|
636
678
|
state.service = service
|
|
637
|
-
if (!state.stats) await refreshStats()
|
|
679
|
+
if (!state.stats) await refreshStats(signal)
|
|
638
680
|
}
|
|
639
681
|
|
|
640
682
|
function renderShell() {
|
|
@@ -660,7 +702,10 @@ function renderShell() {
|
|
|
660
702
|
element('main', { class: 'main' },
|
|
661
703
|
element('header', { class: 'topbar' },
|
|
662
704
|
element('div', { class: 'topbar-title' },
|
|
663
|
-
actionButton('☰', () => { state.menuOpen = !state.menuOpen; renderShell() }, 'ghost mobile-menu', {
|
|
705
|
+
actionButton('☰', () => { state.menuOpen = !state.menuOpen; renderShell() }, 'ghost mobile-menu', {
|
|
706
|
+
'aria-label': state.menuOpen ? '关闭导航菜单' : '打开导航菜单',
|
|
707
|
+
'aria-expanded': String(state.menuOpen),
|
|
708
|
+
}),
|
|
664
709
|
paneToggleButton('main', !state.documentView.sidebarHidden, () => setSidebarHidden(!state.documentView.sidebarHidden), '主导航栏'),
|
|
665
710
|
activeDocumentWorkspace() ? paneToggleButton('library', activeDocumentWorkspace().view.treeOpen, () => {
|
|
666
711
|
const workspace = activeDocumentWorkspace()
|
|
@@ -901,6 +946,7 @@ function renderKnowledgeBases() {
|
|
|
901
946
|
|
|
902
947
|
async function openKnowledgeBaseDocuments(base) {
|
|
903
948
|
captureScrollPosition()
|
|
949
|
+
const request = ++libraryDetailRequest
|
|
904
950
|
const detail = state.libraryDetail
|
|
905
951
|
const changingBase = detail.knowledgeBaseId !== base.id
|
|
906
952
|
detail.knowledgeBaseId = base.id
|
|
@@ -916,13 +962,15 @@ async function openKnowledgeBaseDocuments(base) {
|
|
|
916
962
|
renderShell()
|
|
917
963
|
const workspace = libraryDocumentWorkspace()
|
|
918
964
|
try {
|
|
919
|
-
await reloadDocumentWorkspace(workspace)
|
|
965
|
+
if (!await reloadDocumentWorkspace(workspace) || request !== libraryDetailRequest) return
|
|
920
966
|
selectDefaultDocument(workspace)
|
|
921
967
|
if (workspace.view.documentId) await loadDocumentEditor(workspace, workspace.view.documentId)
|
|
922
968
|
else workspace.view.editor = null
|
|
923
969
|
} catch (error) {
|
|
970
|
+
if (request !== libraryDetailRequest) return
|
|
924
971
|
detail.error = friendlyError(error)
|
|
925
972
|
} finally {
|
|
973
|
+
if (request !== libraryDetailRequest) return
|
|
926
974
|
state.loading = false
|
|
927
975
|
renderShell()
|
|
928
976
|
window.requestAnimationFrame(() => document.querySelector('.library-detail-back')?.focus())
|
|
@@ -934,6 +982,7 @@ async function closeKnowledgeBaseDetail() {
|
|
|
934
982
|
const editor = workspace.view.editor
|
|
935
983
|
const emptyDraft = editor?.isNew && !editor.title.trim() && !editor.body.trim()
|
|
936
984
|
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor(workspace)) return
|
|
985
|
+
libraryDetailRequest += 1
|
|
937
986
|
const baseId = state.libraryDetail.knowledgeBaseId
|
|
938
987
|
state.knowledgeBaseView = 'libraries'
|
|
939
988
|
state.libraryDetail.error = ''
|
|
@@ -998,6 +1047,16 @@ function contextPill(label, value) {
|
|
|
998
1047
|
return element('div', { class: 'context-pill' }, element('strong', {}, label), element('span', { title: value }, value))
|
|
999
1048
|
}
|
|
1000
1049
|
|
|
1050
|
+
function writebackRouteLabel(base) {
|
|
1051
|
+
if (state.service.writebackProvider && state.service.writebackModel) {
|
|
1052
|
+
return `本机覆盖 · ${state.service.writebackProvider} / ${state.service.writebackModel}`
|
|
1053
|
+
}
|
|
1054
|
+
if (base?.writebackProvider && base?.writebackModel) {
|
|
1055
|
+
return `知识库专用 · ${base.writebackProvider} / ${base.writebackModel}`
|
|
1056
|
+
}
|
|
1057
|
+
return '跟随当前会话模型'
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1001
1060
|
function renderKnowledgeBaseCard(base) {
|
|
1002
1061
|
const archived = base.status === 'archived'
|
|
1003
1062
|
const visibleTags = base.defaultTags.slice(0, 4)
|
|
@@ -1017,7 +1076,7 @@ function renderKnowledgeBaseCard(base) {
|
|
|
1017
1076
|
hiddenTagCount ? element('span', { class: 'tag' }, `+${hiddenTagCount}`) : null),
|
|
1018
1077
|
element('div', { class: 'base-card-meta' },
|
|
1019
1078
|
element('span', {}, element('strong', {}, '回写策略'), base.writebackPolicy === 'proactive' ? '主动' : '严谨'),
|
|
1020
|
-
element('span', {}, element('strong', {}, '回写模型'),
|
|
1079
|
+
element('span', {}, element('strong', {}, '回写模型'), writebackRouteLabel(base)),
|
|
1021
1080
|
base.extractionInstructions ? element('span', {}, element('strong', {}, '提取规则'), '已设置') : null,
|
|
1022
1081
|
),
|
|
1023
1082
|
element('div', { class: 'base-card-actions' },
|
|
@@ -1133,9 +1192,7 @@ function renderMountListRow(base, view, targetKind, targetId) {
|
|
|
1133
1192
|
renderShell()
|
|
1134
1193
|
},
|
|
1135
1194
|
})
|
|
1136
|
-
const modelLabel =
|
|
1137
|
-
? `本机 · ${state.service.writebackProvider} / ${state.service.writebackModel}`
|
|
1138
|
-
: '跟随当前会话模型'
|
|
1195
|
+
const modelLabel = writebackRouteLabel(base)
|
|
1139
1196
|
return element('article', { class: `mount-list-row${selected ? ' is-selected' : ''}`, role: 'listitem' },
|
|
1140
1197
|
element('label', { class: 'mount-select' }, checkbox, element('span', { class: 'visually-hidden' }, `选择 ${base.name}`)),
|
|
1141
1198
|
element('div', { class: 'mount-list-main' },
|
|
@@ -1444,12 +1501,9 @@ function renderCandidates() {
|
|
|
1444
1501
|
|
|
1445
1502
|
function renderCandidateCard(candidate) {
|
|
1446
1503
|
const pending = candidate.status === 'pending'
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
const targetDocument = candidate.action === 'create'
|
|
1451
|
-
? `新文档“${candidate.draft.title}”`
|
|
1452
|
-
: `文档“${candidate.draft.title}”`
|
|
1504
|
+
const target = candidate.targetId ? state.candidateTargets.get(candidate.targetId) : null
|
|
1505
|
+
const targetAvailable = candidate.action === 'create' || Boolean(target)
|
|
1506
|
+
const action = candidatePrimaryAction(candidate)
|
|
1453
1507
|
return element('article', { class: 'candidate' },
|
|
1454
1508
|
element('div', { class: 'candidate-header' },
|
|
1455
1509
|
element('div', {},
|
|
@@ -1462,31 +1516,122 @@ function renderCandidateCard(candidate) {
|
|
|
1462
1516
|
),
|
|
1463
1517
|
badge(STATUS_LABELS[candidate.status], candidate.status === 'approved' ? 'success' : candidate.status === 'rejected' ? 'danger' : 'warning'),
|
|
1464
1518
|
),
|
|
1465
|
-
element('div', { class:
|
|
1466
|
-
candidate
|
|
1467
|
-
? element('div', { class: 'candidate-comparison' },
|
|
1468
|
-
element('section', {}, element('strong', {}, '当前知识'), element('pre', {}, conflictTarget?.body || '目标文档不可用,请刷新后重试。')),
|
|
1469
|
-
element('section', {}, element('strong', {}, '候选内容'), element('pre', {}, candidate.draft.body)),
|
|
1470
|
-
)
|
|
1471
|
-
: element('p', { class: 'candidate-content' }, candidate.draft.body),
|
|
1519
|
+
element('div', { class: 'candidate-body' },
|
|
1520
|
+
renderCandidateDiff(candidate, target),
|
|
1472
1521
|
element('div', { class: 'candidate-reason' },
|
|
1473
|
-
element('
|
|
1474
|
-
|
|
1475
|
-
|
|
1522
|
+
element('section', {},
|
|
1523
|
+
element('strong', {}, '写入位置'),
|
|
1524
|
+
element('span', { class: 'candidate-target' }, candidate.action === 'create'
|
|
1525
|
+
? `创建“${candidate.draft.title}”`
|
|
1526
|
+
: target ? `合并到“${target.title}”` : `目标 ${candidate.targetId || '不可用'}`),
|
|
1527
|
+
),
|
|
1528
|
+
renderCandidateMetadataChanges(candidate, target),
|
|
1529
|
+
element('section', {},
|
|
1530
|
+
element('strong', {}, '模型判断依据'),
|
|
1531
|
+
element('p', {}, candidate.reason || '未提供判断说明'),
|
|
1532
|
+
),
|
|
1533
|
+
candidate.reviewNote ? element('section', {},
|
|
1534
|
+
element('strong', {}, '审核备注'),
|
|
1535
|
+
element('p', {}, candidate.reviewNote),
|
|
1536
|
+
) : null,
|
|
1537
|
+
),
|
|
1476
1538
|
),
|
|
1477
1539
|
element('div', { class: 'candidate-footer' },
|
|
1478
1540
|
element('small', {}, `${scopeLabel(candidate.draft.scope)} · 置信度 ${Math.round(candidate.draft.confidence * 100)}%${candidate.targetId ? ` · 目标 ${candidate.targetId}` : ''} · ${formatDate(candidate.createdAt)}`),
|
|
1479
|
-
pending ? element('div', {},
|
|
1541
|
+
pending ? element('div', { class: 'candidate-actions' },
|
|
1480
1542
|
actionButton('拒绝', () => reviewCandidate(candidate, 'reject'), 'danger small'),
|
|
1481
|
-
actionButton(
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1543
|
+
actionButton(action.editLabel, () => openEntryEditor(undefined, candidate), 'small', {
|
|
1544
|
+
disabled: !targetAvailable,
|
|
1545
|
+
title: targetAvailable ? action.editLabel : '目标文档不可用,无法安全编辑合并结果',
|
|
1546
|
+
}),
|
|
1547
|
+
actionButton(action.label, () => reviewCandidate(candidate, 'approve', action.resolution), 'primary small', {
|
|
1548
|
+
disabled: !targetAvailable,
|
|
1549
|
+
title: targetAvailable ? action.label : '目标文档不可用,刷新后再审核',
|
|
1550
|
+
}),
|
|
1485
1551
|
) : null,
|
|
1486
1552
|
),
|
|
1487
1553
|
)
|
|
1488
1554
|
}
|
|
1489
1555
|
|
|
1556
|
+
function renderCandidateDiff(candidate, target) {
|
|
1557
|
+
if (candidate.action !== 'create' && !target) {
|
|
1558
|
+
return element('section', { class: 'candidate-change candidate-change-unavailable', role: 'alert' },
|
|
1559
|
+
element('strong', {}, '无法生成变更预览'),
|
|
1560
|
+
element('p', {}, '目标文档暂时不可用。为避免盲目覆盖,刷新并确认当前版本后再审核。'),
|
|
1561
|
+
)
|
|
1562
|
+
}
|
|
1563
|
+
const review = window.DshKnowledgeReview.createReviewChange(candidate.action, target?.body || '', candidate.draft.body)
|
|
1564
|
+
const title = candidate.action === 'create' ? '新文档内容' : candidate.action === 'conflict' ? '冲突合并预览' : '文档合并预览'
|
|
1565
|
+
const targetLabel = candidate.action === 'create'
|
|
1566
|
+
? candidate.draft.title
|
|
1567
|
+
: `${target.title} · 当前版本 ${target.version}`
|
|
1568
|
+
return element('section', { class: `candidate-change is-${candidate.action}`, 'aria-label': title },
|
|
1569
|
+
element('div', { class: 'candidate-change-heading' },
|
|
1570
|
+
element('div', {}, element('strong', {}, title), element('small', {}, targetLabel)),
|
|
1571
|
+
element('div', { class: 'diff-summary', 'aria-label': `新增 ${review.diff.additions} 行,删除 ${review.diff.deletions} 行,未变更 ${review.diff.unchanged} 行` },
|
|
1572
|
+
element('span', { class: 'diff-stat additions' }, `+${review.diff.additions}`),
|
|
1573
|
+
element('span', { class: 'diff-stat deletions' }, `-${review.diff.deletions}`),
|
|
1574
|
+
element('span', { class: 'diff-stat unchanged' }, `${review.diff.unchanged} 未变`),
|
|
1575
|
+
),
|
|
1576
|
+
),
|
|
1577
|
+
review.diff.simplified ? element('div', { class: 'diff-notice' }, '文档变更较大,已使用简化差异视图。合并内容不受影响。') : null,
|
|
1578
|
+
element('div', { class: 'diff-viewer', role: 'table', 'aria-label': `${title}逐行差异` },
|
|
1579
|
+
element('div', { class: 'diff-column-headings', role: 'row' },
|
|
1580
|
+
element('span', { role: 'columnheader' }, '旧'),
|
|
1581
|
+
element('span', { role: 'columnheader' }, '新'),
|
|
1582
|
+
element('span', { 'aria-hidden': 'true' }),
|
|
1583
|
+
element('span', { role: 'columnheader' }, '正文'),
|
|
1584
|
+
),
|
|
1585
|
+
review.displayLines.length
|
|
1586
|
+
? review.displayLines.map(renderDiffLine)
|
|
1587
|
+
: element('div', { class: 'diff-empty' }, '正文为空,没有可写入的差异。'),
|
|
1588
|
+
),
|
|
1589
|
+
)
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
function renderDiffLine(line) {
|
|
1593
|
+
if (line.kind === 'omitted') {
|
|
1594
|
+
return element('div', { class: 'diff-line is-omitted', role: 'row', 'aria-label': `${line.count} 行未变更,已折叠` },
|
|
1595
|
+
element('span', { class: 'diff-line-number', 'aria-hidden': 'true' }),
|
|
1596
|
+
element('span', { class: 'diff-line-number', 'aria-hidden': 'true' }),
|
|
1597
|
+
element('span', { class: 'diff-marker', 'aria-hidden': 'true' }, '…'),
|
|
1598
|
+
element('span', { class: 'diff-omitted-copy' }, `${line.count} 行未变更`),
|
|
1599
|
+
)
|
|
1600
|
+
}
|
|
1601
|
+
const marker = line.kind === 'add' ? '+' : line.kind === 'remove' ? '-' : ' '
|
|
1602
|
+
const label = line.kind === 'add'
|
|
1603
|
+
? `新增第 ${line.newLine} 行`
|
|
1604
|
+
: line.kind === 'remove' ? `删除原第 ${line.oldLine} 行` : `未变更行 ${line.oldLine}`
|
|
1605
|
+
return element('div', { class: `diff-line is-${line.kind}`, role: 'row', 'aria-label': label },
|
|
1606
|
+
element('span', { class: 'diff-line-number', role: 'cell' }, line.oldLine || ''),
|
|
1607
|
+
element('span', { class: 'diff-line-number', role: 'cell' }, line.newLine || ''),
|
|
1608
|
+
element('span', { class: 'diff-marker', role: 'cell', 'aria-hidden': 'true' }, marker),
|
|
1609
|
+
element('code', { class: 'diff-code', role: 'cell' }, line.text || ' '),
|
|
1610
|
+
)
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
function renderCandidateMetadataChanges(candidate, target) {
|
|
1614
|
+
if (!target || candidate.action === 'create') return null
|
|
1615
|
+
const changes = []
|
|
1616
|
+
if (target.title !== candidate.draft.title) changes.push(`标题:${target.title} → ${candidate.draft.title}`)
|
|
1617
|
+
const addedTags = candidate.draft.tags.filter(tag => !target.tags.includes(tag))
|
|
1618
|
+
if (addedTags.length) changes.push(`新增标签:${addedTags.join('、')}`)
|
|
1619
|
+
if (candidate.draft.confidence > target.confidence) {
|
|
1620
|
+
changes.push(`置信度:${Math.round(target.confidence * 100)}% → ${Math.round(candidate.draft.confidence * 100)}%`)
|
|
1621
|
+
}
|
|
1622
|
+
if (!changes.length) return null
|
|
1623
|
+
return element('section', { class: 'candidate-metadata' },
|
|
1624
|
+
element('strong', {}, '属性变化'),
|
|
1625
|
+
element('ul', {}, changes.map(change => element('li', {}, change))),
|
|
1626
|
+
)
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function candidatePrimaryAction(candidate) {
|
|
1630
|
+
if (candidate.action === 'create') return { label: '写入新文档', editLabel: '编辑后写入' }
|
|
1631
|
+
if (candidate.action === 'conflict') return { label: '确认合并', editLabel: '编辑合并内容', resolution: 'merge' }
|
|
1632
|
+
return { label: '合并到文档', editLabel: '编辑后合并' }
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1490
1635
|
function renderTokens() {
|
|
1491
1636
|
const apiAddress = new URL(state.service.publicApiPrefix, location.origin).href.replace(/\/$/, '')
|
|
1492
1637
|
return element('section', { 'aria-labelledby': 'tokens-heading' },
|
|
@@ -1569,7 +1714,8 @@ function parseTags(value) {
|
|
|
1569
1714
|
return [...new Set(value.split(/[,,]/).map(tag => tag.trim().toLowerCase()).filter(Boolean))]
|
|
1570
1715
|
}
|
|
1571
1716
|
|
|
1572
|
-
function openKnowledgeBaseEditor(base) {
|
|
1717
|
+
async function openKnowledgeBaseEditor(base) {
|
|
1718
|
+
await loadModelCatalog()
|
|
1573
1719
|
const source = base || { name: '', description: '', defaultTags: [], extractionInstructions: '', writebackPolicy: 'conservative' }
|
|
1574
1720
|
const form = element('form', { class: 'form-grid' })
|
|
1575
1721
|
const name = formField('名称', 'input', source.name, { required: true, maxlength: 100, placeholder: '例如:项目规范' })
|
|
@@ -1580,9 +1726,26 @@ function openKnowledgeBaseEditor(base) {
|
|
|
1580
1726
|
{ value: 'conservative', label: '严谨(高置信度直写)' },
|
|
1581
1727
|
{ value: 'proactive', label: '主动(更积极沉淀)' },
|
|
1582
1728
|
], source.writebackPolicy || 'conservative')
|
|
1729
|
+
const dedicatedModel = element('input', {
|
|
1730
|
+
type: 'checkbox',
|
|
1731
|
+
checked: Boolean(source.writebackProvider && source.writebackModel),
|
|
1732
|
+
})
|
|
1733
|
+
const route = modelRouteFields(source.writebackProvider || '', source.writebackModel || '')
|
|
1734
|
+
const routeToggle = element('label', { class: 'check-option span-2' }, dedicatedModel,
|
|
1735
|
+
element('span', {},
|
|
1736
|
+
element('strong', {}, '这个知识库使用专用回写模型'),
|
|
1737
|
+
element('small', {}, '未设置时先使用本机覆盖;本机也未设置时跟随当前会话模型。'),
|
|
1738
|
+
))
|
|
1739
|
+
const syncRouteAvailability = () => {
|
|
1740
|
+
route.provider.input.disabled = route.model.input.disabled = !dedicatedModel.checked
|
|
1741
|
+
route.provider.input.required = route.model.input.required = dedicatedModel.checked
|
|
1742
|
+
}
|
|
1743
|
+
dedicatedModel.addEventListener('change', syncRouteAvailability)
|
|
1744
|
+
syncRouteAvailability()
|
|
1583
1745
|
for (const field of [name, description, tags, instructions]) field.wrapper.classList.add('span-2')
|
|
1584
1746
|
form.append(
|
|
1585
1747
|
name.wrapper, description.wrapper, tags.wrapper, instructions.wrapper, policy.wrapper,
|
|
1748
|
+
routeToggle, route.provider.wrapper, route.model.wrapper,
|
|
1586
1749
|
)
|
|
1587
1750
|
openSheet({
|
|
1588
1751
|
title: base ? '编辑知识库' : '创建知识库',
|
|
@@ -1597,6 +1760,10 @@ function openKnowledgeBaseEditor(base) {
|
|
|
1597
1760
|
defaultTags: parseTags(tags.input.value),
|
|
1598
1761
|
extractionInstructions: instructions.input.value.trim(),
|
|
1599
1762
|
writebackPolicy: policy.input.value,
|
|
1763
|
+
...(dedicatedModel.checked ? {
|
|
1764
|
+
writebackProvider: route.provider.input.value,
|
|
1765
|
+
writebackModel: route.model.input.value,
|
|
1766
|
+
} : {}),
|
|
1600
1767
|
}
|
|
1601
1768
|
if (base) await api(`knowledge-bases/${encodeURIComponent(base.id)}`, { method: 'PUT', body: { draft } })
|
|
1602
1769
|
else await api('knowledge-bases', { method: 'POST', body: { draft } })
|
|
@@ -1830,7 +1997,11 @@ function confirmDeleteKnowledgeBase(base) {
|
|
|
1830
1997
|
|
|
1831
1998
|
function openEntryEditor(entry, candidate) {
|
|
1832
1999
|
const activeWorkspace = activeDocumentWorkspace()
|
|
1833
|
-
const
|
|
2000
|
+
const candidateTarget = candidate?.targetId ? state.candidateTargets.get(candidate.targetId) : null
|
|
2001
|
+
const candidateReview = candidate && (candidate.action === 'create' || candidateTarget)
|
|
2002
|
+
? window.DshKnowledgeReview.createReviewChange(candidate.action, candidateTarget?.body || '', candidate.draft.body)
|
|
2003
|
+
: null
|
|
2004
|
+
const source = candidate ? { ...candidate.draft, body: candidateReview?.after || candidate.draft.body } : entry || {
|
|
1834
2005
|
knowledgeBaseId: activeWorkspace?.view.knowledgeBaseId || undefined,
|
|
1835
2006
|
title: '', body: '', type: 'fact', tags: [], scope: { kind: 'global' }, confidence: .8,
|
|
1836
2007
|
}
|
|
@@ -1863,8 +2034,16 @@ function openEntryEditor(entry, candidate) {
|
|
|
1863
2034
|
)
|
|
1864
2035
|
body.wrapper.classList.add('span-2')
|
|
1865
2036
|
tags.wrapper.classList.add('span-2')
|
|
1866
|
-
const modeTitle = candidate
|
|
1867
|
-
|
|
2037
|
+
const modeTitle = candidate
|
|
2038
|
+
? candidate.action === 'create' ? '编辑新文档' : '编辑合并内容'
|
|
2039
|
+
: entry ? '编辑知识文档' : '新建知识文档'
|
|
2040
|
+
const candidateDescription = candidate?.action === 'create'
|
|
2041
|
+
? '确认后将创建文档并立即参与后续召回。'
|
|
2042
|
+
: '正文已载入完整合并预览;保存前可以处理重复内容或冲突。'
|
|
2043
|
+
const primaryLabel = candidate
|
|
2044
|
+
? candidate.action === 'create' ? '写入新文档' : '保存并合并'
|
|
2045
|
+
: '保存文档'
|
|
2046
|
+
const modal = openSheet({ title: modeTitle, description: candidate ? candidateDescription : '文档保存后会立即参与后续召回。', body: form, primaryLabel, onPrimary: async () => {
|
|
1868
2047
|
if (!form.reportValidity()) return false
|
|
1869
2048
|
const draft = {
|
|
1870
2049
|
knowledgeBaseId: knowledgeBase.input.value,
|
|
@@ -1879,7 +2058,7 @@ function openEntryEditor(entry, candidate) {
|
|
|
1879
2058
|
})
|
|
1880
2059
|
else if (entry) await api(`entries/${encodeURIComponent(entry.id)}`, { method: 'PUT', body: { draft } })
|
|
1881
2060
|
else await api('entries', { method: 'POST', body: { draft } })
|
|
1882
|
-
showToast(candidate ? '
|
|
2061
|
+
showToast(candidate ? candidate.action === 'create' ? '新文档已写入。' : '候选内容已合并。' : '知识已保存。')
|
|
1883
2062
|
state.stats = null
|
|
1884
2063
|
await navigate(candidate ? 'candidates' : state.view)
|
|
1885
2064
|
return true
|
|
@@ -1902,7 +2081,9 @@ function selectField(label, options, value) {
|
|
|
1902
2081
|
async function loadModelCatalog() {
|
|
1903
2082
|
if (state.modelCatalog !== null) return state.modelCatalog
|
|
1904
2083
|
try {
|
|
1905
|
-
const response = await fetch('/knowledge-control/v1/models', {
|
|
2084
|
+
const response = await fetch('/knowledge-control/v1/models', {
|
|
2085
|
+
headers: { accept: 'application/json', 'x-dsh-knowledge-client': 'management-web' },
|
|
2086
|
+
})
|
|
1906
2087
|
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
1907
2088
|
const payload = await response.json()
|
|
1908
2089
|
state.modelCatalog = Array.isArray(payload.providers) ? payload.providers : []
|
|
@@ -1936,10 +2117,19 @@ function modelRouteFields(currentProvider, currentModel) {
|
|
|
1936
2117
|
|
|
1937
2118
|
async function reviewCandidate(candidate, decision, resolution) {
|
|
1938
2119
|
const approve = decision === 'approve'
|
|
2120
|
+
const action = candidatePrimaryAction(candidate)
|
|
2121
|
+
const title = approve ? `${action.label}?` : '拒绝这条候选?'
|
|
2122
|
+
const message = !approve
|
|
2123
|
+
? '拒绝后会保留审核记录,但不会进入知识库。'
|
|
2124
|
+
: candidate.action === 'create'
|
|
2125
|
+
? '确认后将创建新文档,并立即参与后续对话召回。'
|
|
2126
|
+
: candidate.action === 'conflict'
|
|
2127
|
+
? '确认后将保留当前文档内容,并把候选内容合入同一文档。'
|
|
2128
|
+
: '确认后将按预览结果合入目标文档;若审核期间文档发生变化,会转为冲突项。'
|
|
1939
2129
|
openConfirm({
|
|
1940
|
-
title
|
|
1941
|
-
message
|
|
1942
|
-
confirmLabel: approve ?
|
|
2130
|
+
title,
|
|
2131
|
+
message,
|
|
2132
|
+
confirmLabel: approve ? action.label : '确认拒绝', danger: !approve,
|
|
1943
2133
|
onConfirm: async () => {
|
|
1944
2134
|
const reviewed = await api(`candidates/${encodeURIComponent(candidate.id)}/review`, {
|
|
1945
2135
|
method: 'POST', body: { decision, ...(resolution ? { resolution } : {}) },
|
|
@@ -1950,7 +2140,7 @@ async function reviewCandidate(candidate, decision, resolution) {
|
|
|
1950
2140
|
await navigate('candidates')
|
|
1951
2141
|
return
|
|
1952
2142
|
}
|
|
1953
|
-
showToast(approve ? '
|
|
2143
|
+
showToast(approve ? candidate.action === 'create' ? '新文档已写入。' : '候选内容已合并。' : '候选已拒绝。')
|
|
1954
2144
|
state.stats = null
|
|
1955
2145
|
await navigate('candidates')
|
|
1956
2146
|
},
|
|
@@ -2049,14 +2239,19 @@ function openModal({ title, description = '', body, primaryLabel, primaryVariant
|
|
|
2049
2239
|
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
2050
2240
|
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
2051
2241
|
let busy = false
|
|
2052
|
-
|
|
2242
|
+
let formDirty = false
|
|
2243
|
+
const close = (explicit = false) => {
|
|
2053
2244
|
if (busy) return
|
|
2245
|
+
if (!explicit && formDirty) {
|
|
2246
|
+
showToast('表单有未保存的修改,请先保存,或使用“取消”放弃修改。', 'error')
|
|
2247
|
+
return
|
|
2248
|
+
}
|
|
2054
2249
|
document.removeEventListener('keydown', onKeyDown)
|
|
2055
2250
|
backdrop.remove()
|
|
2056
2251
|
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
2057
2252
|
}
|
|
2058
|
-
const closeButton = actionButton('×', close, 'ghost', { 'aria-label': '关闭对话框' })
|
|
2059
|
-
const cancel = actionButton(cancelLabel, close)
|
|
2253
|
+
const closeButton = actionButton('×', () => close(), 'ghost', { 'aria-label': '关闭对话框' })
|
|
2254
|
+
const cancel = actionButton(cancelLabel, () => close(true))
|
|
2060
2255
|
const primary = primaryLabel ? actionButton(primaryLabel, async () => {
|
|
2061
2256
|
if (busy) return
|
|
2062
2257
|
busy = true
|
|
@@ -2067,7 +2262,7 @@ function openModal({ title, description = '', body, primaryLabel, primaryVariant
|
|
|
2067
2262
|
try {
|
|
2068
2263
|
const shouldClose = await onPrimary()
|
|
2069
2264
|
busy = false
|
|
2070
|
-
if (shouldClose !== false) close()
|
|
2265
|
+
if (shouldClose !== false) close(true)
|
|
2071
2266
|
else { primary.disabled = false; cancel.disabled = false; primary.textContent = original }
|
|
2072
2267
|
} catch (error) {
|
|
2073
2268
|
busy = false
|
|
@@ -2088,6 +2283,10 @@ function openModal({ title, description = '', body, primaryLabel, primaryVariant
|
|
|
2088
2283
|
)
|
|
2089
2284
|
backdrop.append(dialog)
|
|
2090
2285
|
backdrop.addEventListener('mousedown', (event) => { if (event.target === backdrop) close() })
|
|
2286
|
+
if (body.matches?.('form') || body.querySelector?.('form')) {
|
|
2287
|
+
body.addEventListener('input', () => { formDirty = true })
|
|
2288
|
+
body.addEventListener('change', () => { formDirty = true })
|
|
2289
|
+
}
|
|
2091
2290
|
document.body.append(backdrop)
|
|
2092
2291
|
document.addEventListener('keydown', onKeyDown)
|
|
2093
2292
|
window.setTimeout(() => (dialog.querySelector('input, textarea, select, button') || dialog).focus(), 0)
|
|
@@ -2118,4 +2317,11 @@ function friendlyError(error) {
|
|
|
2118
2317
|
return error.message || '操作失败,请稍后重试。'
|
|
2119
2318
|
}
|
|
2120
2319
|
|
|
2320
|
+
window.addEventListener('beforeunload', event => {
|
|
2321
|
+
const editor = activeDocumentWorkspace()?.view.editor
|
|
2322
|
+
if (!editor?.dirty) return
|
|
2323
|
+
event.preventDefault()
|
|
2324
|
+
event.returnValue = ''
|
|
2325
|
+
})
|
|
2326
|
+
|
|
2121
2327
|
void installHostThemeBridge().then(() => boot())
|