@lemoncat7/dsh-knowledge 0.6.0 → 0.7.2
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 +12 -4
- package/docs/architecture.md +7 -6
- package/docs/document-knowledge-design.zh-CN.md +2 -2
- package/docs/requirements.md +4 -1
- package/lib/api.js +23 -1
- package/lib/api.js.map +1 -1
- package/lib/client.js +1 -0
- package/lib/client.js.map +2 -2
- package/lib/domain.d.ts +15 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +6 -0
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +70 -22
- package/lib/extraction.js.map +1 -1
- package/lib/local-provider.d.ts +8 -1
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +220 -11
- package/lib/local-provider.js.map +1 -1
- package/lib/provider.d.ts +4 -1
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +4 -1
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +9 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/tools.d.ts +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +148 -1
- package/lib/tools.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +255 -148
- package/web/styles.css +248 -127
package/web/app.js
CHANGED
|
@@ -11,6 +11,16 @@ const TYPE_DOCUMENTS = {
|
|
|
11
11
|
preference: 'preferences.md', fact: 'facts.md', decision: 'decisions.md', procedure: 'procedures.md', lesson: 'lessons.md',
|
|
12
12
|
}
|
|
13
13
|
const DOCUMENT_LAYOUT_KEY = 'dsh-knowledge.document-layout'
|
|
14
|
+
const DOCUMENT_COLUMN_LAYOUTS = Object.freeze({
|
|
15
|
+
library: {
|
|
16
|
+
widthKey: 'libraryWidth', hiddenKey: 'libraryHidden', cssVariable: '--library-width',
|
|
17
|
+
minimum: 170, maximum: 360, controls: 'knowledge-library-column',
|
|
18
|
+
},
|
|
19
|
+
documentList: {
|
|
20
|
+
widthKey: 'documentListWidth', hiddenKey: 'documentListHidden', cssVariable: '--document-list-width',
|
|
21
|
+
minimum: 210, maximum: 480, controls: 'document-list-column',
|
|
22
|
+
},
|
|
23
|
+
})
|
|
14
24
|
const pageParams = new URLSearchParams(location.search)
|
|
15
25
|
const mountContext = {
|
|
16
26
|
sessionId: pageParams.get('sessionId')?.trim() || '',
|
|
@@ -27,6 +37,8 @@ const state = {
|
|
|
27
37
|
stats: null,
|
|
28
38
|
overview: null,
|
|
29
39
|
knowledgeBases: [],
|
|
40
|
+
knowledgeBaseView: 'libraries',
|
|
41
|
+
knowledgeBaseQuery: '',
|
|
30
42
|
mounts: [],
|
|
31
43
|
resolvedMounts: [],
|
|
32
44
|
mountContext,
|
|
@@ -51,12 +63,17 @@ const state = {
|
|
|
51
63
|
},
|
|
52
64
|
candidates: [],
|
|
53
65
|
candidateStatus: 'pending',
|
|
66
|
+
settings: { writebackPolicy: 'conservative', updatedAt: '' },
|
|
67
|
+
settingsSaving: false,
|
|
54
68
|
tokens: [],
|
|
55
69
|
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
70
|
+
scrollPositions: new Map(),
|
|
56
71
|
loading: false,
|
|
57
72
|
error: '',
|
|
58
73
|
}
|
|
59
74
|
|
|
75
|
+
let scrollRestoreFrame = 0
|
|
76
|
+
|
|
60
77
|
function readDocumentLayout() {
|
|
61
78
|
const fallback = {
|
|
62
79
|
sidebarHidden: false, libraryHidden: false, documentListHidden: false,
|
|
@@ -93,6 +110,37 @@ function clampNumber(value, minimum, maximum, fallback) {
|
|
|
93
110
|
return Number.isFinite(number) ? Math.min(maximum, Math.max(minimum, Math.round(number))) : fallback
|
|
94
111
|
}
|
|
95
112
|
|
|
113
|
+
function captureScrollPosition() {
|
|
114
|
+
const shell = app.querySelector('.app-shell[data-view]')
|
|
115
|
+
if (!shell || shell.dataset.loading === 'true') return
|
|
116
|
+
const regions = {}
|
|
117
|
+
shell.querySelectorAll('[data-scroll-key]').forEach(node => {
|
|
118
|
+
const key = node.getAttribute('data-scroll-key')
|
|
119
|
+
if (key) regions[key] = { left: node.scrollLeft, top: node.scrollTop }
|
|
120
|
+
})
|
|
121
|
+
state.scrollPositions.set(shell.dataset.view, {
|
|
122
|
+
window: { left: window.scrollX, top: window.scrollY },
|
|
123
|
+
regions,
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function restoreScrollPosition(view) {
|
|
128
|
+
if (state.loading) return
|
|
129
|
+
const saved = state.scrollPositions.get(view)
|
|
130
|
+
if (!saved) return
|
|
131
|
+
if (scrollRestoreFrame) window.cancelAnimationFrame(scrollRestoreFrame)
|
|
132
|
+
scrollRestoreFrame = window.requestAnimationFrame(() => {
|
|
133
|
+
window.scrollTo(saved.window.left, saved.window.top)
|
|
134
|
+
app.querySelectorAll('[data-scroll-key]').forEach(node => {
|
|
135
|
+
const position = saved.regions[node.getAttribute('data-scroll-key')]
|
|
136
|
+
if (!position) return
|
|
137
|
+
node.scrollLeft = position.left
|
|
138
|
+
node.scrollTop = position.top
|
|
139
|
+
})
|
|
140
|
+
scrollRestoreFrame = 0
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
96
144
|
function element(tag, attributes = {}, ...children) {
|
|
97
145
|
const node = document.createElement(tag)
|
|
98
146
|
for (const [key, value] of Object.entries(attributes)) {
|
|
@@ -122,6 +170,16 @@ function paneToggleButton(pane, visible, onClick, label) {
|
|
|
122
170
|
}, element('span', { class: `pane-icon pane-icon-${pane}`, 'aria-hidden': 'true' }))
|
|
123
171
|
}
|
|
124
172
|
|
|
173
|
+
function interfaceIcon(name, className = 'interface-icon') {
|
|
174
|
+
const paths = {
|
|
175
|
+
search: 'M10.8 4.5a6.3 6.3 0 1 0 0 12.6 6.3 6.3 0 0 0 0-12.6Zm4.6 11 4.1 4',
|
|
176
|
+
}
|
|
177
|
+
return element('svg', {
|
|
178
|
+
class: className, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
179
|
+
'stroke-width': '1.8', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
180
|
+
}, element('path', { d: paths[name] }))
|
|
181
|
+
}
|
|
182
|
+
|
|
125
183
|
function badge(label, variant = '') {
|
|
126
184
|
return element('span', { class: `badge ${variant}`.trim() }, label)
|
|
127
185
|
}
|
|
@@ -221,7 +279,7 @@ function renderLogin(message = '') {
|
|
|
221
279
|
|
|
222
280
|
function signOut() {
|
|
223
281
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
224
|
-
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], tokens: [] })
|
|
282
|
+
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], settings: { writebackPolicy: 'conservative', updatedAt: '' }, tokens: [] })
|
|
225
283
|
if (AUTH_MODE === 'same-origin') void boot()
|
|
226
284
|
else renderLogin()
|
|
227
285
|
}
|
|
@@ -257,15 +315,16 @@ async function ensureKnowledgeBases(force = false) {
|
|
|
257
315
|
}
|
|
258
316
|
|
|
259
317
|
async function loadKnowledgeBasesPage() {
|
|
260
|
-
const requests = [api('knowledge-bases'), api('mounts')]
|
|
318
|
+
const requests = [api('knowledge-bases'), api('mounts'), api('settings')]
|
|
261
319
|
if (state.mountContext.sessionId) {
|
|
262
320
|
const params = new URLSearchParams({ sessionId: state.mountContext.sessionId })
|
|
263
321
|
if (state.mountContext.projectId) params.set('projectId', state.mountContext.projectId)
|
|
264
322
|
requests.push(api(`mounts/resolve?${params}`))
|
|
265
323
|
}
|
|
266
|
-
const [bases, mounts, resolved = []] = await Promise.all(requests)
|
|
324
|
+
const [bases, mounts, settings, resolved = []] = await Promise.all(requests)
|
|
267
325
|
state.knowledgeBases = bases
|
|
268
326
|
state.mounts = mounts
|
|
327
|
+
state.settings = settings
|
|
269
328
|
state.resolvedMounts = resolved
|
|
270
329
|
await refreshStats()
|
|
271
330
|
}
|
|
@@ -346,6 +405,7 @@ async function loadTokens() {
|
|
|
346
405
|
}
|
|
347
406
|
|
|
348
407
|
function renderShell() {
|
|
408
|
+
captureScrollPosition()
|
|
349
409
|
const titles = {
|
|
350
410
|
overview: ['概览', '知识库运行状态与最近活动'],
|
|
351
411
|
bases: ['知识库', '创建知识库,并限定项目与会话的召回和写入范围'],
|
|
@@ -356,6 +416,7 @@ function renderShell() {
|
|
|
356
416
|
const [title, subtitle] = titles[state.view]
|
|
357
417
|
const shell = element('div', {
|
|
358
418
|
class: 'app-shell', 'data-menu-open': String(state.menuOpen),
|
|
419
|
+
'data-view': state.view, 'data-loading': String(state.loading),
|
|
359
420
|
'data-sidebar-hidden': String(state.documentView.sidebarHidden),
|
|
360
421
|
style: `--sidebar-width: ${state.documentView.sidebarWidth}px`,
|
|
361
422
|
},
|
|
@@ -376,6 +437,7 @@ function renderShell() {
|
|
|
376
437
|
if (event.target === shell) { state.menuOpen = false; renderShell() }
|
|
377
438
|
})
|
|
378
439
|
app.replaceChildren(shell)
|
|
440
|
+
restoreScrollPosition(state.view)
|
|
379
441
|
}
|
|
380
442
|
|
|
381
443
|
function renderAppSidebarResizer() {
|
|
@@ -445,19 +507,21 @@ function setSidebarHidden(hidden) {
|
|
|
445
507
|
|
|
446
508
|
function renderSidebar() {
|
|
447
509
|
const pending = state.stats?.candidates.pending
|
|
448
|
-
const
|
|
449
|
-
['
|
|
450
|
-
|
|
510
|
+
const navGroups = [
|
|
511
|
+
['工作区', [['overview', '概览'], ['bases', '知识库'], ['entries', '文档'], ['candidates', '审核']]],
|
|
512
|
+
['服务', [['tokens', '访问管理']].filter(([id]) => id !== 'tokens' || !state.service.remote)],
|
|
513
|
+
].filter(([, items]) => items.length)
|
|
451
514
|
return element('aside', { class: 'sidebar', 'aria-label': '知识库导航' },
|
|
452
515
|
element('div', { class: 'brand' },
|
|
453
|
-
element('div', { class: 'brand-
|
|
454
|
-
element('div', { class: 'brand-copy' }, element('strong', {}, 'DSH Knowledge'), element('span', {}, '管理控制台')),
|
|
516
|
+
element('div', { class: 'brand-copy' }, element('span', {}, 'DSH Knowledge'), element('strong', {}, '知识库')),
|
|
455
517
|
),
|
|
456
|
-
element('nav', { class: 'nav' },
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
518
|
+
element('nav', { class: 'nav' }, navGroups.map(([group, items]) => element('div', { class: 'nav-group' },
|
|
519
|
+
element('div', { class: 'nav-group-label' }, group),
|
|
520
|
+
items.map(([id, label]) => element('button', {
|
|
521
|
+
type: 'button', class: 'nav-button', 'aria-current': state.view === id ? 'page' : undefined,
|
|
522
|
+
onClick: () => navigate(id),
|
|
523
|
+
}, element('span', { class: 'nav-label' }, label),
|
|
524
|
+
id === 'candidates' && pending ? element('span', { class: 'nav-count', 'aria-label': `${pending} 条待审核` }, pending) : null))))),
|
|
461
525
|
element('div', { class: 'sidebar-footer' },
|
|
462
526
|
element('div', { class: 'connection' }, element('span', { class: 'status-dot', 'aria-hidden': 'true' }), state.service.remote ? '中央知识库已连接' : '本地知识库已连接'),
|
|
463
527
|
AUTH_MODE === 'bearer' ? actionButton('退出当前会话', signOut, 'ghost small') : null,
|
|
@@ -476,7 +540,16 @@ function renderCurrentView() {
|
|
|
476
540
|
}
|
|
477
541
|
|
|
478
542
|
function loadingView() {
|
|
479
|
-
return element('div', { class: 'loading', role: 'status'
|
|
543
|
+
return element('div', { class: 'loading-skeleton', role: 'status', 'aria-label': '正在加载' },
|
|
544
|
+
element('span', { class: 'visually-hidden' }, '正在加载'),
|
|
545
|
+
element('div', { class: 'skeleton-line skeleton-title', 'aria-hidden': 'true' }),
|
|
546
|
+
element('div', { class: 'skeleton-line skeleton-copy', 'aria-hidden': 'true' }),
|
|
547
|
+
element('div', { class: 'skeleton-grid', 'aria-hidden': 'true' },
|
|
548
|
+
element('div', { class: 'skeleton-block' }),
|
|
549
|
+
element('div', { class: 'skeleton-block' }),
|
|
550
|
+
element('div', { class: 'skeleton-block' }),
|
|
551
|
+
),
|
|
552
|
+
)
|
|
480
553
|
}
|
|
481
554
|
|
|
482
555
|
function errorView(message, retry) {
|
|
@@ -516,24 +589,60 @@ function renderKnowledgeBases() {
|
|
|
516
589
|
const activeBases = state.knowledgeBases.filter(base => base.status === 'active')
|
|
517
590
|
const archivedBases = state.knowledgeBases.filter(base => base.status === 'archived')
|
|
518
591
|
const contextAvailable = Boolean(state.mountContext.projectId || state.mountContext.sessionId)
|
|
592
|
+
const query = state.knowledgeBaseQuery.trim().toLocaleLowerCase()
|
|
593
|
+
const matchesQuery = base => !query || [base.name, base.description, base.defaultTags.join(' '), base.writebackProvider, base.writebackModel]
|
|
594
|
+
.some(value => String(value || '').toLocaleLowerCase().includes(query))
|
|
595
|
+
const visibleActiveBases = activeBases.filter(matchesQuery)
|
|
596
|
+
const visibleArchivedBases = archivedBases.filter(matchesQuery)
|
|
597
|
+
const switcher = element('div', { class: 'workspace-switcher' },
|
|
598
|
+
element('div', { class: 'workspace-switcher-leading' },
|
|
599
|
+
element('div', { class: 'tabs workspace-tabs', role: 'tablist', 'aria-label': '知识库管理范围' }, [
|
|
600
|
+
['libraries', '知识库', activeBases.length],
|
|
601
|
+
['mounts', '项目与会话挂载', state.mounts.filter(mount => mount.enabled).length],
|
|
602
|
+
].map(([id, label, count]) => element('button', {
|
|
603
|
+
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.knowledgeBaseView === id),
|
|
604
|
+
onClick: () => { state.knowledgeBaseView = id; renderShell() },
|
|
605
|
+
}, element('span', {}, label), element('span', { class: 'tab-count' }, count)))),
|
|
606
|
+
renderWritebackPolicyControl(),
|
|
607
|
+
),
|
|
608
|
+
element('p', {}, state.settings.writebackPolicy === 'conservative'
|
|
609
|
+
? '严谨模式只保留明确或已验证的长期知识,宁可漏记也不堆积噪声。'
|
|
610
|
+
: '主动模式会更积极地沉淀可复用内容,仍会排除敏感信息和临时输出。'),
|
|
611
|
+
)
|
|
519
612
|
return element('div', { class: 'bases-page' },
|
|
520
|
-
|
|
613
|
+
switcher,
|
|
614
|
+
state.knowledgeBaseView === 'libraries' ? element('section', { class: 'library-management', 'aria-labelledby': 'bases-heading' },
|
|
521
615
|
element('div', { class: 'section-heading' },
|
|
522
|
-
element('div', {}, element('h2', { id: 'bases-heading' },
|
|
616
|
+
element('div', {}, element('h2', { id: 'bases-heading' }, '我的知识库'), element('p', {}, '名称和描述帮助 AI 判断知识应该写到哪里。')),
|
|
523
617
|
actionButton('+ 创建知识库', () => openKnowledgeBaseEditor(), 'primary'),
|
|
524
618
|
),
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
619
|
+
element('div', { class: 'knowledge-base-toolbar' },
|
|
620
|
+
element('div', { class: 'search-box base-search' }, interfaceIcon('search', 'search-symbol'), element('input', {
|
|
621
|
+
class: 'input', type: 'search', value: state.knowledgeBaseQuery,
|
|
622
|
+
placeholder: '搜索名称、描述、标签或模型', 'aria-label': '搜索知识库',
|
|
623
|
+
onInput: event => {
|
|
624
|
+
state.knowledgeBaseQuery = event.target.value
|
|
625
|
+
renderShell()
|
|
626
|
+
document.querySelector('.base-search input')?.focus()
|
|
627
|
+
},
|
|
628
|
+
})),
|
|
629
|
+
element('div', { class: 'base-result-summary', 'aria-live': 'polite' }, query
|
|
630
|
+
? `找到 ${visibleActiveBases.length + visibleArchivedBases.length} 个知识库`
|
|
631
|
+
: `${activeBases.length} 个可用 · ${archivedBases.length} 个已归档`),
|
|
632
|
+
),
|
|
633
|
+
visibleActiveBases.length
|
|
634
|
+
? element('div', { class: 'base-grid' }, visibleActiveBases.map(renderKnowledgeBaseCard))
|
|
635
|
+
: query && visibleArchivedBases.length === 0
|
|
636
|
+
? emptyState('没有匹配的知识库', '尝试搜索名称、描述、标签或回写模型。')
|
|
637
|
+
: !query ? emptyState('还没有可用知识库', '使用右上角按钮创建第一个知识库。') : null,
|
|
638
|
+
visibleArchivedBases.length ? element('details', { class: 'archived-bases', open: Boolean(query) },
|
|
639
|
+
element('summary', {}, element('span', {}, '已归档知识库'), element('span', { class: 'summary-count' }, visibleArchivedBases.length)),
|
|
640
|
+
element('div', { class: 'base-grid' }, visibleArchivedBases.map(renderKnowledgeBaseCard)),
|
|
531
641
|
) : null,
|
|
532
|
-
),
|
|
533
|
-
element('section', { class: 'mount-section', 'aria-labelledby': 'mounts-heading' },
|
|
642
|
+
) : element('section', { class: 'mount-section', 'aria-labelledby': 'mounts-heading' },
|
|
534
643
|
element('div', { class: 'section-heading' }, element('div', {},
|
|
535
|
-
element('h2', { id: 'mounts-heading' }, '
|
|
536
|
-
element('p', {}, '
|
|
644
|
+
element('h2', { id: 'mounts-heading' }, '挂载范围'),
|
|
645
|
+
element('p', {}, '会话默认继承项目;只有需要差异时才创建会话覆盖。'),
|
|
537
646
|
)),
|
|
538
647
|
contextAvailable
|
|
539
648
|
? element('div', { class: 'mount-context' },
|
|
@@ -548,27 +657,67 @@ function renderKnowledgeBases() {
|
|
|
548
657
|
)
|
|
549
658
|
}
|
|
550
659
|
|
|
660
|
+
function renderWritebackPolicyControl() {
|
|
661
|
+
const policies = [
|
|
662
|
+
['conservative', '严谨'],
|
|
663
|
+
['proactive', '主动'],
|
|
664
|
+
]
|
|
665
|
+
return element('div', {
|
|
666
|
+
class: 'writeback-policy-control',
|
|
667
|
+
'aria-label': '全局回写策略',
|
|
668
|
+
'aria-busy': String(state.settingsSaving),
|
|
669
|
+
},
|
|
670
|
+
element('span', { class: 'writeback-policy-label' }, '回写策略'),
|
|
671
|
+
element('div', { class: 'writeback-policy-segments', role: 'radiogroup', 'aria-label': '选择全局回写策略' }, policies.map(([value, label]) => element('button', {
|
|
672
|
+
type: 'button', role: 'radio', class: 'writeback-policy-option',
|
|
673
|
+
'aria-checked': String(state.settings.writebackPolicy === value),
|
|
674
|
+
disabled: state.settingsSaving,
|
|
675
|
+
onClick: () => void updateWritebackPolicy(value),
|
|
676
|
+
}, label))))
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function updateWritebackPolicy(writebackPolicy) {
|
|
680
|
+
if (state.settingsSaving || state.settings.writebackPolicy === writebackPolicy) return
|
|
681
|
+
const previous = state.settings
|
|
682
|
+
state.settings = { ...state.settings, writebackPolicy }
|
|
683
|
+
state.settingsSaving = true
|
|
684
|
+
renderShell()
|
|
685
|
+
try {
|
|
686
|
+
state.settings = await api('settings', { method: 'PUT', body: { patch: { writebackPolicy } } })
|
|
687
|
+
showToast(`全局回写策略已切换为${writebackPolicy === 'conservative' ? '严谨' : '主动'}模式`)
|
|
688
|
+
} catch (error) {
|
|
689
|
+
state.settings = previous
|
|
690
|
+
showToast(friendlyError(error), 'error')
|
|
691
|
+
} finally {
|
|
692
|
+
state.settingsSaving = false
|
|
693
|
+
renderShell()
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
551
697
|
function contextPill(label, value) {
|
|
552
698
|
return element('div', { class: 'context-pill' }, element('strong', {}, label), element('span', { title: value }, value))
|
|
553
699
|
}
|
|
554
700
|
|
|
555
701
|
function renderKnowledgeBaseCard(base) {
|
|
556
702
|
const archived = base.status === 'archived'
|
|
703
|
+
const visibleTags = base.defaultTags.slice(0, 4)
|
|
704
|
+
const hiddenTagCount = Math.max(0, base.defaultTags.length - visibleTags.length)
|
|
557
705
|
return element('article', { class: `base-card${archived ? ' is-archived' : ''}` },
|
|
558
706
|
element('div', { class: 'base-card-header' },
|
|
559
|
-
element('div', {
|
|
707
|
+
element('div', { class: 'base-card-identity' },
|
|
708
|
+
element('span', { class: 'base-symbol', 'aria-hidden': 'true' }, base.name.trim().slice(0, 1).toLocaleUpperCase() || 'K'),
|
|
709
|
+
element('div', {}, element('h3', {}, base.name), element('small', { title: base.id }, base.id === 'default' ? '系统默认库' : `ID · ${base.id}`)),
|
|
710
|
+
),
|
|
560
711
|
badge(archived ? '已归档' : '可用', archived ? '' : 'success'),
|
|
561
712
|
),
|
|
562
|
-
element('
|
|
563
|
-
base.
|
|
564
|
-
?
|
|
565
|
-
: element('span', { class: '
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
:
|
|
569
|
-
|
|
570
|
-
element('strong', {}, '回写模型'),
|
|
571
|
-
badge(base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : '跟随当前会话'),
|
|
713
|
+
element('p', { class: 'base-description' }, base.description || '通用知识库,尚未设置匹配描述。'),
|
|
714
|
+
element('div', { class: 'base-card-tags' }, visibleTags.length
|
|
715
|
+
? visibleTags.map(tag => element('span', { class: 'tag' }, `#${tag}`))
|
|
716
|
+
: element('span', { class: 'tag is-empty' }, '无默认标签'),
|
|
717
|
+
hiddenTagCount ? element('span', { class: 'tag' }, `+${hiddenTagCount}`) : null),
|
|
718
|
+
element('div', { class: 'base-card-meta' },
|
|
719
|
+
element('span', {}, element('strong', {}, '回写模型'), base.writebackProvider && base.writebackModel ? `${base.writebackProvider} / ${base.writebackModel}` : '跟随当前会话'),
|
|
720
|
+
base.extractionInstructions ? element('span', {}, element('strong', {}, '提取规则'), '已设置') : null,
|
|
572
721
|
),
|
|
573
722
|
element('div', { class: 'base-card-actions' },
|
|
574
723
|
actionButton('查看知识', () => {
|
|
@@ -657,7 +806,7 @@ function renderMountManager(activeBases) {
|
|
|
657
806
|
renderShell()
|
|
658
807
|
},
|
|
659
808
|
}, label))),
|
|
660
|
-
element('div', { class: 'search-box mount-search' },
|
|
809
|
+
element('div', { class: 'search-box mount-search' }, interfaceIcon('search', 'search-symbol'), searchInput),
|
|
661
810
|
filter,
|
|
662
811
|
),
|
|
663
812
|
element('div', { class: 'mount-target-line' },
|
|
@@ -666,7 +815,7 @@ function renderMountManager(activeBases) {
|
|
|
666
815
|
element('span', { class: 'field-hint' }, `显示 ${visibleRows.length} / ${activeBases.length}`),
|
|
667
816
|
),
|
|
668
817
|
visibleRows.length
|
|
669
|
-
? element('div', { class: 'mount-table', role: 'list', 'aria-label': '知识库挂载列表' }, visibleRows.map(({ base, view }) => renderMountListRow(base, view, manager.targetKind, targetId)))
|
|
818
|
+
? element('div', { class: 'mount-table', role: 'list', 'aria-label': '知识库挂载列表', 'data-scroll-key': 'mount-table' }, visibleRows.map(({ base, view }) => renderMountListRow(base, view, manager.targetKind, targetId)))
|
|
670
819
|
: emptyState('没有匹配的知识库', '调整搜索词或筛选条件。'),
|
|
671
820
|
element('div', { class: 'mount-bulk-bar', 'aria-live': 'polite' },
|
|
672
821
|
element('strong', {}, `已选择 ${selectedCount} 个`),
|
|
@@ -760,19 +909,16 @@ function renderEntries() {
|
|
|
760
909
|
|
|
761
910
|
return element('section', { class: 'document-page', 'aria-labelledby': 'documents-heading' },
|
|
762
911
|
element('div', { class: 'document-page-toolbar' },
|
|
763
|
-
|
|
912
|
+
renderDocumentModeTabs(),
|
|
913
|
+
element('div', { class: 'search-box document-global-search' }, interfaceIcon('search', 'search-symbol'), search),
|
|
764
914
|
element('select', {
|
|
765
915
|
class: 'select compact-library-picker', 'aria-label': '选择知识库', value: view.knowledgeBaseId,
|
|
766
916
|
onChange: (event) => { view.knowledgeBaseId = event.target.value; view.documentId = ''; selectDefaultDocument(); renderShell() },
|
|
767
917
|
}, activeBases.map(base => element('option', { value: base.id, selected: base.id === view.knowledgeBaseId }, base.name))),
|
|
768
|
-
element('div', { class: '
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
paneToggleButton('library', !view.libraryHidden, () => setDocumentColumnHidden('library', !view.libraryHidden), '知识库二级栏'),
|
|
772
|
-
paneToggleButton('document', !view.documentListHidden, () => setDocumentColumnHidden('documentList', !view.documentListHidden), '文档列表栏'),
|
|
918
|
+
element('div', { class: 'document-toolbar-actions' },
|
|
919
|
+
renderDocumentColumnControls(),
|
|
920
|
+
actionButton('+ 新建知识', () => openEntryEditor(), 'primary'),
|
|
773
921
|
),
|
|
774
|
-
actionButton('调整栏宽', openLayoutEditor, 'ghost small', { 'aria-label': '调整主导航、知识库和文档栏宽度' }),
|
|
775
|
-
actionButton('+ 新建知识', () => openEntryEditor(), 'primary'),
|
|
776
922
|
),
|
|
777
923
|
element('div', {
|
|
778
924
|
class: 'document-browser',
|
|
@@ -780,11 +926,11 @@ function renderEntries() {
|
|
|
780
926
|
'data-document-list-hidden': String(view.documentListHidden),
|
|
781
927
|
style: `--library-width:${view.libraryWidth}px;--document-list-width:${view.documentListWidth}px`,
|
|
782
928
|
},
|
|
783
|
-
element('aside', { class: 'knowledge-library-column', 'aria-label': '知识库列表' },
|
|
929
|
+
element('aside', { id: 'knowledge-library-column', class: 'knowledge-library-column', 'aria-label': '知识库列表' },
|
|
784
930
|
element('header', { class: 'column-header' },
|
|
785
931
|
element('div', {}, element('h2', { id: 'documents-heading' }, '知识库'), element('span', {}, `${activeBases.length} 个可用`)),
|
|
786
932
|
),
|
|
787
|
-
activeBases.length ? element('div', { class: 'library-list', role: 'listbox', tabindex: '0', onKeyDown: event => moveDocumentSelection(event, 'base') },
|
|
933
|
+
activeBases.length ? element('div', { class: 'library-list', role: 'listbox', tabindex: '0', 'data-scroll-key': 'library-list', onKeyDown: event => moveDocumentSelection(event, 'base') },
|
|
788
934
|
activeBases.map(base => {
|
|
789
935
|
const documentCount = state.documents.filter(document => document.knowledgeBaseId === base.id).length
|
|
790
936
|
const selected = base.id === view.knowledgeBaseId
|
|
@@ -798,11 +944,11 @@ function renderEntries() {
|
|
|
798
944
|
element('footer', { class: 'column-footer' }, actionButton('+ 新建知识库', () => openKnowledgeBaseEditor(), 'ghost small')),
|
|
799
945
|
),
|
|
800
946
|
renderColumnResizer('library', '调整知识库栏宽度'),
|
|
801
|
-
element('aside', { class: 'document-list-column', 'aria-label': '文档列表' },
|
|
947
|
+
element('aside', { id: 'document-list-column', class: 'document-list-column', 'aria-label': '文档列表' },
|
|
802
948
|
element('header', { class: 'column-header' },
|
|
803
949
|
element('div', {}, element('h2', {}, selectedBase?.name || '文档'), element('span', {}, query ? `找到 ${visibleDocuments.length} 篇` : `${allBaseDocuments.length} 篇文档`)),
|
|
804
950
|
),
|
|
805
|
-
visibleDocuments.length ? element('div', { class: 'document-list', role: 'listbox', tabindex: '0', onKeyDown: event => moveDocumentSelection(event, 'document') },
|
|
951
|
+
visibleDocuments.length ? element('div', { class: 'document-list', role: 'listbox', tabindex: '0', 'data-scroll-key': 'document-list', onKeyDown: event => moveDocumentSelection(event, 'document') },
|
|
806
952
|
visibleDocuments.map(document => element('button', {
|
|
807
953
|
type: 'button', class: 'document-row', role: 'option', 'aria-selected': String(document.id === view.documentId),
|
|
808
954
|
onClick: () => { view.documentId = document.id; renderShell() },
|
|
@@ -824,104 +970,56 @@ function setDocumentColumnHidden(column, hidden) {
|
|
|
824
970
|
renderShell()
|
|
825
971
|
}
|
|
826
972
|
|
|
827
|
-
function
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return element('div', {
|
|
833
|
-
class: 'column-resizer', 'data-column': column, role: 'separator', tabindex: '0',
|
|
834
|
-
title: `${label};可拖动或使用左右方向键`,
|
|
835
|
-
'aria-label': label, 'aria-orientation': 'vertical',
|
|
836
|
-
'aria-valuemin': minimum, 'aria-valuemax': maximum, 'aria-valuenow': value,
|
|
837
|
-
onPointerDown: event => startColumnResize(event, column, minimum, maximum),
|
|
838
|
-
onKeyDown: event => resizeColumnWithKeyboard(event, column, minimum, maximum),
|
|
839
|
-
}, element('span', { 'aria-hidden': 'true' }, '⋮'))
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
function openLayoutEditor() {
|
|
843
|
-
const fields = [
|
|
844
|
-
layoutRangeField('主导航栏', state.documentView.sidebarWidth, 190, 340),
|
|
845
|
-
layoutRangeField('知识库二级栏', state.documentView.libraryWidth, 170, 360),
|
|
846
|
-
layoutRangeField('文档列表栏', state.documentView.documentListWidth, 210, 480),
|
|
847
|
-
]
|
|
848
|
-
const visibility = [
|
|
849
|
-
layoutVisibilityOption('显示主导航栏', !state.documentView.sidebarHidden),
|
|
850
|
-
layoutVisibilityOption('显示知识库二级栏', !state.documentView.libraryHidden),
|
|
851
|
-
layoutVisibilityOption('显示文档列表栏', !state.documentView.documentListHidden),
|
|
852
|
-
]
|
|
853
|
-
const reset = actionButton('恢复默认布局', () => {
|
|
854
|
-
const defaults = [236, 220, 280]
|
|
855
|
-
fields.forEach((field, index) => field.setValue(defaults[index]))
|
|
856
|
-
visibility.forEach(option => { option.input.checked = true })
|
|
857
|
-
}, 'ghost small')
|
|
858
|
-
const body = element('div', { class: 'layout-editor' },
|
|
859
|
-
element('div', { class: 'layout-visibility', 'aria-label': '边栏显示设置' }, visibility.map(option => option.wrapper)),
|
|
860
|
-
fields.map(field => field.wrapper),
|
|
861
|
-
element('div', { class: 'layout-editor-note' },
|
|
862
|
-
element('span', {}, '也可以直接拖动栏与栏之间的三点分隔线。窗口过窄时会自动切换为紧凑布局。'),
|
|
863
|
-
reset,
|
|
864
|
-
),
|
|
973
|
+
function renderDocumentColumnControls() {
|
|
974
|
+
return element('div', { class: 'document-column-controls', role: 'group', 'aria-label': '显示文档栏位' },
|
|
975
|
+
element('span', { class: 'document-column-controls-label', 'aria-hidden': 'true' }, '显示'),
|
|
976
|
+
documentColumnControl('library', '知识库栏'),
|
|
977
|
+
documentColumnControl('documentList', '文档栏'),
|
|
865
978
|
)
|
|
866
|
-
openModal({
|
|
867
|
-
title: '调整边栏宽度',
|
|
868
|
-
description: '分别设置管理导航、知识库二级栏和文档列表栏。',
|
|
869
|
-
body,
|
|
870
|
-
primaryLabel: '应用布局',
|
|
871
|
-
onPrimary: async () => {
|
|
872
|
-
state.documentView.sidebarHidden = !visibility[0].input.checked
|
|
873
|
-
state.documentView.libraryHidden = !visibility[1].input.checked
|
|
874
|
-
state.documentView.documentListHidden = !visibility[2].input.checked
|
|
875
|
-
state.documentView.sidebarWidth = fields[0].value()
|
|
876
|
-
state.documentView.libraryWidth = fields[1].value()
|
|
877
|
-
state.documentView.documentListWidth = fields[2].value()
|
|
878
|
-
saveDocumentLayout()
|
|
879
|
-
renderShell()
|
|
880
|
-
showToast('边栏宽度已保存。')
|
|
881
|
-
return true
|
|
882
|
-
},
|
|
883
|
-
})
|
|
884
979
|
}
|
|
885
980
|
|
|
886
|
-
function
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
981
|
+
function documentColumnControl(column, label) {
|
|
982
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
983
|
+
const visible = !state.documentView[layout.hiddenKey]
|
|
984
|
+
const action = `${visible ? '收起' : '展开'}${label}`
|
|
985
|
+
return element('button', {
|
|
986
|
+
type: 'button', class: 'document-column-control', 'data-column': column,
|
|
987
|
+
'aria-label': action, 'aria-pressed': String(visible), title: action,
|
|
988
|
+
onClick: () => setDocumentColumnHidden(column, visible),
|
|
989
|
+
}, label)
|
|
892
990
|
}
|
|
893
991
|
|
|
894
|
-
function
|
|
895
|
-
const
|
|
896
|
-
const
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
),
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
}
|
|
992
|
+
function renderColumnResizer(column, label) {
|
|
993
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
994
|
+
const hidden = state.documentView[layout.hiddenKey]
|
|
995
|
+
if (hidden) return null
|
|
996
|
+
const value = state.documentView[layout.widthKey]
|
|
997
|
+
return element('div', {
|
|
998
|
+
class: 'column-resizer', 'data-column': column, role: 'separator', tabindex: '0',
|
|
999
|
+
title: `${label};可拖动或使用左右方向键`,
|
|
1000
|
+
'aria-label': label, 'aria-controls': layout.controls, 'aria-orientation': 'vertical',
|
|
1001
|
+
'aria-valuemin': layout.minimum, 'aria-valuemax': layout.maximum, 'aria-valuenow': value,
|
|
1002
|
+
'aria-valuetext': `${value} 像素`,
|
|
1003
|
+
onPointerDown: event => startColumnResize(event, column),
|
|
1004
|
+
onKeyDown: event => resizeColumnWithKeyboard(event, column),
|
|
1005
|
+
}, element('span', { 'aria-hidden': 'true' }, '⋮'))
|
|
909
1006
|
}
|
|
910
1007
|
|
|
911
|
-
function startColumnResize(event, column
|
|
1008
|
+
function startColumnResize(event, column) {
|
|
912
1009
|
if (event.button !== 0) return
|
|
913
1010
|
event.preventDefault()
|
|
1011
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
914
1012
|
const handle = event.currentTarget
|
|
915
1013
|
const browser = handle.closest('.document-browser')
|
|
916
1014
|
if (!browser) return
|
|
917
1015
|
const startX = event.clientX
|
|
918
|
-
const startWidth =
|
|
1016
|
+
const startWidth = state.documentView[layout.widthKey]
|
|
919
1017
|
handle.setPointerCapture?.(event.pointerId)
|
|
920
1018
|
handle.classList.add('is-dragging')
|
|
921
1019
|
document.body.classList.add('is-resizing-columns')
|
|
922
1020
|
const move = moveEvent => {
|
|
923
|
-
const width = clampNumber(startWidth + moveEvent.clientX - startX, minimum, maximum, startWidth)
|
|
924
|
-
|
|
1021
|
+
const width = clampNumber(startWidth + moveEvent.clientX - startX, layout.minimum, layout.maximum, startWidth)
|
|
1022
|
+
setDocumentColumnWidth(column, width, browser, handle)
|
|
925
1023
|
}
|
|
926
1024
|
const finish = () => {
|
|
927
1025
|
handle.classList.remove('is-dragging')
|
|
@@ -936,27 +1034,32 @@ function startColumnResize(event, column, minimum, maximum) {
|
|
|
936
1034
|
handle.addEventListener('pointercancel', finish)
|
|
937
1035
|
}
|
|
938
1036
|
|
|
939
|
-
function resizeColumnWithKeyboard(event, column
|
|
1037
|
+
function resizeColumnWithKeyboard(event, column) {
|
|
940
1038
|
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return
|
|
941
1039
|
event.preventDefault()
|
|
942
|
-
const
|
|
943
|
-
const
|
|
944
|
-
: event.key === 'End' ? maximum
|
|
945
|
-
: clampNumber(current + (event.key === 'ArrowRight' ? 16 : -16), minimum, maximum, current)
|
|
1040
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
1041
|
+
const current = state.documentView[layout.widthKey]
|
|
946
1042
|
const browser = event.currentTarget.closest('.document-browser')
|
|
947
|
-
|
|
1043
|
+
const width = event.key === 'Home' ? layout.minimum
|
|
1044
|
+
: event.key === 'End' ? layout.maximum
|
|
1045
|
+
: clampNumber(current + (event.key === 'ArrowRight' ? 16 : -16), layout.minimum, layout.maximum, current)
|
|
1046
|
+
setDocumentColumnWidth(column, width, browser, event.currentTarget)
|
|
948
1047
|
saveDocumentLayout()
|
|
949
1048
|
}
|
|
950
1049
|
|
|
951
|
-
function
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
} else {
|
|
956
|
-
state.documentView.documentListWidth = width
|
|
957
|
-
browser?.style.setProperty('--document-list-width', `${width}px`)
|
|
958
|
-
}
|
|
1050
|
+
function setDocumentColumnWidth(column, width, browser, handle) {
|
|
1051
|
+
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
1052
|
+
state.documentView[layout.widthKey] = width
|
|
1053
|
+
browser?.style.setProperty(layout.cssVariable, `${width}px`)
|
|
959
1054
|
handle?.setAttribute('aria-valuenow', String(width))
|
|
1055
|
+
handle?.setAttribute('aria-valuetext', `${width} 像素`)
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function renderDocumentModeTabs() {
|
|
1059
|
+
return element('div', { class: 'tabs', role: 'tablist', 'aria-label': '知识视图' },
|
|
1060
|
+
documentViewTab('文档', 'documents'),
|
|
1061
|
+
documentViewTab('条目管理', 'entries'),
|
|
1062
|
+
)
|
|
960
1063
|
}
|
|
961
1064
|
|
|
962
1065
|
function documentViewTab(label, mode) {
|
|
@@ -1002,7 +1105,7 @@ function renderDocumentReader(document, base) {
|
|
|
1002
1105
|
.map(item => element('option', { value: item.id, selected: item.id === document.id }, item.relPath))),
|
|
1003
1106
|
element('div', { class: 'reader-actions' }, badge(`${document.entryCount} 条知识`), actionButton('添加内容', () => openEntryEditor(), 'small')),
|
|
1004
1107
|
),
|
|
1005
|
-
element('article', { class: 'markdown-document' }, renderMarkdown(document.content)),
|
|
1108
|
+
element('article', { class: 'markdown-document', 'data-scroll-key': 'document-reader' }, renderMarkdown(document.content)),
|
|
1006
1109
|
element('footer', { class: 'reader-footer' },
|
|
1007
1110
|
element('span', {}, `自动整理 · ${formatDate(document.updatedAt)}`),
|
|
1008
1111
|
element('span', {}, `SHA-256 · ${document.contentHash.slice(0, 8)}`),
|
|
@@ -1077,7 +1180,11 @@ function renderLegacyEntries() {
|
|
|
1077
1180
|
projectInput,
|
|
1078
1181
|
actionButton('搜索', () => {}, 'primary', { type: 'submit' }),
|
|
1079
1182
|
)
|
|
1080
|
-
return element('section', { 'aria-labelledby': 'entries-heading' },
|
|
1183
|
+
return element('section', { class: 'document-page legacy-entries-page', 'aria-labelledby': 'entries-heading' },
|
|
1184
|
+
element('div', { class: 'document-page-toolbar legacy-entries-toolbar' },
|
|
1185
|
+
renderDocumentModeTabs(),
|
|
1186
|
+
element('div', { class: 'document-toolbar-actions' }, actionButton('+ 新建知识', () => openEntryEditor(), 'primary')),
|
|
1187
|
+
),
|
|
1081
1188
|
element('div', { class: 'section-heading' }, element('div', {}, element('h2', { id: 'entries-heading' }, `${STATUS_LABELS[state.entryFilters.status]} · ${state.entries.length}`), element('p', {}, '搜索范围会包含全局知识和指定项目知识'))),
|
|
1082
1189
|
toolbar,
|
|
1083
1190
|
state.entries.length
|