@lemoncat7/dsh-knowledge 0.7.3 → 1.0.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 +16 -15
- package/cordis.patch.yml +0 -1
- package/docs/architecture.md +37 -19
- package/docs/document-knowledge-design.zh-CN.md +2 -2
- package/docs/requirements.md +9 -5
- package/lib/api.js +5 -1
- package/lib/api.js.map +1 -1
- package/lib/client.d.ts +6 -0
- package/lib/client.d.ts.map +1 -0
- package/lib/client.js +207 -170
- package/lib/client.js.map +3 -3
- package/lib/config.d.ts +2 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +5 -3
- package/lib/config.js.map +1 -1
- package/lib/documents/markdown.d.ts +33 -0
- package/lib/documents/markdown.d.ts.map +1 -0
- package/lib/documents/markdown.js +91 -0
- package/lib/documents/markdown.js.map +1 -0
- package/lib/documents/path.d.ts +4 -0
- package/lib/documents/path.d.ts.map +1 -0
- package/lib/documents/path.js +11 -0
- package/lib/documents/path.js.map +1 -0
- package/lib/documents/store.d.ts +32 -0
- package/lib/documents/store.d.ts.map +1 -0
- package/lib/documents/store.js +272 -0
- package/lib/documents/store.js.map +1 -0
- package/lib/domain.d.ts +2 -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 +210 -73
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +4 -4
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +2 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +145 -79
- package/lib/local-provider.js.map +1 -1
- package/lib/recall.d.ts +7 -2
- package/lib/recall.d.ts.map +1 -1
- package/lib/recall.js +46 -30
- package/lib/recall.js.map +1 -1
- package/lib/retrieval.d.ts +14 -4
- package/lib/retrieval.d.ts.map +1 -1
- package/lib/retrieval.js +133 -25
- package/lib/retrieval.js.map +1 -1
- package/lib/theme-bridge.d.ts +23 -0
- package/lib/theme-bridge.d.ts.map +1 -0
- package/lib/theme-bridge.js +72 -0
- package/lib/theme-bridge.js.map +1 -0
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +165 -7
- package/lib/tools.js.map +1 -1
- package/lib/web-markdown-preview.d.ts +3 -0
- package/lib/web-markdown-preview.d.ts.map +1 -0
- package/lib/web-markdown-preview.js +24 -0
- package/lib/web-markdown-preview.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 +16 -3
- package/web/app.js +326 -359
- package/web/index.html +1 -0
- package/web/markdown-preview.js +72 -0
- package/web/styles.css +177 -200
package/web/app.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const API_BASE = document.querySelector('meta[name="dsh-knowledge-api"]')?.content || '/knowledge-api/v1'
|
|
2
2
|
const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')?.content || 'bearer'
|
|
3
|
+
const HOST_THEME_MESSAGE = '@lemoncat7/dsh-knowledge/host-theme'
|
|
4
|
+
const HOST_THEME_READY_MESSAGE = '@lemoncat7/dsh-knowledge/host-theme-ready'
|
|
5
|
+
const HOST_THEME_PROTOCOL_VERSION = 1
|
|
6
|
+
const HOST_THEME_COLOR_TOKENS = new Set([
|
|
7
|
+
'--bg', '--surface', '--surface-raised', '--surface-soft', '--surface-hover',
|
|
8
|
+
'--text', '--text-secondary', '--text-tertiary', '--border', '--border-strong',
|
|
9
|
+
'--accent', '--accent-hover', '--accent-soft', '--on-accent',
|
|
10
|
+
'--success', '--success-soft', '--warning', '--warning-soft', '--danger', '--danger-soft',
|
|
11
|
+
])
|
|
12
|
+
const HOST_THEME_STYLE_TOKENS = new Set(['--shadow'])
|
|
3
13
|
const TOKEN_KEY = 'dsh-knowledge.session-token'
|
|
4
14
|
const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
|
|
5
15
|
const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
|
|
@@ -7,20 +17,8 @@ const ACTION_LABELS = { create: '新增', update: '更新', conflict: '冲突' }
|
|
|
7
17
|
const STATUS_LABELS = { active: '生效中', archived: '已归档', pending: '待审核', approved: '已通过', rejected: '已拒绝' }
|
|
8
18
|
const CHANGE_LABELS = { create: '创建', update: '更新', archive: '归档', restore: '恢复' }
|
|
9
19
|
const WRITE_MODE_LABELS = { none: '仅召回', audit: '审核写入', direct: '直接写入' }
|
|
10
|
-
const
|
|
11
|
-
preference: 'preferences.md', fact: 'facts.md', decision: 'decisions.md', procedure: 'procedures.md', lesson: 'lessons.md',
|
|
12
|
-
}
|
|
20
|
+
const EVIDENCE_LABELS = { explicit: '用户明确', verified: '结果已验证', inferred: '模型推断' }
|
|
13
21
|
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
|
-
})
|
|
24
22
|
const pageParams = new URLSearchParams(location.search)
|
|
25
23
|
const mountContext = {
|
|
26
24
|
sessionId: pageParams.get('sessionId')?.trim() || '',
|
|
@@ -32,7 +30,7 @@ const savedDocumentLayout = readDocumentLayout()
|
|
|
32
30
|
|
|
33
31
|
const state = {
|
|
34
32
|
token: sessionStorage.getItem(TOKEN_KEY) || '',
|
|
35
|
-
view: '
|
|
33
|
+
view: 'entries',
|
|
36
34
|
menuOpen: false,
|
|
37
35
|
stats: null,
|
|
38
36
|
overview: null,
|
|
@@ -53,13 +51,13 @@ const state = {
|
|
|
53
51
|
entryFilters: { query: '', type: '', status: 'active', projectId: '', knowledgeBaseId: '' },
|
|
54
52
|
documents: [],
|
|
55
53
|
documentView: {
|
|
56
|
-
knowledgeBaseId: '', documentId: '', query: '',
|
|
54
|
+
knowledgeBaseId: '', documentId: '', query: '',
|
|
55
|
+
mode: 'preview',
|
|
56
|
+
treeOpen: false,
|
|
57
57
|
sidebarHidden: savedDocumentLayout.sidebarHidden,
|
|
58
|
-
libraryHidden: savedDocumentLayout.libraryHidden,
|
|
59
|
-
documentListHidden: savedDocumentLayout.documentListHidden,
|
|
60
58
|
sidebarWidth: savedDocumentLayout.sidebarWidth,
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
expandedBases: new Set(),
|
|
60
|
+
editor: null,
|
|
63
61
|
},
|
|
64
62
|
candidates: [],
|
|
65
63
|
candidateStatus: 'pending',
|
|
@@ -74,20 +72,44 @@ const state = {
|
|
|
74
72
|
|
|
75
73
|
let scrollRestoreFrame = 0
|
|
76
74
|
|
|
75
|
+
function installHostThemeBridge() {
|
|
76
|
+
if (window.parent === window) return
|
|
77
|
+
window.addEventListener('message', event => {
|
|
78
|
+
if (event.source !== window.parent || event.origin !== window.location.origin) return
|
|
79
|
+
const message = event.data
|
|
80
|
+
if (!message || message.type !== HOST_THEME_MESSAGE || message.version !== HOST_THEME_PROTOCOL_VERSION) return
|
|
81
|
+
if (message.colorScheme !== 'light' && message.colorScheme !== 'dark') return
|
|
82
|
+
if (!message.tokens || typeof message.tokens !== 'object' || Array.isArray(message.tokens)) return
|
|
83
|
+
|
|
84
|
+
const root = document.documentElement
|
|
85
|
+
for (const name of [...HOST_THEME_COLOR_TOKENS, ...HOST_THEME_STYLE_TOKENS]) root.style.removeProperty(name)
|
|
86
|
+
for (const [name, value] of Object.entries(message.tokens)) {
|
|
87
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512) continue
|
|
88
|
+
if (HOST_THEME_COLOR_TOKENS.has(name) && CSS.supports('color', value)) root.style.setProperty(name, value)
|
|
89
|
+
else if (HOST_THEME_STYLE_TOKENS.has(name) && CSS.supports('box-shadow', value)) root.style.setProperty(name, value)
|
|
90
|
+
}
|
|
91
|
+
root.dataset.dshHostTheme = 'true'
|
|
92
|
+
root.style.colorScheme = message.colorScheme
|
|
93
|
+
document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', message.colorScheme)
|
|
94
|
+
const background = root.style.getPropertyValue('--bg')
|
|
95
|
+
if (background) document.querySelector('meta[name="theme-color"]')?.setAttribute('content', background)
|
|
96
|
+
})
|
|
97
|
+
window.parent.postMessage({
|
|
98
|
+
type: HOST_THEME_READY_MESSAGE,
|
|
99
|
+
version: HOST_THEME_PROTOCOL_VERSION,
|
|
100
|
+
}, window.location.origin)
|
|
101
|
+
}
|
|
102
|
+
|
|
77
103
|
function readDocumentLayout() {
|
|
78
104
|
const fallback = {
|
|
79
|
-
sidebarHidden: false,
|
|
80
|
-
sidebarWidth: 236,
|
|
105
|
+
sidebarHidden: false,
|
|
106
|
+
sidebarWidth: 236,
|
|
81
107
|
}
|
|
82
108
|
try {
|
|
83
109
|
const value = JSON.parse(localStorage.getItem(DOCUMENT_LAYOUT_KEY) || '{}')
|
|
84
110
|
return {
|
|
85
111
|
sidebarHidden: value.sidebarHidden === true,
|
|
86
|
-
libraryHidden: value.libraryHidden === true,
|
|
87
|
-
documentListHidden: value.documentListHidden === true,
|
|
88
112
|
sidebarWidth: clampNumber(value.sidebarWidth, 190, 340, fallback.sidebarWidth),
|
|
89
|
-
libraryWidth: clampNumber(value.libraryWidth, 170, 360, fallback.libraryWidth),
|
|
90
|
-
documentListWidth: clampNumber(value.documentListWidth, 210, 480, fallback.documentListWidth),
|
|
91
113
|
}
|
|
92
114
|
} catch { return fallback }
|
|
93
115
|
}
|
|
@@ -96,11 +118,7 @@ function saveDocumentLayout() {
|
|
|
96
118
|
try {
|
|
97
119
|
localStorage.setItem(DOCUMENT_LAYOUT_KEY, JSON.stringify({
|
|
98
120
|
sidebarHidden: state.documentView.sidebarHidden,
|
|
99
|
-
libraryHidden: state.documentView.libraryHidden,
|
|
100
|
-
documentListHidden: state.documentView.documentListHidden,
|
|
101
121
|
sidebarWidth: state.documentView.sidebarWidth,
|
|
102
|
-
libraryWidth: state.documentView.libraryWidth,
|
|
103
|
-
documentListWidth: state.documentView.documentListWidth,
|
|
104
122
|
}))
|
|
105
123
|
} catch {}
|
|
106
124
|
}
|
|
@@ -217,7 +235,7 @@ async function boot() {
|
|
|
217
235
|
if (AUTH_MODE === 'same-origin') {
|
|
218
236
|
state.token = ''
|
|
219
237
|
try { state.service = await api('service') } catch {}
|
|
220
|
-
await navigate('
|
|
238
|
+
await navigate('entries')
|
|
221
239
|
return
|
|
222
240
|
}
|
|
223
241
|
if (!state.token) {
|
|
@@ -226,7 +244,7 @@ async function boot() {
|
|
|
226
244
|
}
|
|
227
245
|
try {
|
|
228
246
|
await api('entries?limit=1')
|
|
229
|
-
await navigate('
|
|
247
|
+
await navigate('entries')
|
|
230
248
|
} catch (error) {
|
|
231
249
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
232
250
|
state.token = ''
|
|
@@ -253,7 +271,7 @@ function renderLogin(message = '') {
|
|
|
253
271
|
try {
|
|
254
272
|
await api('entries?limit=1')
|
|
255
273
|
sessionStorage.setItem(TOKEN_KEY, token)
|
|
256
|
-
await navigate('
|
|
274
|
+
await navigate('entries')
|
|
257
275
|
} catch (requestError) {
|
|
258
276
|
state.token = ''
|
|
259
277
|
error.textContent = requestError.status === 401 ? '令牌无效或已被撤销。' : '连接失败,请检查服务状态后重试。'
|
|
@@ -377,6 +395,8 @@ async function loadDocuments() {
|
|
|
377
395
|
: bases.find(base => base.status === 'active')?.id || bases[0]?.id || ''
|
|
378
396
|
}
|
|
379
397
|
selectDefaultDocument()
|
|
398
|
+
if (view.documentId) await loadDocumentEditor(view.documentId)
|
|
399
|
+
else view.editor = null
|
|
380
400
|
if (!state.stats) await refreshStats()
|
|
381
401
|
}
|
|
382
402
|
|
|
@@ -384,10 +404,107 @@ function selectDefaultDocument() {
|
|
|
384
404
|
const view = state.documentView
|
|
385
405
|
const documents = state.documents.filter(document => document.knowledgeBaseId === view.knowledgeBaseId)
|
|
386
406
|
if (!documents.some(document => document.id === view.documentId)) {
|
|
387
|
-
view.documentId = documents
|
|
407
|
+
view.documentId = documents[0]?.id || ''
|
|
408
|
+
}
|
|
409
|
+
if (view.knowledgeBaseId) view.expandedBases.add(view.knowledgeBaseId)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function loadDocumentEditor(id) {
|
|
413
|
+
const entry = await api(`entries/${encodeURIComponent(id)}`)
|
|
414
|
+
state.documentView.mode = 'preview'
|
|
415
|
+
state.documentView.editor = {
|
|
416
|
+
...entry,
|
|
417
|
+
tagsText: entry.tags.join(', '),
|
|
418
|
+
dirty: false,
|
|
419
|
+
isNew: false,
|
|
420
|
+
saveState: '已保存',
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function createBlankDocument(baseId) {
|
|
425
|
+
const base = state.knowledgeBases.find(item => item.id === baseId && item.status === 'active')
|
|
426
|
+
if (!base) return showToast('请先选择一个可用知识库。', 'error')
|
|
427
|
+
const view = state.documentView
|
|
428
|
+
view.knowledgeBaseId = base.id
|
|
429
|
+
view.documentId = ''
|
|
430
|
+
view.mode = 'edit'
|
|
431
|
+
view.treeOpen = false
|
|
432
|
+
view.expandedBases.add(base.id)
|
|
433
|
+
view.editor = {
|
|
434
|
+
id: '', knowledgeBaseId: base.id, title: '', body: '', type: 'fact', tags: [], tagsText: '',
|
|
435
|
+
scope: { kind: 'global' }, confidence: .8, dirty: true, isNew: true, saveState: '新文档',
|
|
436
|
+
}
|
|
437
|
+
renderShell()
|
|
438
|
+
document.querySelector('.note-title-input')?.focus()
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function startBlankDocument(baseId) {
|
|
442
|
+
const editor = state.documentView.editor
|
|
443
|
+
const emptyDraft = editor?.isNew && !editor.title.trim() && !editor.body.trim()
|
|
444
|
+
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor()) return
|
|
445
|
+
createBlankDocument(baseId)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function selectDocument(id) {
|
|
449
|
+
const editor = state.documentView.editor
|
|
450
|
+
const emptyDraft = editor?.isNew && !editor.title.trim() && !editor.body.trim()
|
|
451
|
+
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor()) return
|
|
452
|
+
state.documentView.documentId = id
|
|
453
|
+
state.documentView.treeOpen = false
|
|
454
|
+
await loadDocumentEditor(id)
|
|
455
|
+
renderShell()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function editorDraft(editor) {
|
|
459
|
+
return {
|
|
460
|
+
knowledgeBaseId: editor.knowledgeBaseId,
|
|
461
|
+
title: editor.title.trim(),
|
|
462
|
+
body: editor.body.trim(),
|
|
463
|
+
type: editor.type,
|
|
464
|
+
tags: parseTags(editor.tagsText),
|
|
465
|
+
scope: editor.scope,
|
|
466
|
+
confidence: editor.confidence,
|
|
467
|
+
...(editor.source ? { source: editor.source } : {}),
|
|
388
468
|
}
|
|
389
469
|
}
|
|
390
470
|
|
|
471
|
+
async function saveDocumentEditor() {
|
|
472
|
+
const editor = state.documentView.editor
|
|
473
|
+
if (!editor || !editor.dirty) return true
|
|
474
|
+
if (!editor.title.trim() || !editor.body.trim()) {
|
|
475
|
+
showToast('标题和正文填写完整后才能保存。', 'error')
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
editor.saveState = '正在保存…'
|
|
479
|
+
updateEditorSaveState(editor.saveState)
|
|
480
|
+
try {
|
|
481
|
+
const draft = editorDraft(editor)
|
|
482
|
+
const saved = editor.isNew
|
|
483
|
+
? await api('entries', { method: 'POST', body: { draft } })
|
|
484
|
+
: await api(`entries/${encodeURIComponent(editor.id)}`, { method: 'PUT', body: { draft } })
|
|
485
|
+
editor.id = saved.id
|
|
486
|
+
editor.isNew = false
|
|
487
|
+
editor.dirty = false
|
|
488
|
+
editor.updatedAt = saved.updatedAt
|
|
489
|
+
editor.saveState = '已保存'
|
|
490
|
+
state.documentView.documentId = saved.id
|
|
491
|
+
updateEditorSaveState(editor.saveState)
|
|
492
|
+
const documents = await api('documents')
|
|
493
|
+
state.documents = documents
|
|
494
|
+
return true
|
|
495
|
+
} catch (error) {
|
|
496
|
+
editor.saveState = '保存失败'
|
|
497
|
+
updateEditorSaveState(editor.saveState)
|
|
498
|
+
showToast(friendlyError(error), 'error')
|
|
499
|
+
return false
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function updateEditorSaveState(label) {
|
|
504
|
+
const node = document.querySelector('.editor-save-status')
|
|
505
|
+
if (node) node.textContent = label
|
|
506
|
+
}
|
|
507
|
+
|
|
391
508
|
async function loadCandidates() {
|
|
392
509
|
const [candidates] = await Promise.all([
|
|
393
510
|
api(`candidates?status=${state.candidateStatus}&limit=100`),
|
|
@@ -408,9 +525,9 @@ function renderShell() {
|
|
|
408
525
|
captureScrollPosition()
|
|
409
526
|
const titles = {
|
|
410
527
|
overview: ['概览', '知识库运行状态与最近活动'],
|
|
411
|
-
bases: ['
|
|
412
|
-
entries: ['
|
|
413
|
-
candidates: ['
|
|
528
|
+
bases: ['知识库与挂载', '管理知识目录,并限定项目与会话的召回和写入范围'],
|
|
529
|
+
entries: ['知识文档', '在知识目录中阅读、整理和维护 Markdown 文档'],
|
|
530
|
+
candidates: ['待审核', '确认 AI 提取结果后再写入知识文档'],
|
|
414
531
|
tokens: ['访问管理', '管理其他客户端连接中央知识库的权限'],
|
|
415
532
|
}
|
|
416
533
|
const [title, subtitle] = titles[state.view]
|
|
@@ -427,6 +544,10 @@ function renderShell() {
|
|
|
427
544
|
element('div', { class: 'topbar-title' },
|
|
428
545
|
actionButton('☰', () => { state.menuOpen = !state.menuOpen; renderShell() }, 'ghost mobile-menu', { 'aria-label': '打开导航菜单' }),
|
|
429
546
|
paneToggleButton('main', !state.documentView.sidebarHidden, () => setSidebarHidden(!state.documentView.sidebarHidden), '主导航栏'),
|
|
547
|
+
state.view === 'entries' ? paneToggleButton('library', state.documentView.treeOpen, () => {
|
|
548
|
+
state.documentView.treeOpen = !state.documentView.treeOpen
|
|
549
|
+
renderShell()
|
|
550
|
+
}, '知识目录') : null,
|
|
430
551
|
element('div', {}, element('h1', {}, title), element('p', {}, subtitle)),
|
|
431
552
|
),
|
|
432
553
|
),
|
|
@@ -508,8 +629,8 @@ function setSidebarHidden(hidden) {
|
|
|
508
629
|
function renderSidebar() {
|
|
509
630
|
const pending = state.stats?.candidates.pending
|
|
510
631
|
const navGroups = [
|
|
511
|
-
['
|
|
512
|
-
['
|
|
632
|
+
['知识工作区', [['entries', '文档'], ['candidates', '待审核'], ['bases', '知识库与挂载']]],
|
|
633
|
+
['连接', [['tokens', '访问管理']].filter(([id]) => id !== 'tokens' || !state.service.remote)],
|
|
513
634
|
].filter(([, items]) => items.length)
|
|
514
635
|
return element('aside', { class: 'sidebar', 'aria-label': '知识库导航' },
|
|
515
636
|
element('div', { class: 'brand' },
|
|
@@ -724,7 +845,6 @@ function renderKnowledgeBaseCard(base) {
|
|
|
724
845
|
state.entryFilters.knowledgeBaseId = base.id
|
|
725
846
|
state.documentView.knowledgeBaseId = base.id
|
|
726
847
|
state.documentView.documentId = ''
|
|
727
|
-
state.documentView.mode = 'documents'
|
|
728
848
|
void navigate('entries')
|
|
729
849
|
}, 'ghost small'),
|
|
730
850
|
archived ? actionButton('恢复', () => confirmRestoreKnowledgeBase(base), 'small') : actionButton('编辑', () => openKnowledgeBaseEditor(base), 'small'),
|
|
@@ -883,347 +1003,182 @@ function compactEmpty(message) {
|
|
|
883
1003
|
}
|
|
884
1004
|
|
|
885
1005
|
function renderEntries() {
|
|
886
|
-
if (state.documentView.mode === 'entries') return renderLegacyEntries()
|
|
887
1006
|
const view = state.documentView
|
|
888
1007
|
const query = view.query.trim().toLocaleLowerCase()
|
|
889
1008
|
const activeBases = state.knowledgeBases.filter(base => base.status === 'active')
|
|
890
|
-
const selectedBase =
|
|
891
|
-
const allBaseDocuments = state.documents.filter(document => document.knowledgeBaseId === view.knowledgeBaseId)
|
|
892
|
-
const visibleDocuments = allBaseDocuments.filter(document => !query || [document.title, document.relPath, document.content]
|
|
893
|
-
.some(value => value.toLocaleLowerCase().includes(query)))
|
|
894
|
-
const selectedDocument = state.documents.find(document => document.id === view.documentId)
|
|
1009
|
+
const selectedBase = activeBases.find(base => base.id === view.knowledgeBaseId)
|
|
895
1010
|
const search = element('input', {
|
|
896
|
-
class: '
|
|
1011
|
+
class: 'note-tree-search', type: 'search', value: view.query, placeholder: '搜索文档', 'aria-label': '搜索知识库文档',
|
|
897
1012
|
onInput: (event) => {
|
|
898
1013
|
view.query = event.target.value
|
|
899
|
-
const stillVisible = state.documents.some(document => document.id === view.documentId
|
|
900
|
-
&& document.knowledgeBaseId === view.knowledgeBaseId
|
|
901
|
-
&& (!view.query.trim() || [document.title, document.relPath, document.content].some(value => value.toLocaleLowerCase().includes(view.query.trim().toLocaleLowerCase()))))
|
|
902
|
-
if (!stillVisible) view.documentId = ''
|
|
903
1014
|
renderShell()
|
|
904
|
-
document.querySelector('.
|
|
1015
|
+
const input = document.querySelector('.note-tree-search')
|
|
1016
|
+
input?.focus()
|
|
1017
|
+
input?.setSelectionRange(input.value.length, input.value.length)
|
|
905
1018
|
},
|
|
906
1019
|
})
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
element('
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
),
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
class: '
|
|
925
|
-
'
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
element('footer', { class: 'column-footer' }, actionButton('+ 新建知识库', () => openKnowledgeBaseEditor(), 'ghost small')),
|
|
945
|
-
),
|
|
946
|
-
renderColumnResizer('library', '调整知识库栏宽度'),
|
|
947
|
-
element('aside', { id: 'document-list-column', class: 'document-list-column', 'aria-label': '文档列表' },
|
|
948
|
-
element('header', { class: 'column-header' },
|
|
949
|
-
element('div', {}, element('h2', {}, selectedBase?.name || '文档'), element('span', {}, query ? `找到 ${visibleDocuments.length} 篇` : `${allBaseDocuments.length} 篇文档`)),
|
|
950
|
-
),
|
|
951
|
-
visibleDocuments.length ? element('div', { class: 'document-list', role: 'listbox', tabindex: '0', 'data-scroll-key': 'document-list', onKeyDown: event => moveDocumentSelection(event, 'document') },
|
|
952
|
-
visibleDocuments.map(document => element('button', {
|
|
953
|
-
type: 'button', class: 'document-row', role: 'option', 'aria-selected': String(document.id === view.documentId),
|
|
954
|
-
onClick: () => { view.documentId = document.id; renderShell() },
|
|
955
|
-
}, element('span', { class: 'document-icon', 'aria-hidden': 'true' }, document.relPath === 'README.md' ? '▣' : '≡'),
|
|
956
|
-
element('span', { class: 'document-row-copy' }, element('strong', {}, document.title), element('small', {}, document.relPath)),
|
|
957
|
-
element('span', { class: 'document-entry-count' }, document.entryCount))))
|
|
958
|
-
: element('div', { class: 'document-empty' }, query ? '没有匹配的文档' : '这个知识库还没有文档'),
|
|
1020
|
+
const tree = activeBases.map(base => {
|
|
1021
|
+
const expanded = view.expandedBases.has(base.id) || Boolean(query)
|
|
1022
|
+
const documents = state.documents.filter(document => document.knowledgeBaseId === base.id
|
|
1023
|
+
&& (!query || [document.title, document.relPath, document.content].some(value => value.toLocaleLowerCase().includes(query))))
|
|
1024
|
+
return element('section', { class: 'note-tree-group', 'data-expanded': String(expanded) },
|
|
1025
|
+
element('button', {
|
|
1026
|
+
type: 'button', class: 'note-tree-base', 'aria-expanded': String(expanded),
|
|
1027
|
+
onClick: () => {
|
|
1028
|
+
view.knowledgeBaseId = base.id
|
|
1029
|
+
if (expanded && !query) view.expandedBases.delete(base.id)
|
|
1030
|
+
else view.expandedBases.add(base.id)
|
|
1031
|
+
renderShell()
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
element('span', { class: 'tree-disclosure', 'aria-hidden': 'true' }),
|
|
1035
|
+
element('span', { class: 'tree-folder-icon', 'aria-hidden': 'true' }),
|
|
1036
|
+
element('span', { class: 'tree-base-name' }, base.name),
|
|
1037
|
+
element('span', { class: 'tree-count' }, documents.length)),
|
|
1038
|
+
expanded ? element('div', { class: 'note-tree-documents', role: 'group', 'aria-label': `${base.name}文档` },
|
|
1039
|
+
documents.map(document => element('button', {
|
|
1040
|
+
type: 'button', class: 'note-tree-document', 'aria-current': document.id === view.documentId ? 'page' : undefined,
|
|
1041
|
+
onClick: () => { void selectDocument(document.id) },
|
|
1042
|
+
}, element('span', { class: 'tree-document-icon', 'aria-hidden': 'true' }), element('span', { class: 'tree-document-copy' },
|
|
1043
|
+
element('strong', {}, document.title), element('small', {}, document.relPath)))),
|
|
1044
|
+
!query ? element('button', { type: 'button', class: 'note-tree-new', onClick: () => { void startBlankDocument(base.id) } },
|
|
1045
|
+
element('span', { 'aria-hidden': 'true' }, '+'), '新建文档') : null,
|
|
1046
|
+
) : null,
|
|
1047
|
+
)
|
|
1048
|
+
})
|
|
1049
|
+
return element('section', {
|
|
1050
|
+
class: 'note-workspace', 'aria-labelledby': 'documents-heading',
|
|
1051
|
+
'data-tree-open': String(view.treeOpen),
|
|
1052
|
+
},
|
|
1053
|
+
element('aside', { class: 'note-tree-panel', 'aria-label': '知识目录' },
|
|
1054
|
+
element('header', { class: 'note-tree-header' },
|
|
1055
|
+
element('div', {}, element('h2', { id: 'documents-heading' }, '知识目录'), element('span', {}, `${state.documents.length} 篇文档`)),
|
|
1056
|
+
actionButton('+', () => { void startBlankDocument(view.knowledgeBaseId || activeBases[0]?.id) }, 'ghost note-add-button', { 'aria-label': '新建文档', title: '新建文档' }),
|
|
959
1057
|
),
|
|
960
|
-
|
|
961
|
-
|
|
1058
|
+
element('div', { class: 'note-tree-search-wrap' }, interfaceIcon('search', 'search-symbol'), search),
|
|
1059
|
+
element('nav', { class: 'note-tree', 'data-scroll-key': 'note-tree' }, tree.length ? tree : element('div', { class: 'note-tree-empty' }, '还没有知识库')),
|
|
1060
|
+
element('footer', { class: 'note-tree-footer' }, actionButton('新建知识库', () => openKnowledgeBaseEditor(), 'ghost small')),
|
|
962
1061
|
),
|
|
1062
|
+
view.treeOpen ? element('button', {
|
|
1063
|
+
type: 'button', class: 'note-tree-scrim', 'aria-label': '关闭知识目录',
|
|
1064
|
+
onClick: () => { view.treeOpen = false; renderShell() },
|
|
1065
|
+
}) : null,
|
|
1066
|
+
renderNoteEditor(view.editor, selectedBase),
|
|
963
1067
|
)
|
|
964
1068
|
}
|
|
965
1069
|
|
|
966
|
-
function
|
|
967
|
-
if (
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
980
|
-
|
|
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)
|
|
990
|
-
}
|
|
991
|
-
|
|
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' }, '⋮'))
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
function startColumnResize(event, column) {
|
|
1009
|
-
if (event.button !== 0) return
|
|
1010
|
-
event.preventDefault()
|
|
1011
|
-
const layout = DOCUMENT_COLUMN_LAYOUTS[column]
|
|
1012
|
-
const handle = event.currentTarget
|
|
1013
|
-
const browser = handle.closest('.document-browser')
|
|
1014
|
-
if (!browser) return
|
|
1015
|
-
const startX = event.clientX
|
|
1016
|
-
const startWidth = state.documentView[layout.widthKey]
|
|
1017
|
-
handle.setPointerCapture?.(event.pointerId)
|
|
1018
|
-
handle.classList.add('is-dragging')
|
|
1019
|
-
document.body.classList.add('is-resizing-columns')
|
|
1020
|
-
const move = moveEvent => {
|
|
1021
|
-
const width = clampNumber(startWidth + moveEvent.clientX - startX, layout.minimum, layout.maximum, startWidth)
|
|
1022
|
-
setDocumentColumnWidth(column, width, browser, handle)
|
|
1070
|
+
function renderNoteEditor(editor, base) {
|
|
1071
|
+
if (!editor) return element('main', { class: 'note-editor note-editor-empty' },
|
|
1072
|
+
element('div', { class: 'note-empty-content' },
|
|
1073
|
+
element('span', { class: 'empty-document-mark', 'aria-hidden': 'true' }),
|
|
1074
|
+
element('h3', {}, '选择或新建一篇文档'),
|
|
1075
|
+
element('p', {}, '文档保存后会立即参与当前知识库的搜索与召回。'),
|
|
1076
|
+
actionButton('新建文档', () => { void startBlankDocument(state.documentView.knowledgeBaseId || state.knowledgeBases.find(item => item.status === 'active')?.id) }, 'primary'),
|
|
1077
|
+
))
|
|
1078
|
+
const saveShortcut = event => {
|
|
1079
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLocaleLowerCase() === 's') {
|
|
1080
|
+
event.preventDefault()
|
|
1081
|
+
void saveDocumentEditor().then(saved => { if (saved) renderShell() })
|
|
1082
|
+
}
|
|
1023
1083
|
}
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
handle.removeEventListener('pointercancel', finish)
|
|
1030
|
-
saveDocumentLayout()
|
|
1084
|
+
const update = (key, value) => {
|
|
1085
|
+
editor[key] = value
|
|
1086
|
+
editor.dirty = true
|
|
1087
|
+
editor.saveState = '未保存'
|
|
1088
|
+
updateEditorSaveState(editor.saveState)
|
|
1031
1089
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1090
|
+
const title = element('input', {
|
|
1091
|
+
class: 'note-title-input', value: editor.title, maxlength: 200, placeholder: '无标题文档', 'aria-label': '文档标题',
|
|
1092
|
+
onInput: event => update('title', event.target.value), onKeyDown: saveShortcut,
|
|
1093
|
+
})
|
|
1094
|
+
const body = element('textarea', {
|
|
1095
|
+
class: 'note-body-editor', maxlength: 50000, placeholder: '从这里开始记录…', 'aria-label': '文档正文',
|
|
1096
|
+
onInput: event => { update('body', event.target.value); resizeDocumentEditor(event.target) }, onKeyDown: saveShortcut,
|
|
1097
|
+
})
|
|
1098
|
+
body.value = editor.body
|
|
1099
|
+
const mode = state.documentView.mode === 'edit' ? 'edit' : 'preview'
|
|
1100
|
+
if (mode === 'edit') window.requestAnimationFrame(() => resizeDocumentEditor(body))
|
|
1101
|
+
return element('main', { class: 'note-editor', 'aria-label': '文档编辑器' },
|
|
1102
|
+
element('header', { class: 'note-editor-toolbar' },
|
|
1103
|
+
element('div', { class: 'note-breadcrumb' },
|
|
1104
|
+
element('span', {}, base?.name || '知识库'),
|
|
1105
|
+
element('span', { 'aria-hidden': 'true' }, '/'),
|
|
1106
|
+
element('strong', {}, editor.isNew ? '新文档' : state.documents.find(item => item.id === editor.id)?.relPath || editor.title)),
|
|
1107
|
+
element('div', { class: 'note-editor-actions' },
|
|
1108
|
+
element('div', { class: 'note-mode-switch', role: 'tablist', 'aria-label': '文档视图' }, [
|
|
1109
|
+
['edit', '编辑'],
|
|
1110
|
+
['preview', '预览'],
|
|
1111
|
+
].map(([value, label]) => element('button', {
|
|
1112
|
+
type: 'button', role: 'tab', 'aria-selected': String(mode === value),
|
|
1113
|
+
onClick: () => switchDocumentMode(value),
|
|
1114
|
+
}, label))),
|
|
1115
|
+
element('span', { class: 'editor-save-status', role: 'status' }, editor.saveState),
|
|
1116
|
+
!editor.isNew ? actionButton('删除', () => confirmDeleteDocument(editor), 'ghost small') : null,
|
|
1117
|
+
actionButton(editor.isNew ? '创建文档' : '保存', () => { void saveDocumentEditor().then(saved => { if (saved) renderShell() }) }, 'primary small'),
|
|
1118
|
+
),
|
|
1119
|
+
),
|
|
1120
|
+
element('div', { class: 'note-editor-scroll', 'data-scroll-key': 'note-editor' },
|
|
1121
|
+
element('article', { class: `note-paper is-${mode}`, 'data-document-mode': mode },
|
|
1122
|
+
mode === 'edit' ? title : element('h1', { class: 'note-preview-title' }, editor.title || '无标题文档'),
|
|
1123
|
+
element('div', { class: 'note-document-meta' },
|
|
1124
|
+
element('span', {}, editor.isNew ? '尚未保存' : `更新于 ${formatDate(editor.updatedAt)}`),
|
|
1125
|
+
element('span', {}, editor.scope.kind === 'global' ? '全局知识' : `项目 · ${editor.scope.id}`)),
|
|
1126
|
+
mode === 'edit' ? body : renderMarkdownPreview(editor.body),
|
|
1127
|
+
),
|
|
1128
|
+
),
|
|
1129
|
+
element('footer', { class: 'note-inspector' },
|
|
1130
|
+
element('label', {}, element('span', {}, '类型'), element('select', {
|
|
1131
|
+
class: 'note-meta-select', onChange: event => update('type', event.target.value),
|
|
1132
|
+
}, TYPES.map(type => element('option', { value: type, selected: type === editor.type }, TYPE_LABELS[type])))),
|
|
1133
|
+
element('label', { class: 'note-tags-field' }, element('span', {}, '标签'), element('input', {
|
|
1134
|
+
value: editor.tagsText, placeholder: '用逗号分隔', onInput: event => update('tagsText', event.target.value),
|
|
1135
|
+
})),
|
|
1136
|
+
element('span', { class: 'note-format-hint' }, mode === 'edit' ? 'Markdown · Ctrl/⌘ S 保存' : 'Markdown 预览'),
|
|
1137
|
+
),
|
|
1062
1138
|
)
|
|
1063
1139
|
}
|
|
1064
1140
|
|
|
1065
|
-
function
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
onClick: async () => {
|
|
1069
|
-
state.documentView.mode = mode
|
|
1070
|
-
if (mode === 'entries') await loadEntries()
|
|
1071
|
-
else await loadDocuments()
|
|
1072
|
-
renderShell()
|
|
1073
|
-
},
|
|
1074
|
-
}, label)
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
function moveDocumentSelection(event, kind) {
|
|
1078
|
-
if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp' && event.key !== 'Enter') return
|
|
1079
|
-
const view = state.documentView
|
|
1080
|
-
const values = kind === 'base'
|
|
1081
|
-
? state.knowledgeBases.filter(base => base.status === 'active').map(base => base.id)
|
|
1082
|
-
: state.documents.filter(document => document.knowledgeBaseId === view.knowledgeBaseId).map(document => document.id)
|
|
1083
|
-
const current = kind === 'base' ? view.knowledgeBaseId : view.documentId
|
|
1084
|
-
if (event.key === 'Enter') return
|
|
1085
|
-
event.preventDefault()
|
|
1086
|
-
const index = Math.max(0, values.indexOf(current))
|
|
1087
|
-
const next = event.key === 'ArrowDown' ? Math.min(values.length - 1, index + 1) : Math.max(0, index - 1)
|
|
1088
|
-
if (!values[next]) return
|
|
1089
|
-
if (kind === 'base') { view.knowledgeBaseId = values[next]; view.documentId = ''; selectDefaultDocument() }
|
|
1090
|
-
else view.documentId = values[next]
|
|
1141
|
+
function switchDocumentMode(mode) {
|
|
1142
|
+
if (mode !== 'edit' && mode !== 'preview') return
|
|
1143
|
+
state.documentView.mode = mode
|
|
1091
1144
|
renderShell()
|
|
1092
|
-
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
function renderDocumentReader(document, base) {
|
|
1096
|
-
if (!document) return element('main', { class: 'document-reader document-reader-empty' },
|
|
1097
|
-
element('div', {}, element('strong', {}, '选择一篇文档'), element('p', {}, '文档内容会在这里显示。')))
|
|
1098
|
-
return element('main', { class: 'document-reader', 'aria-labelledby': 'document-reader-title' },
|
|
1099
|
-
element('header', { class: 'reader-toolbar' },
|
|
1100
|
-
element('div', { class: 'reader-title' }, element('span', {}, base?.name || document.knowledgeBaseId), element('strong', { id: 'document-reader-title' }, document.relPath)),
|
|
1101
|
-
element('select', {
|
|
1102
|
-
class: 'select mobile-document-picker', 'aria-label': '选择文档',
|
|
1103
|
-
onChange: (event) => { state.documentView.documentId = event.target.value; renderShell() },
|
|
1104
|
-
}, state.documents.filter(item => item.knowledgeBaseId === document.knowledgeBaseId)
|
|
1105
|
-
.map(item => element('option', { value: item.id, selected: item.id === document.id }, item.relPath))),
|
|
1106
|
-
element('div', { class: 'reader-actions' }, badge(`${document.entryCount} 条知识`), actionButton('添加内容', () => openEntryEditor(), 'small')),
|
|
1107
|
-
),
|
|
1108
|
-
element('article', { class: 'markdown-document', 'data-scroll-key': 'document-reader' }, renderMarkdown(document.content)),
|
|
1109
|
-
element('footer', { class: 'reader-footer' },
|
|
1110
|
-
element('span', {}, `自动整理 · ${formatDate(document.updatedAt)}`),
|
|
1111
|
-
element('span', {}, `SHA-256 · ${document.contentHash.slice(0, 8)}`),
|
|
1112
|
-
),
|
|
1113
|
-
)
|
|
1145
|
+
if (mode === 'edit') window.requestAnimationFrame(() => document.querySelector('.note-body-editor')?.focus())
|
|
1114
1146
|
}
|
|
1115
1147
|
|
|
1116
|
-
function
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
let code = []
|
|
1122
|
-
let inCode = false
|
|
1123
|
-
const flushParagraph = () => {
|
|
1124
|
-
if (paragraph.length) nodes.push(element('p', {}, paragraph.join(' ')))
|
|
1125
|
-
paragraph = []
|
|
1126
|
-
}
|
|
1127
|
-
const flushList = () => {
|
|
1128
|
-
if (list.length) nodes.push(element('ul', {}, list.map(item => element('li', {}, item))))
|
|
1129
|
-
list = []
|
|
1130
|
-
}
|
|
1131
|
-
const flushCode = () => {
|
|
1132
|
-
if (code.length) nodes.push(element('pre', {}, element('code', {}, code.join('\n'))))
|
|
1133
|
-
code = []
|
|
1134
|
-
}
|
|
1135
|
-
for (const raw of lines) {
|
|
1136
|
-
const line = raw.trimEnd()
|
|
1137
|
-
if (line.startsWith('```')) {
|
|
1138
|
-
flushParagraph(); flushList()
|
|
1139
|
-
if (inCode) flushCode()
|
|
1140
|
-
inCode = !inCode
|
|
1141
|
-
continue
|
|
1142
|
-
}
|
|
1143
|
-
if (inCode) { code.push(raw); continue }
|
|
1144
|
-
const heading = /^(#{1,4})\s+(.+)$/.exec(line)
|
|
1145
|
-
const bullet = /^[-*]\s+(.+)$/.exec(line)
|
|
1146
|
-
if (heading) {
|
|
1147
|
-
flushParagraph(); flushList()
|
|
1148
|
-
nodes.push(element(`h${heading[1].length}`, {}, heading[2]))
|
|
1149
|
-
} else if (line.startsWith('> ')) {
|
|
1150
|
-
flushParagraph(); flushList()
|
|
1151
|
-
nodes.push(element('blockquote', {}, line.slice(2)))
|
|
1152
|
-
} else if (bullet) {
|
|
1153
|
-
flushParagraph(); list.push(bullet[1])
|
|
1154
|
-
} else if (!line.trim()) {
|
|
1155
|
-
flushParagraph(); flushList()
|
|
1156
|
-
} else if (/^<small>.*<\/small>$/.test(line.trim())) {
|
|
1157
|
-
flushParagraph(); flushList()
|
|
1158
|
-
nodes.push(element('small', { class: 'markdown-metadata' }, line.trim().replace(/^<small>|<\/small>$/g, '')))
|
|
1159
|
-
} else paragraph.push(line.trim())
|
|
1160
|
-
}
|
|
1161
|
-
flushParagraph(); flushList(); flushCode()
|
|
1162
|
-
return nodes
|
|
1148
|
+
function renderMarkdownPreview(markdown) {
|
|
1149
|
+
if (!markdown.trim()) return element('div', { class: 'markdown-preview is-empty', role: 'document' }, '暂无正文')
|
|
1150
|
+
const preview = element('div', { class: 'markdown-preview', role: 'document' })
|
|
1151
|
+
preview.innerHTML = window.DshKnowledgeMarkdown.renderMarkdown(markdown)
|
|
1152
|
+
return preview
|
|
1163
1153
|
}
|
|
1164
1154
|
|
|
1165
|
-
function
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
const applyFilters = async () => {
|
|
1169
|
-
state.entryFilters.query = queryInput.value
|
|
1170
|
-
state.entryFilters.projectId = projectInput.value
|
|
1171
|
-
state.loading = true
|
|
1172
|
-
renderShell()
|
|
1173
|
-
try { await loadEntries() } catch (error) { state.error = friendlyError(error) } finally { state.loading = false; renderShell() }
|
|
1174
|
-
}
|
|
1175
|
-
const toolbar = element('form', { class: 'toolbar', onSubmit: (event) => { event.preventDefault(); void applyFilters() } },
|
|
1176
|
-
element('div', { class: 'search-box' }, element('span', { class: 'search-symbol', 'aria-hidden': 'true' }, '⌕'), queryInput),
|
|
1177
|
-
selectControl('状态', [{ value: 'active', label: '生效中' }, { value: 'archived', label: '已归档' }], state.entryFilters.status, (value) => { state.entryFilters.status = value; void applyFilters() }),
|
|
1178
|
-
selectControl('类型', [{ value: '', label: '全部类型' }, ...TYPES.map(type => ({ value: type, label: TYPE_LABELS[type] }))], state.entryFilters.type, (value) => { state.entryFilters.type = value; void applyFilters() }),
|
|
1179
|
-
selectControl('知识库', [{ value: '', label: '全部知识库' }, ...state.knowledgeBases.map(base => ({ value: base.id, label: base.name }))], state.entryFilters.knowledgeBaseId, (value) => { state.entryFilters.knowledgeBaseId = value; void applyFilters() }),
|
|
1180
|
-
projectInput,
|
|
1181
|
-
actionButton('搜索', () => {}, 'primary', { type: 'submit' }),
|
|
1182
|
-
)
|
|
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
|
-
),
|
|
1188
|
-
element('div', { class: 'section-heading' }, element('div', {}, element('h2', { id: 'entries-heading' }, `${STATUS_LABELS[state.entryFilters.status]} · ${state.entries.length}`), element('p', {}, '搜索范围会包含全局知识和指定项目知识'))),
|
|
1189
|
-
toolbar,
|
|
1190
|
-
state.entries.length
|
|
1191
|
-
? element('div', { class: 'card-grid' }, state.entries.map(renderEntryCard))
|
|
1192
|
-
: emptyState('没有匹配的知识', '调整筛选条件,或者手动创建第一条知识。', '新建知识', () => openEntryEditor()),
|
|
1193
|
-
state.nextCursor ? element('div', { class: 'loading' }, actionButton('加载更多', async () => {
|
|
1194
|
-
const cursor = state.nextCursor
|
|
1195
|
-
state.nextCursor = null
|
|
1196
|
-
try { await loadEntries(cursor); renderShell() } catch (error) { showToast(friendlyError(error), 'error') }
|
|
1197
|
-
})) : null,
|
|
1198
|
-
)
|
|
1155
|
+
function resizeDocumentEditor(editor) {
|
|
1156
|
+
editor.style.height = 'auto'
|
|
1157
|
+
editor.style.height = `${Math.max(420, editor.scrollHeight)}px`
|
|
1199
1158
|
}
|
|
1200
1159
|
|
|
1201
|
-
function
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
!archived ? actionButton('归档', () => confirmArchive(entry), 'ghost small') : actionButton('彻底删除', () => confirmDelete(entry), 'danger small'),
|
|
1217
|
-
),
|
|
1218
|
-
),
|
|
1219
|
-
)
|
|
1160
|
+
function confirmDeleteDocument(editor) {
|
|
1161
|
+
openConfirm({
|
|
1162
|
+
title: `删除“${editor.title}”?`,
|
|
1163
|
+
message: '文档和对应的召回知识会被永久删除,此操作无法撤销。',
|
|
1164
|
+
confirmLabel: '永久删除', danger: true,
|
|
1165
|
+
onConfirm: async () => {
|
|
1166
|
+
await api(`entries/${encodeURIComponent(editor.id)}`, { method: 'DELETE' })
|
|
1167
|
+
state.documentView.documentId = ''
|
|
1168
|
+
state.documentView.editor = null
|
|
1169
|
+
state.stats = null
|
|
1170
|
+
await loadDocuments()
|
|
1171
|
+
renderShell()
|
|
1172
|
+
showToast('文档已删除。')
|
|
1173
|
+
},
|
|
1174
|
+
})
|
|
1220
1175
|
}
|
|
1221
1176
|
|
|
1222
1177
|
function renderCandidates() {
|
|
1223
1178
|
const statuses = [['pending', '待审核'], ['approved', '已通过'], ['rejected', '已拒绝']]
|
|
1224
1179
|
return element('section', { 'aria-labelledby': 'candidates-heading' },
|
|
1225
1180
|
element('div', { class: 'section-heading' },
|
|
1226
|
-
element('div', {}, element('h2', { id: 'candidates-heading' }, 'AI 提取候选'), element('p', {}, '
|
|
1181
|
+
element('div', {}, element('h2', { id: 'candidates-heading' }, 'AI 提取候选'), element('p', {}, '审核写入、模型推断、低置信度结果和冲突项会在这里等待确认;只有高置信度且证据明确的结果才能直接写入。')),
|
|
1227
1182
|
element('div', { class: 'tabs', role: 'tablist', 'aria-label': '候选状态' }, statuses.map(([value, label]) => element('button', {
|
|
1228
1183
|
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.candidateStatus === value),
|
|
1229
1184
|
onClick: async () => { state.candidateStatus = value; await navigate('candidates') },
|
|
@@ -1236,11 +1191,17 @@ function renderCandidates() {
|
|
|
1236
1191
|
|
|
1237
1192
|
function renderCandidateCard(candidate) {
|
|
1238
1193
|
const pending = candidate.status === 'pending'
|
|
1239
|
-
const targetDocument =
|
|
1194
|
+
const targetDocument = candidate.action === 'create'
|
|
1195
|
+
? `新文档“${candidate.draft.title}”`
|
|
1196
|
+
: `文档“${candidate.draft.title}”`
|
|
1240
1197
|
return element('article', { class: 'candidate' },
|
|
1241
1198
|
element('div', { class: 'candidate-header' },
|
|
1242
1199
|
element('div', {},
|
|
1243
|
-
element('div', {},
|
|
1200
|
+
element('div', {},
|
|
1201
|
+
badge(knowledgeBaseName(candidate.draft.knowledgeBaseId)), ' ',
|
|
1202
|
+
badge(ACTION_LABELS[candidate.action], candidate.action === 'conflict' ? 'warning' : 'accent'), ' ',
|
|
1203
|
+
badge(TYPE_LABELS[candidate.draft.type]), ' ',
|
|
1204
|
+
candidate.draft.source?.evidence ? badge(EVIDENCE_LABELS[candidate.draft.source.evidence] || candidate.draft.source.evidence) : null),
|
|
1244
1205
|
element('h3', {}, candidate.draft.title),
|
|
1245
1206
|
),
|
|
1246
1207
|
badge(STATUS_LABELS[candidate.status], candidate.status === 'approved' ? 'success' : candidate.status === 'rejected' ? 'danger' : 'warning'),
|
|
@@ -1249,7 +1210,7 @@ function renderCandidateCard(candidate) {
|
|
|
1249
1210
|
element('p', { class: 'candidate-content' }, candidate.draft.body),
|
|
1250
1211
|
element('div', { class: 'candidate-reason' },
|
|
1251
1212
|
element('strong', {}, '文档变更'),
|
|
1252
|
-
element('span', { class: 'candidate-target' },
|
|
1213
|
+
element('span', { class: 'candidate-target' }, `${candidate.action === 'create' ? '创建' : '更新'}${targetDocument}`),
|
|
1253
1214
|
element('strong', {}, '模型判断依据'), candidate.reason || '未提供判断说明'),
|
|
1254
1215
|
),
|
|
1255
1216
|
element('div', { class: 'candidate-footer' },
|
|
@@ -1373,7 +1334,7 @@ function openKnowledgeBaseEditor(base) {
|
|
|
1373
1334
|
)),
|
|
1374
1335
|
provider.wrapper, model.wrapper, modelHint,
|
|
1375
1336
|
)
|
|
1376
|
-
|
|
1337
|
+
openSheet({
|
|
1377
1338
|
title: base ? '编辑知识库' : '创建知识库',
|
|
1378
1339
|
description: '匹配描述先判断对话是否属于该库;提取要求再规定应该收录什么。',
|
|
1379
1340
|
body: form,
|
|
@@ -1421,7 +1382,7 @@ function openBulkMountEditor() {
|
|
|
1421
1382
|
includeTags.wrapper,
|
|
1422
1383
|
excludeTags.wrapper,
|
|
1423
1384
|
)
|
|
1424
|
-
|
|
1385
|
+
openSheet({
|
|
1425
1386
|
title: `批量挂载到${manager.targetKind === 'project' ? '项目' : '会话'}`,
|
|
1426
1387
|
description: targetId,
|
|
1427
1388
|
body: form,
|
|
@@ -1520,7 +1481,7 @@ function openMountEditor(base, targetKind, targetId, explicit, inherited) {
|
|
|
1520
1481
|
}
|
|
1521
1482
|
enabled.addEventListener('change', updateAvailability)
|
|
1522
1483
|
updateAvailability()
|
|
1523
|
-
|
|
1484
|
+
openSheet({
|
|
1524
1485
|
title: `${targetKind === 'project' ? '项目' : '会话'}挂载 · ${base.name}`,
|
|
1525
1486
|
description: targetId,
|
|
1526
1487
|
body: form,
|
|
@@ -1646,8 +1607,8 @@ function openEntryEditor(entry, candidate) {
|
|
|
1646
1607
|
)
|
|
1647
1608
|
body.wrapper.classList.add('span-2')
|
|
1648
1609
|
tags.wrapper.classList.add('span-2')
|
|
1649
|
-
const modeTitle = candidate ? '编辑并通过候选' : entry ? '
|
|
1650
|
-
const modal =
|
|
1610
|
+
const modeTitle = candidate ? '编辑并通过候选' : entry ? '编辑知识文档' : '新建知识文档'
|
|
1611
|
+
const modal = openSheet({ title: modeTitle, description: candidate ? '保存后,这条候选会立即通过并写入知识库。' : '文档保存后会立即参与后续召回。', body: form, primaryLabel: candidate ? '通过并保存' : '保存文档', onPrimary: async () => {
|
|
1651
1612
|
if (!form.reportValidity()) return false
|
|
1652
1613
|
const draft = {
|
|
1653
1614
|
knowledgeBaseId: knowledgeBase.input.value,
|
|
@@ -1777,10 +1738,15 @@ function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
|
1777
1738
|
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
1778
1739
|
}
|
|
1779
1740
|
|
|
1780
|
-
function
|
|
1741
|
+
function openSheet(options) {
|
|
1742
|
+
return openModal({ ...options, presentation: 'sheet' })
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal' }) {
|
|
1781
1746
|
const previouslyFocused = document.activeElement
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1747
|
+
const isSheet = presentation === 'sheet'
|
|
1748
|
+
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
1749
|
+
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
1784
1750
|
let busy = false
|
|
1785
1751
|
const close = () => {
|
|
1786
1752
|
if (busy) return
|
|
@@ -1851,4 +1817,5 @@ function friendlyError(error) {
|
|
|
1851
1817
|
return error.message || '操作失败,请稍后重试。'
|
|
1852
1818
|
}
|
|
1853
1819
|
|
|
1820
|
+
installHostThemeBridge()
|
|
1854
1821
|
void boot()
|