@lemoncat7/dsh-knowledge 2.3.3 → 2.3.6
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 +5 -3
- package/docs/architecture.md +2 -0
- package/docs/releases/2.3.5.md +26 -0
- package/docs/releases/2.3.6.md +17 -0
- package/docs/ui-maintenance.md +97 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +20 -1
- package/lib/api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +357 -206
- package/lib/client.js.map +4 -4
- package/lib/design-tokens.d.ts +12 -0
- package/lib/design-tokens.d.ts.map +1 -0
- package/lib/design-tokens.js +58 -0
- package/lib/design-tokens.js.map +1 -0
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +2 -1
- package/lib/extraction.js.map +1 -1
- package/lib/knowledge-activity-controller.d.ts +3 -8
- package/lib/knowledge-activity-controller.d.ts.map +1 -1
- package/lib/knowledge-activity-controller.js +19 -7
- package/lib/knowledge-activity-controller.js.map +1 -1
- package/lib/knowledge-activity-notes.d.ts.map +1 -1
- package/lib/knowledge-activity-notes.js +19 -10
- package/lib/knowledge-activity-notes.js.map +1 -1
- package/lib/knowledge-activity-panel.d.ts.map +1 -1
- package/lib/knowledge-activity-panel.js +33 -21
- package/lib/knowledge-activity-panel.js.map +1 -1
- package/lib/knowledge-activity-presentation.d.ts +14 -0
- package/lib/knowledge-activity-presentation.d.ts.map +1 -0
- package/lib/knowledge-activity-presentation.js +77 -0
- package/lib/knowledge-activity-presentation.js.map +1 -0
- package/lib/knowledge-activity-state.d.ts +17 -0
- package/lib/knowledge-activity-state.d.ts.map +1 -0
- package/lib/knowledge-activity-state.js +14 -0
- package/lib/knowledge-activity-state.js.map +1 -0
- package/lib/latest-request.d.ts +7 -0
- package/lib/latest-request.d.ts.map +1 -0
- package/lib/latest-request.js +14 -0
- package/lib/latest-request.js.map +1 -0
- package/lib/local-provider.d.ts +0 -2
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +2 -323
- package/lib/local-provider.js.map +1 -1
- package/lib/notes/share-page-styles.d.ts +2 -0
- package/lib/notes/share-page-styles.d.ts.map +1 -0
- package/lib/notes/share-page-styles.js +11 -0
- package/lib/notes/share-page-styles.js.map +1 -0
- package/lib/notes/share-page.d.ts.map +1 -1
- package/lib/notes/share-page.js +1 -8
- package/lib/notes/share-page.js.map +1 -1
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +41 -6
- package/lib/remote-provider.js.map +1 -1
- package/lib/search-query.d.ts +2 -0
- package/lib/search-query.d.ts.map +1 -0
- package/lib/search-query.js +50 -0
- package/lib/search-query.js.map +1 -0
- package/lib/storage/migrations.d.ts +5 -0
- package/lib/storage/migrations.d.ts.map +1 -0
- package/lib/storage/migrations.js +325 -0
- package/lib/storage/migrations.js.map +1 -0
- package/lib/theme-bridge.d.ts +9 -7
- package/lib/theme-bridge.d.ts.map +1 -1
- package/lib/theme-bridge.js +3 -23
- package/lib/theme-bridge.js.map +1 -1
- package/lib/web-note-editor-selection.d.ts.map +1 -1
- package/lib/web-note-editor-selection.js +50 -15
- package/lib/web-note-editor-selection.js.map +1 -1
- package/lib/web-note-editor.d.ts.map +1 -1
- package/lib/web-note-editor.js +15 -1
- package/lib/web-note-editor.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +4 -0
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +78 -176
- package/web/design-tokens.css +1 -0
- package/web/dialogs.js +94 -0
- package/web/document-actions.js +68 -0
- package/web/index.html +1 -0
- package/web/model-catalog.js +22 -0
- package/web/note-editor.js +57 -57
- package/web/select-control.js +193 -0
- package/web/styles.css +93 -185
- package/web/ui-primitives.js +5 -0
package/web/app.js
CHANGED
|
@@ -3,14 +3,20 @@ const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')
|
|
|
3
3
|
const WEB_PATH = document.querySelector('meta[name="dsh-knowledge-web"]')?.content || '/knowledge'
|
|
4
4
|
const ASSET_VERSION = document.querySelector('meta[name="dsh-knowledge-asset-version"]')?.content || ''
|
|
5
5
|
const moduleUrl = name => `./${name}.js${ASSET_VERSION ? `?v=${encodeURIComponent(ASSET_VERSION)}` : ''}`
|
|
6
|
-
const [apiModule, themeModule, uiModule] = await Promise.all([
|
|
6
|
+
const [apiModule, themeModule, uiModule, modelCatalogModule, dialogModule, selectModule, documentActionsModule] = await Promise.all([
|
|
7
7
|
import(moduleUrl('api-client')),
|
|
8
8
|
import(moduleUrl('host-theme')),
|
|
9
9
|
import(moduleUrl('ui-primitives')),
|
|
10
|
+
import(moduleUrl('model-catalog')),
|
|
11
|
+
import(moduleUrl('dialogs')),
|
|
12
|
+
import(moduleUrl('select-control')),
|
|
13
|
+
import(moduleUrl('document-actions')),
|
|
10
14
|
])
|
|
11
15
|
const { createApiClient } = apiModule
|
|
12
16
|
const { installHostThemeBridge } = themeModule
|
|
13
17
|
const { actionButton, badge, createToastPresenter, element, interfaceIcon, paneToggleButton } = uiModule
|
|
18
|
+
const { renderMenu: renderDocumentMenu, closeMenus: closeDocumentMenus } = documentActionsModule.createDocumentMenuPresenter(uiModule)
|
|
19
|
+
const readModelCatalog = modelCatalogModule.createModelCatalogLoader()
|
|
14
20
|
const TOKEN_KEY = 'dsh-knowledge.session-token'
|
|
15
21
|
const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
|
|
16
22
|
const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
|
|
@@ -35,6 +41,8 @@ const mountContext = {
|
|
|
35
41
|
const app = document.querySelector('#app')
|
|
36
42
|
const toastRegion = document.querySelector('#toast-region')
|
|
37
43
|
const showToast = createToastPresenter(toastRegion)
|
|
44
|
+
const { openConfirm, openSheet, openModal } = dialogModule.createDialogPresenter({ element, actionButton, interfaceIcon, showToast, friendlyError })
|
|
45
|
+
selectModule.installSelectControls()
|
|
38
46
|
const savedDocumentLayout = readDocumentLayout()
|
|
39
47
|
|
|
40
48
|
function createDocumentViewState(overrides = {}) {
|
|
@@ -335,7 +343,10 @@ function updateLoadingPhase(request, label, progress) {
|
|
|
335
343
|
if (request !== navigationRequest || !state.loading) return
|
|
336
344
|
state.loadingPhase = label
|
|
337
345
|
state.loadingProgress = Math.max(0, Math.min(1, progress))
|
|
338
|
-
|
|
346
|
+
const progressNode = document.querySelector('.route-progress')
|
|
347
|
+
progressNode?.setAttribute('aria-label', label)
|
|
348
|
+
progressNode?.setAttribute('aria-valuenow', String(Math.round(state.loadingProgress * 100)))
|
|
349
|
+
progressNode?.firstElementChild?.style.setProperty('--route-progress', String(state.loadingProgress))
|
|
339
350
|
}
|
|
340
351
|
|
|
341
352
|
async function saveBeforeNavigation() {
|
|
@@ -939,6 +950,7 @@ async function loadTokens(signal) {
|
|
|
939
950
|
}
|
|
940
951
|
|
|
941
952
|
function renderShell() {
|
|
953
|
+
closeDocumentMenus()
|
|
942
954
|
releaseNoteEditors()
|
|
943
955
|
captureScrollPosition()
|
|
944
956
|
const titles = {
|
|
@@ -962,15 +974,15 @@ function renderShell() {
|
|
|
962
974
|
},
|
|
963
975
|
renderSidebar(),
|
|
964
976
|
renderAppSidebarResizer(),
|
|
965
|
-
|
|
966
|
-
type: 'button', class: 'app-sidebar-scrim',
|
|
977
|
+
element('button', {
|
|
978
|
+
type: 'button', class: 'app-sidebar-scrim', hidden: !state.menuOpen,
|
|
967
979
|
'aria-label': '关闭导航菜单',
|
|
968
|
-
onClick: () =>
|
|
969
|
-
})
|
|
980
|
+
onClick: () => setMenuOpen(false),
|
|
981
|
+
}),
|
|
970
982
|
element('main', { class: 'main' },
|
|
971
983
|
element('header', { class: 'topbar' },
|
|
972
984
|
element('div', { class: 'topbar-title' },
|
|
973
|
-
actionButton('
|
|
985
|
+
actionButton(interfaceIcon('menu'), () => setMenuOpen(!state.menuOpen), 'ghost mobile-menu', {
|
|
974
986
|
'aria-label': state.menuOpen ? '关闭导航菜单' : '打开导航菜单',
|
|
975
987
|
'aria-expanded': String(state.menuOpen),
|
|
976
988
|
}),
|
|
@@ -1031,24 +1043,38 @@ function startSidebarResize(event, minimum, maximum) {
|
|
|
1031
1043
|
if (!shell) return
|
|
1032
1044
|
const startX = event.clientX
|
|
1033
1045
|
const startWidth = state.documentView.sidebarWidth
|
|
1046
|
+
let pendingWidth = startWidth
|
|
1047
|
+
let frame = 0
|
|
1034
1048
|
handle.setPointerCapture?.(event.pointerId)
|
|
1035
1049
|
handle.classList.add('is-dragging')
|
|
1036
1050
|
document.body.classList.add('is-resizing-columns')
|
|
1037
1051
|
const move = moveEvent => {
|
|
1038
|
-
|
|
1039
|
-
|
|
1052
|
+
if (moveEvent.pointerId !== event.pointerId) return
|
|
1053
|
+
pendingWidth = clampNumber(startWidth + moveEvent.clientX - startX, minimum, maximum, startWidth)
|
|
1054
|
+
if (!frame) frame = window.requestAnimationFrame(() => {
|
|
1055
|
+
frame = 0
|
|
1056
|
+
if (shell.isConnected) setSidebarWidth(pendingWidth, shell, handle)
|
|
1057
|
+
})
|
|
1040
1058
|
}
|
|
1041
|
-
const finish =
|
|
1059
|
+
const finish = endEvent => {
|
|
1060
|
+
if (endEvent.pointerId !== event.pointerId) return
|
|
1061
|
+
if (frame) window.cancelAnimationFrame(frame)
|
|
1062
|
+
frame = 0
|
|
1063
|
+
if (endEvent.type === 'pointerup') pendingWidth = clampNumber(startWidth + endEvent.clientX - startX, minimum, maximum, startWidth)
|
|
1064
|
+
if (shell.isConnected) setSidebarWidth(pendingWidth, shell, handle)
|
|
1042
1065
|
handle.classList.remove('is-dragging')
|
|
1043
1066
|
document.body.classList.remove('is-resizing-columns')
|
|
1044
1067
|
handle.removeEventListener('pointermove', move)
|
|
1045
1068
|
handle.removeEventListener('pointerup', finish)
|
|
1046
1069
|
handle.removeEventListener('pointercancel', finish)
|
|
1070
|
+
handle.removeEventListener('lostpointercapture', finish)
|
|
1071
|
+
if (handle.hasPointerCapture?.(event.pointerId)) handle.releasePointerCapture(event.pointerId)
|
|
1047
1072
|
saveDocumentLayout()
|
|
1048
1073
|
}
|
|
1049
1074
|
handle.addEventListener('pointermove', move)
|
|
1050
1075
|
handle.addEventListener('pointerup', finish)
|
|
1051
1076
|
handle.addEventListener('pointercancel', finish)
|
|
1077
|
+
handle.addEventListener('lostpointercapture', finish)
|
|
1052
1078
|
}
|
|
1053
1079
|
|
|
1054
1080
|
function resizeSidebarWithKeyboard(event, minimum, maximum) {
|
|
@@ -1068,6 +1094,18 @@ function setSidebarWidth(width, shell, handle) {
|
|
|
1068
1094
|
handle?.setAttribute('aria-valuenow', String(width))
|
|
1069
1095
|
}
|
|
1070
1096
|
|
|
1097
|
+
function setMenuOpen(open) {
|
|
1098
|
+
state.menuOpen = open
|
|
1099
|
+
const shell = app.querySelector('.app-shell')
|
|
1100
|
+
if (!shell) return
|
|
1101
|
+
shell.dataset.menuOpen = String(open)
|
|
1102
|
+
shell.querySelector('.app-sidebar-scrim')?.toggleAttribute('hidden', !open)
|
|
1103
|
+
const toggle = shell.querySelector('.mobile-menu')
|
|
1104
|
+
toggle?.setAttribute('aria-expanded', String(open))
|
|
1105
|
+
toggle?.setAttribute('aria-label', open ? '关闭导航菜单' : '打开导航菜单')
|
|
1106
|
+
applySidebarVisibility(shell, state.documentView.sidebarHidden)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1071
1109
|
function setSidebarHidden(hidden) {
|
|
1072
1110
|
state.documentView.sidebarHidden = hidden
|
|
1073
1111
|
saveDocumentLayout()
|
|
@@ -1734,13 +1772,16 @@ function renderNoteEditor(workspace, editor, base) {
|
|
|
1734
1772
|
element('div', { class: 'note-editor-actions' },
|
|
1735
1773
|
finalized ? badge(DOCUMENT_STATE_LABELS[editor.documentState] || '已结束', 'success') : readOnly ? badge('只读') : null,
|
|
1736
1774
|
element('span', { class: 'editor-save-status', role: 'status' }, editor.saveState),
|
|
1737
|
-
!readOnly && !editor.isNew && documentWorkspaceBases(workspace).some(item => item.id !== editor.knowledgeBaseId)
|
|
1738
|
-
? actionButton('移动到…', () => openMoveKnowledgeDocument(workspace, editor), 'ghost small')
|
|
1739
|
-
: null,
|
|
1740
1775
|
finalized && !readOnly ? actionButton('重新打开', () => reopenDocument(workspace, editor), 'small') : null,
|
|
1741
|
-
!readOnly && !editor.isNew && !finalized ? actionButton('标记结束', () => openFinalizeDocument(workspace, editor), 'small') : null,
|
|
1742
|
-
!readOnly && !editor.isNew && !finalized ? actionButton('删除', () => confirmDeleteDocument(workspace, editor), 'ghost small') : null,
|
|
1743
1776
|
!readOnly && !finalized ? actionButton(editor.isNew ? '创建文档' : '保存', () => { void saveDocumentEditor(workspace).then(saved => { if (saved) renderShell() }) }, 'primary small') : null,
|
|
1777
|
+
!readOnly && !editor.isNew ? renderDocumentMenu(editor.title || '知识文档', [
|
|
1778
|
+
[
|
|
1779
|
+
...(documentWorkspaceBases(workspace).some(item => item.id !== editor.knowledgeBaseId)
|
|
1780
|
+
? [{ label: '移动到…', icon: 'move', run: () => openMoveKnowledgeDocument(workspace, editor) }] : []),
|
|
1781
|
+
...(!finalized ? [{ label: '标记结束', icon: 'check', run: () => openFinalizeDocument(workspace, editor) }] : []),
|
|
1782
|
+
],
|
|
1783
|
+
!finalized ? [{ label: '删除文档', icon: 'trash', danger: true, run: () => confirmDeleteDocument(workspace, editor) }] : [],
|
|
1784
|
+
]) : null,
|
|
1744
1785
|
),
|
|
1745
1786
|
),
|
|
1746
1787
|
element('div', { class: 'note-editor-scroll', 'data-scroll-key': 'note-editor' },
|
|
@@ -2660,86 +2701,34 @@ function renderNoteFile(node) {
|
|
|
2660
2701
|
|
|
2661
2702
|
function renderNoteFileToolbar(node, options = {}) {
|
|
2662
2703
|
return element('header', { class: 'notes-document-toolbar' },
|
|
2663
|
-
element('div', { class: 'notes-toolbar-leading' },
|
|
2664
|
-
renderNoteDocumentBreadcrumb(node),
|
|
2665
|
-
),
|
|
2704
|
+
element('div', { class: 'notes-toolbar-leading' }, renderNoteDocumentBreadcrumb(node)),
|
|
2666
2705
|
element('div', { class: 'notes-document-actions' },
|
|
2667
2706
|
options.editable ? element('span', { class: 'notes-save-state', role: 'status', 'data-note-save-state': '', 'data-dirty': String(state.notes.dirty) }, state.notes.dirty ? '未保存' : '已保存') : null,
|
|
2668
|
-
options.enhanced ? actionButton('查找', () => markdownEditorHandle?.openFind(), 'ghost small', { 'data-note-find': '', 'data-note-mobile-overflow': '', 'aria-keyshortcuts': 'Control+F Meta+F', 'aria-pressed': 'false' }) : null,
|
|
2669
2707
|
options.enhanced ? actionButton([
|
|
2670
|
-
interfaceIcon('outline', 'notes-toolbar-action-icon'),
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
options.editable ? actionButton('历史', () => { void openNoteHistory(node) }, 'ghost small', { 'data-note-history': '', 'data-note-mobile-overflow': '' }) : null,
|
|
2674
|
-
noteDownloadButton(node, 'ghost small', '下载', { 'data-note-mobile-overflow': '' }),
|
|
2675
|
-
actionButton(noteShareForNode(node.id) ? '分享中' : '分享', () => { void openNoteShare(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2676
|
-
actionButton('复制引用', () => { void copyNoteReference(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2677
|
-
actionButton('重命名', () => openRenameNoteNode(node), 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2678
|
-
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', 'data-note-mobile-overflow': '', disabled: !state.notes.dirty }) : null,
|
|
2708
|
+
interfaceIcon('outline', 'notes-toolbar-action-icon'), element('span', {}, '大纲'),
|
|
2709
|
+
], () => markdownEditorHandle?.toggleOutline(), 'ghost small notes-outline-action', { 'data-note-outline': '', 'aria-label': '打开标题大纲', title: '标题大纲', 'aria-controls': 'dsh-note-outline', 'aria-expanded': 'false', 'aria-pressed': 'false' }) : null,
|
|
2710
|
+
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', disabled: !state.notes.dirty, 'aria-keyshortcuts': 'Control+S Meta+S' }) : null,
|
|
2679
2711
|
renderNoteFileOverflowMenu(node, options),
|
|
2680
2712
|
),
|
|
2681
2713
|
)
|
|
2682
2714
|
}
|
|
2683
2715
|
|
|
2684
2716
|
function renderNoteFileOverflowMenu(node, options) {
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
interfaceIcon('more', 'notes-document-more-icon'),
|
|
2701
|
-
)
|
|
2702
|
-
details.addEventListener('toggle', () => {
|
|
2703
|
-
summary.setAttribute('aria-expanded', String(details.open))
|
|
2704
|
-
summary.setAttribute('aria-label', `${details.open ? '关闭' : '打开'} ${node.name} 的更多操作`)
|
|
2705
|
-
})
|
|
2706
|
-
const closeThen = action => event => {
|
|
2707
|
-
details.open = false
|
|
2708
|
-
action(event)
|
|
2709
|
-
}
|
|
2710
|
-
const menuItems = []
|
|
2711
|
-
if (options.editable && state.notes.dirty) {
|
|
2712
|
-
menuItems.push(noteMenuAction('保存修改', 'save', closeThen(() => { void saveNoteDocument() })))
|
|
2713
|
-
menuItems.push(noteMenuDivider())
|
|
2714
|
-
}
|
|
2715
|
-
if (options.enhanced) {
|
|
2716
|
-
menuItems.push(noteMenuAction('查找', 'search', closeThen(() => markdownEditorHandle?.openFind())))
|
|
2717
|
-
}
|
|
2718
|
-
if (options.editable) menuItems.push(noteMenuAction('页面历史', 'history', closeThen(() => { void openNoteHistory(node) })))
|
|
2719
|
-
if (options.enhanced || options.editable) menuItems.push(noteMenuDivider())
|
|
2720
|
-
menuItems.push(
|
|
2721
|
-
noteMenuAction('下载', 'download', closeThen(event => { void downloadNoteFile(node, event.currentTarget) })),
|
|
2722
|
-
noteMenuAction(noteShareForNode(node.id) ? '管理分享' : '创建分享', 'link', closeThen(() => { void openNoteShare(node) })),
|
|
2723
|
-
noteMenuAction('复制引用', 'link', closeThen(() => { void copyNoteReference(node) })),
|
|
2724
|
-
noteMenuAction('重命名', 'rename', closeThen(() => openRenameNoteNode(node))),
|
|
2725
|
-
noteMenuDivider(),
|
|
2726
|
-
noteMenuAction('查看已分享', 'outline', closeThen(() => { void navigate('shares') })),
|
|
2727
|
-
)
|
|
2728
|
-
const menu = element('div', { class: 'notes-document-more-menu', role: 'menu', 'aria-label': `${node.name} 的更多操作` }, menuItems)
|
|
2729
|
-
summary.setAttribute('aria-expanded', 'false')
|
|
2730
|
-
details.append(summary, menu)
|
|
2731
|
-
return details
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
function noteMenuAction(label, icon, onClick, attributes = {}) {
|
|
2735
|
-
return actionButton([
|
|
2736
|
-
interfaceIcon(icon, 'notes-document-menu-icon'),
|
|
2737
|
-
element('span', { class: 'notes-document-menu-label' }, label),
|
|
2738
|
-
], onClick, 'ghost small notes-document-menu-item', { role: 'menuitem', ...attributes })
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
function noteMenuDivider() {
|
|
2742
|
-
return element('div', { class: 'notes-document-menu-divider', role: 'separator' })
|
|
2717
|
+
return renderDocumentMenu(node.name, [
|
|
2718
|
+
[
|
|
2719
|
+
...(options.enhanced ? [{ label: '查找', icon: 'search', run: () => markdownEditorHandle?.openFind(), attributes: { 'data-note-find': '', 'aria-keyshortcuts': 'Control+F Meta+F' } }] : []),
|
|
2720
|
+
...(options.editable ? [{ label: '页面历史', icon: 'history', run: () => { void openNoteHistory(node) }, attributes: { 'data-note-history': '' } }] : []),
|
|
2721
|
+
],
|
|
2722
|
+
[
|
|
2723
|
+
{ label: '下载', icon: 'download', run: event => { void downloadNoteFile(node, event.currentTarget) } },
|
|
2724
|
+
{ label: noteShareForNode(node.id) ? '管理分享' : '创建分享', icon: 'link', run: () => { void openNoteShare(node) } },
|
|
2725
|
+
{ label: '复制引用', icon: 'link', run: () => { void copyNoteReference(node) } },
|
|
2726
|
+
],
|
|
2727
|
+
[
|
|
2728
|
+
{ label: '重命名', icon: 'rename', run: () => openRenameNoteNode(node) },
|
|
2729
|
+
{ label: '查看已分享', icon: 'outline', run: () => { void navigate('shares') } },
|
|
2730
|
+
],
|
|
2731
|
+
])
|
|
2743
2732
|
}
|
|
2744
2733
|
|
|
2745
2734
|
function renderNoteFileStatusbar(node) {
|
|
@@ -4202,19 +4191,12 @@ function selectField(label, options, value) {
|
|
|
4202
4191
|
}
|
|
4203
4192
|
|
|
4204
4193
|
async function loadModelCatalog() {
|
|
4205
|
-
if (state.modelCatalog !== null) return state.modelCatalog
|
|
4206
4194
|
try {
|
|
4207
|
-
|
|
4208
|
-
headers: { accept: 'application/json', 'x-dsh-knowledge-client': 'management-web' },
|
|
4209
|
-
})
|
|
4210
|
-
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
4211
|
-
const payload = await response.json()
|
|
4212
|
-
state.modelCatalog = Array.isArray(payload.providers) ? payload.providers : []
|
|
4195
|
+
state.modelCatalog = await readModelCatalog()
|
|
4213
4196
|
} catch (error) {
|
|
4214
|
-
|
|
4215
|
-
showToast(`无法读取当前 DSH 的模型目录:${error instanceof Error ? error.message : String(error)}`, 'error')
|
|
4197
|
+
showToast(`无法读取当前 DSH 的模型目录:${error.message},重新打开可重试。`, 'error')
|
|
4216
4198
|
}
|
|
4217
|
-
return state.modelCatalog
|
|
4199
|
+
return state.modelCatalog || []
|
|
4218
4200
|
}
|
|
4219
4201
|
|
|
4220
4202
|
function modelRouteFields(currentProvider, currentModel) {
|
|
@@ -4434,86 +4416,6 @@ function confirmDeleteToken(token) {
|
|
|
4434
4416
|
} })
|
|
4435
4417
|
}
|
|
4436
4418
|
|
|
4437
|
-
function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
4438
|
-
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
4439
|
-
}
|
|
4440
|
-
|
|
4441
|
-
function openSheet(options) {
|
|
4442
|
-
return openModal({ ...options, presentation: 'sheet' })
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', className = '', onClose }) {
|
|
4446
|
-
const previouslyFocused = document.activeElement
|
|
4447
|
-
const isSheet = presentation === 'sheet'
|
|
4448
|
-
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
4449
|
-
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'} ${className}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
4450
|
-
let busy = false
|
|
4451
|
-
let formDirty = false
|
|
4452
|
-
let closed = false
|
|
4453
|
-
const close = (explicit = false) => {
|
|
4454
|
-
if (busy || closed) return
|
|
4455
|
-
if (!explicit && formDirty) {
|
|
4456
|
-
showToast('表单有未保存的修改,请先保存,或使用“取消”放弃修改。', 'error')
|
|
4457
|
-
return
|
|
4458
|
-
}
|
|
4459
|
-
document.removeEventListener('keydown', onKeyDown)
|
|
4460
|
-
closed = true
|
|
4461
|
-
backdrop.remove()
|
|
4462
|
-
onClose?.()
|
|
4463
|
-
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
4464
|
-
}
|
|
4465
|
-
const closeButton = actionButton('×', () => close(), 'ghost', { 'aria-label': '关闭对话框' })
|
|
4466
|
-
const cancel = actionButton(cancelLabel, () => close(true))
|
|
4467
|
-
const primary = primaryLabel ? actionButton(primaryLabel, async () => {
|
|
4468
|
-
if (busy) return
|
|
4469
|
-
busy = true
|
|
4470
|
-
primary.disabled = true
|
|
4471
|
-
cancel.disabled = true
|
|
4472
|
-
const original = primary.textContent
|
|
4473
|
-
primary.textContent = '正在处理…'
|
|
4474
|
-
try {
|
|
4475
|
-
const shouldClose = await onPrimary()
|
|
4476
|
-
busy = false
|
|
4477
|
-
if (shouldClose !== false) close(true)
|
|
4478
|
-
else { primary.disabled = false; cancel.disabled = false; primary.textContent = original }
|
|
4479
|
-
} catch (error) {
|
|
4480
|
-
busy = false
|
|
4481
|
-
primary.disabled = false
|
|
4482
|
-
cancel.disabled = false
|
|
4483
|
-
primary.textContent = original
|
|
4484
|
-
showToast(friendlyError(error), 'error')
|
|
4485
|
-
}
|
|
4486
|
-
}, primaryVariant) : null
|
|
4487
|
-
const onKeyDown = (event) => {
|
|
4488
|
-
if (event.key === 'Escape') { event.preventDefault(); close() }
|
|
4489
|
-
if (event.key === 'Tab') trapFocus(event, dialog)
|
|
4490
|
-
}
|
|
4491
|
-
dialog.append(
|
|
4492
|
-
element('header', { class: 'dialog-header' }, element('div', {}, element('h2', { id: 'dialog-title' }, title), description ? element('p', {}, description) : null), closeButton),
|
|
4493
|
-
element('div', { class: 'dialog-body' }, body),
|
|
4494
|
-
element('footer', { class: 'dialog-footer' }, cancel, primary),
|
|
4495
|
-
)
|
|
4496
|
-
backdrop.append(dialog)
|
|
4497
|
-
backdrop.addEventListener('mousedown', (event) => { if (event.target === backdrop) close() })
|
|
4498
|
-
if (body.matches?.('form') || body.querySelector?.('form')) {
|
|
4499
|
-
body.addEventListener('input', () => { formDirty = true })
|
|
4500
|
-
body.addEventListener('change', () => { formDirty = true })
|
|
4501
|
-
}
|
|
4502
|
-
document.body.append(backdrop)
|
|
4503
|
-
document.addEventListener('keydown', onKeyDown)
|
|
4504
|
-
window.setTimeout(() => (dialog.querySelector('input, textarea, select, button') || dialog).focus(), 0)
|
|
4505
|
-
return { close, dialog }
|
|
4506
|
-
}
|
|
4507
|
-
|
|
4508
|
-
function trapFocus(event, container) {
|
|
4509
|
-
const focusable = [...container.querySelectorAll('button:not(:disabled), input:not(:disabled), select:not(:disabled), textarea:not(:disabled), [tabindex]:not([tabindex="-1"])')]
|
|
4510
|
-
if (!focusable.length) return
|
|
4511
|
-
const first = focusable[0]
|
|
4512
|
-
const last = focusable.at(-1)
|
|
4513
|
-
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus() }
|
|
4514
|
-
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
|
|
4515
|
-
}
|
|
4516
|
-
|
|
4517
4419
|
function formatDate(value) {
|
|
4518
4420
|
try { return new Intl.DateTimeFormat('zh-CN', { dateStyle: 'medium', timeStyle: 'short' }).format(new Date(value)) } catch { return value }
|
|
4519
4421
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--font-ui:ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei UI", Arial, sans-serif;--font-reading:var(--font-ui);--font-mono:"SFMono-Regular", "Cascadia Code", Menlo, Consolas, monospace;--bg:#ebebeb;--surface:#f4f4f4;--surface-raised:#fafafa;--surface-soft:#eeeeef;--surface-hover:rgb(118 118 128 / 9%);--dialog-surface:#f4f4f4;--menu-surface:rgb(232 234 236 / 98%);--menu-surface-solid:#e8eaec;--document-surface:rgb(218 222 226 / 32%);--selection-background:#b8c8d6;--selection-text:#17212b;--activity-surface:rgba(255, 255, 255, 0.14);--text:#1d1d1f;--text-secondary:#515154;--text-tertiary:#6e6e73;--border:rgb(60 60 67 / 14%);--border-strong:rgb(60 60 67 / 24%);--accent:#3a3a3c;--accent-hover:#1d1d1f;--accent-soft:#e2e2e5;--on-accent:#ffffff;--success:#248a3d;--success-soft:#e8f5eb;--warning:#c93400;--warning-soft:#fff1e8;--danger:#d70015;--danger-soft:#ffebed;--shadow:0 24px 64px rgb(0 0 0 / 20%);--knowledge-embedded-surface:rgba(255, 255, 255, 0.08);--knowledge-embedded-filter:saturate(.65) contrast(1.015) blur(24px);--knowledge-embedded-control:rgba(255, 255, 255, 0.27);}@media(prefers-color-scheme:dark){:root:not([data-color-scheme]){--bg:#101719;--surface:#182022;--surface-raised:#20292b;--surface-soft:rgb(184 204 205 / 6%);--surface-hover:rgb(105 182 186 / 9%);--dialog-surface:#20292b;--menu-surface:rgb(29 38 40 / 98%);--menu-surface-solid:#1d2628;--document-surface:rgb(130 138 146 / 6%);--selection-background:#455b68;--selection-text:#ffffff;--activity-surface:rgb(29 36 38 / 24%);--text:#e3eaeb;--text-secondary:#bbc9cc;--text-tertiary:#95a7ab;--border:rgb(184 204 205 / 10%);--border-strong:rgb(184 204 205 / 18%);--accent:#69b6ba;--accent-hover:#8aced0;--accent-soft:rgb(105 182 186 / 13%);--on-accent:#101819;--success:#30d158;--success-soft:rgb(48 209 88 / 13%);--warning:#ff9f0a;--warning-soft:rgb(255 159 10 / 13%);--danger:#ff453a;--danger-soft:rgb(255 69 58 / 14%);--shadow:0 28px 72px rgb(0 0 0 / 42%);--knowledge-embedded-surface:rgb(16 23 25 / 18%);--knowledge-embedded-filter:saturate(.55) contrast(1.02) blur(24px);--knowledge-embedded-control:rgb(39 50 53 / 72%);}}:root[data-color-scheme="dark"]{--bg:#101719;--surface:#182022;--surface-raised:#20292b;--surface-soft:rgb(184 204 205 / 6%);--surface-hover:rgb(105 182 186 / 9%);--dialog-surface:#20292b;--menu-surface:rgb(29 38 40 / 98%);--menu-surface-solid:#1d2628;--document-surface:rgb(130 138 146 / 6%);--selection-background:#455b68;--selection-text:#ffffff;--activity-surface:rgb(29 36 38 / 24%);--text:#e3eaeb;--text-secondary:#bbc9cc;--text-tertiary:#95a7ab;--border:rgb(184 204 205 / 10%);--border-strong:rgb(184 204 205 / 18%);--accent:#69b6ba;--accent-hover:#8aced0;--accent-soft:rgb(105 182 186 / 13%);--on-accent:#101819;--success:#30d158;--success-soft:rgb(48 209 88 / 13%);--warning:#ff9f0a;--warning-soft:rgb(255 159 10 / 13%);--danger:#ff453a;--danger-soft:rgb(255 69 58 / 14%);--shadow:0 28px 72px rgb(0 0 0 / 42%);--knowledge-embedded-surface:rgb(16 23 25 / 18%);--knowledge-embedded-filter:saturate(.55) contrast(1.02) blur(24px);--knowledge-embedded-control:rgb(39 50 53 / 72%);}
|
package/web/dialogs.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Shared modal lifecycle, keyboard handling and dirty-form protection. */
|
|
2
|
+
export function createDialogPresenter({ element, actionButton, interfaceIcon, showToast, friendlyError }) {
|
|
3
|
+
let sequence = 0
|
|
4
|
+
const stack = []
|
|
5
|
+
function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
6
|
+
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function openSheet(options) {
|
|
10
|
+
return openModal({ ...options, presentation: 'sheet' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', className = '', onClose }) {
|
|
14
|
+
const previouslyFocused = document.activeElement
|
|
15
|
+
const isSheet = presentation === 'sheet'
|
|
16
|
+
const titleId = `knowledge-dialog-title-${++sequence}`
|
|
17
|
+
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
18
|
+
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'} ${className}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': titleId, tabindex: '-1' })
|
|
19
|
+
let busy = false
|
|
20
|
+
let formDirty = false
|
|
21
|
+
let closed = false
|
|
22
|
+
const close = (explicit = false) => {
|
|
23
|
+
if (busy || closed) return
|
|
24
|
+
if (!explicit && formDirty) {
|
|
25
|
+
showToast('表单有未保存的修改,请先保存,或使用“取消”放弃修改。', 'error')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
document.removeEventListener('keydown', onKeyDown)
|
|
29
|
+
closed = true
|
|
30
|
+
stack.splice(stack.indexOf(dialog), 1)
|
|
31
|
+
backdrop.remove()
|
|
32
|
+
onClose?.()
|
|
33
|
+
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
34
|
+
}
|
|
35
|
+
const closeButton = actionButton(interfaceIcon('close'), () => close(), 'ghost', { 'aria-label': '关闭对话框' })
|
|
36
|
+
const cancel = actionButton(cancelLabel, () => close(true))
|
|
37
|
+
const primary = primaryLabel ? actionButton(primaryLabel, async () => {
|
|
38
|
+
if (busy) return
|
|
39
|
+
busy = true
|
|
40
|
+
primary.disabled = true
|
|
41
|
+
cancel.disabled = true
|
|
42
|
+
const original = primary.textContent
|
|
43
|
+
primary.textContent = '正在处理…'
|
|
44
|
+
try {
|
|
45
|
+
const shouldClose = await onPrimary()
|
|
46
|
+
busy = false
|
|
47
|
+
if (shouldClose !== false) close(true)
|
|
48
|
+
else { primary.disabled = false; cancel.disabled = false; primary.textContent = original }
|
|
49
|
+
} catch (error) {
|
|
50
|
+
busy = false
|
|
51
|
+
primary.disabled = false
|
|
52
|
+
cancel.disabled = false
|
|
53
|
+
primary.textContent = original
|
|
54
|
+
showToast(friendlyError(error), 'error')
|
|
55
|
+
}
|
|
56
|
+
}, primaryVariant) : null
|
|
57
|
+
const onKeyDown = (event) => {
|
|
58
|
+
if (stack.at(-1) !== dialog || event.defaultPrevented) return
|
|
59
|
+
if (event.key === 'Escape') { event.preventDefault(); close() }
|
|
60
|
+
if (event.key === 'Tab') trapFocus(event, dialog)
|
|
61
|
+
}
|
|
62
|
+
dialog.append(
|
|
63
|
+
element('header', { class: 'dialog-header' }, element('div', {}, element('h2', { id: titleId }, title), description ? element('p', {}, description) : null), closeButton),
|
|
64
|
+
element('div', { class: 'dialog-body' }, body),
|
|
65
|
+
element('footer', { class: 'dialog-footer' }, cancel, primary),
|
|
66
|
+
)
|
|
67
|
+
backdrop.append(dialog)
|
|
68
|
+
backdrop.addEventListener('mousedown', (event) => { if (event.target === backdrop) close() })
|
|
69
|
+
if (body.matches?.('form') || body.querySelector?.('form')) {
|
|
70
|
+
body.addEventListener('input', () => { formDirty = true })
|
|
71
|
+
body.addEventListener('change', () => { formDirty = true })
|
|
72
|
+
}
|
|
73
|
+
document.body.append(backdrop)
|
|
74
|
+
stack.push(dialog)
|
|
75
|
+
document.addEventListener('keydown', onKeyDown)
|
|
76
|
+
window.setTimeout(() => { if (!closed && stack.at(-1) === dialog) (focusableElements(dialog)[0] || dialog).focus() }, 0)
|
|
77
|
+
return { close, dialog }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function trapFocus(event, container) {
|
|
81
|
+
const focusable = focusableElements(container)
|
|
82
|
+
if (!focusable.length) return
|
|
83
|
+
const first = focusable[0]
|
|
84
|
+
const last = focusable.at(-1)
|
|
85
|
+
if (event.shiftKey && (document.activeElement === first || !container.contains(document.activeElement))) { event.preventDefault(); last.focus() }
|
|
86
|
+
else if (!event.shiftKey && (document.activeElement === last || !container.contains(document.activeElement))) { event.preventDefault(); first.focus() }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function focusableElements(container) {
|
|
90
|
+
return [...container.querySelectorAll('button, input, select, textarea, a[href], [tabindex]')]
|
|
91
|
+
.filter(node => !node.disabled && node.tabIndex >= 0 && !node.closest('[hidden], [inert], [aria-hidden="true"]') && node.getClientRects().length > 0)
|
|
92
|
+
}
|
|
93
|
+
return { openConfirm, openSheet, openModal }
|
|
94
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** Shared document overflow: one action list and keyboard model for every width. */
|
|
2
|
+
export function createDocumentMenuPresenter({ element, actionButton, interfaceIcon }) {
|
|
3
|
+
const openMenus = new Set()
|
|
4
|
+
const closeMenus = () => { for (const close of [...openMenus]) close() }
|
|
5
|
+
function renderMenu(label, groups) {
|
|
6
|
+
const actions = groups.filter(group => group.length)
|
|
7
|
+
if (!actions.length) return null
|
|
8
|
+
let events
|
|
9
|
+
const details = element('details', { class: 'notes-document-more' })
|
|
10
|
+
const summary = element('summary', {
|
|
11
|
+
class: 'button ghost small', title: '更多操作', 'aria-label': `${label}的更多操作`,
|
|
12
|
+
'aria-haspopup': 'menu', 'aria-expanded': 'false',
|
|
13
|
+
onClick: event => { event.preventDefault(); setOpen(!details.open) },
|
|
14
|
+
}, interfaceIcon('more', 'notes-document-more-icon'))
|
|
15
|
+
const close = (restoreFocus = false) => {
|
|
16
|
+
details.open = false
|
|
17
|
+
summary.setAttribute('aria-expanded', 'false')
|
|
18
|
+
events?.abort()
|
|
19
|
+
events = undefined
|
|
20
|
+
openMenus.delete(close)
|
|
21
|
+
if (restoreFocus && summary.isConnected) summary.focus()
|
|
22
|
+
}
|
|
23
|
+
const menu = element('div', { class: 'notes-document-more-menu', role: 'menu', 'aria-label': `${label}的更多操作` },
|
|
24
|
+
actions.flatMap((group, index) => [
|
|
25
|
+
index ? element('div', { class: 'notes-document-menu-divider', role: 'separator' }) : null,
|
|
26
|
+
...group.map(action => actionButton([
|
|
27
|
+
interfaceIcon(action.icon, 'notes-document-menu-icon'),
|
|
28
|
+
element('span', { class: 'notes-document-menu-label' }, action.label),
|
|
29
|
+
], event => { close(true); action.run(event) }, `ghost small notes-document-menu-item${action.danger ? ' is-danger' : ''}`, {
|
|
30
|
+
role: 'menuitem', tabindex: '-1', ...action.attributes,
|
|
31
|
+
})),
|
|
32
|
+
]),
|
|
33
|
+
)
|
|
34
|
+
const items = () => [...menu.querySelectorAll('[role="menuitem"]:not(:disabled)')]
|
|
35
|
+
const setOpen = (open, last = false) => {
|
|
36
|
+
if (!open) return close(true)
|
|
37
|
+
closeMenus()
|
|
38
|
+
details.open = true
|
|
39
|
+
summary.setAttribute('aria-expanded', 'true')
|
|
40
|
+
openMenus.add(close)
|
|
41
|
+
const bounds = summary.getBoundingClientRect()
|
|
42
|
+
menu.style.maxHeight = `${Math.max(100, window.innerHeight - bounds.bottom - 16)}px`
|
|
43
|
+
events = new AbortController()
|
|
44
|
+
const { signal } = events
|
|
45
|
+
document.addEventListener('pointerdown', event => { if (!details.contains(event.target)) close() }, { signal })
|
|
46
|
+
window.addEventListener('resize', () => close(), { signal, once: true })
|
|
47
|
+
const choices = items()
|
|
48
|
+
choices[last ? choices.length - 1 : 0]?.focus()
|
|
49
|
+
}
|
|
50
|
+
details.addEventListener('keydown', event => {
|
|
51
|
+
if (event.key === 'Escape' && details.open) { event.preventDefault(); event.stopPropagation(); close(true); return }
|
|
52
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return
|
|
53
|
+
event.preventDefault()
|
|
54
|
+
if (!details.open) return setOpen(true, event.key === 'ArrowUp' || event.key === 'End')
|
|
55
|
+
const choices = items()
|
|
56
|
+
const index = choices.indexOf(document.activeElement)
|
|
57
|
+
const next = event.key === 'Home' ? 0 : event.key === 'End' ? choices.length - 1
|
|
58
|
+
: (index + (event.key === 'ArrowDown' ? 1 : -1) + choices.length) % choices.length
|
|
59
|
+
choices[next]?.focus()
|
|
60
|
+
})
|
|
61
|
+
details.addEventListener('focusout', event => {
|
|
62
|
+
if (event.relatedTarget && !details.contains(event.relatedTarget)) close()
|
|
63
|
+
})
|
|
64
|
+
details.append(summary, menu)
|
|
65
|
+
return details
|
|
66
|
+
}
|
|
67
|
+
return { renderMenu, closeMenus }
|
|
68
|
+
}
|
package/web/index.html
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<meta name="dsh-knowledge-web" content="__DSH_KNOWLEDGE_WEB_PATH__">
|
|
11
11
|
<meta name="dsh-knowledge-asset-version" content="__DSH_KNOWLEDGE_ASSET_VERSION__">
|
|
12
12
|
<title>DSH 知识库</title>
|
|
13
|
+
<link rel="stylesheet" href="__DSH_KNOWLEDGE_WEB_PATH__/design-tokens.css?v=__DSH_KNOWLEDGE_ASSET_VERSION__">
|
|
13
14
|
<link rel="stylesheet" href="__DSH_KNOWLEDGE_WEB_PATH__/styles.css?v=__DSH_KNOWLEDGE_ASSET_VERSION__">
|
|
14
15
|
<script src="__DSH_KNOWLEDGE_WEB_PATH__/markdown-preview.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
15
16
|
<script src="__DSH_KNOWLEDGE_WEB_PATH__/change-review.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Cache successful model discovery; share in-flight reads and allow retry after failure. */
|
|
2
|
+
export function createModelCatalogLoader(request = fetch, ttlMs = 60000) {
|
|
3
|
+
let cached
|
|
4
|
+
let expires = 0
|
|
5
|
+
let pending
|
|
6
|
+
return function load() {
|
|
7
|
+
if (cached !== undefined && Date.now() < expires) return Promise.resolve(cached)
|
|
8
|
+
if (pending) return pending
|
|
9
|
+
pending = (async () => {
|
|
10
|
+
const response = await request('/knowledge-control/v1/models', {
|
|
11
|
+
headers: { accept: 'application/json', 'x-dsh-knowledge-client': 'management-web' },
|
|
12
|
+
})
|
|
13
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
|
|
14
|
+
const payload = await response.json()
|
|
15
|
+
if (!Array.isArray(payload.providers)) throw new Error('模型目录返回了无效数据')
|
|
16
|
+
cached = payload.providers
|
|
17
|
+
expires = Date.now() + ttlMs
|
|
18
|
+
return cached
|
|
19
|
+
})().finally(() => { pending = undefined })
|
|
20
|
+
return pending
|
|
21
|
+
}
|
|
22
|
+
}
|