@lemoncat7/dsh-knowledge 0.7.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.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 +224 -171
- 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 +9 -6
- 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 +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +21 -10
- package/lib/web.js.map +1 -1
- package/package.json +16 -3
- package/web/app.js +338 -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,56 @@ const state = {
|
|
|
74
72
|
|
|
75
73
|
let scrollRestoreFrame = 0
|
|
76
74
|
|
|
75
|
+
function installHostThemeBridge() {
|
|
76
|
+
if (window.parent === window) return
|
|
77
|
+
const parentOrigin = referrerOrigin()
|
|
78
|
+
window.addEventListener('message', event => {
|
|
79
|
+
if (event.source !== window.parent) return
|
|
80
|
+
if (parentOrigin && event.origin !== parentOrigin) return
|
|
81
|
+
const message = event.data
|
|
82
|
+
if (!message || message.type !== HOST_THEME_MESSAGE || message.version !== HOST_THEME_PROTOCOL_VERSION) return
|
|
83
|
+
if (message.colorScheme !== 'light' && message.colorScheme !== 'dark') return
|
|
84
|
+
if (!message.tokens || typeof message.tokens !== 'object' || Array.isArray(message.tokens)) return
|
|
85
|
+
|
|
86
|
+
const root = document.documentElement
|
|
87
|
+
for (const name of [...HOST_THEME_COLOR_TOKENS, ...HOST_THEME_STYLE_TOKENS]) root.style.removeProperty(name)
|
|
88
|
+
for (const [name, value] of Object.entries(message.tokens)) {
|
|
89
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 512) continue
|
|
90
|
+
if (HOST_THEME_COLOR_TOKENS.has(name) && CSS.supports('color', value)) root.style.setProperty(name, value)
|
|
91
|
+
else if (HOST_THEME_STYLE_TOKENS.has(name) && CSS.supports('box-shadow', value)) root.style.setProperty(name, value)
|
|
92
|
+
}
|
|
93
|
+
root.dataset.dshHostTheme = 'true'
|
|
94
|
+
root.style.colorScheme = message.colorScheme
|
|
95
|
+
document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', message.colorScheme)
|
|
96
|
+
const background = root.style.getPropertyValue('--bg')
|
|
97
|
+
if (background) document.querySelector('meta[name="theme-color"]')?.setAttribute('content', background)
|
|
98
|
+
})
|
|
99
|
+
window.parent.postMessage({
|
|
100
|
+
type: HOST_THEME_READY_MESSAGE,
|
|
101
|
+
version: HOST_THEME_PROTOCOL_VERSION,
|
|
102
|
+
}, parentOrigin || '*')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function referrerOrigin() {
|
|
106
|
+
if (!document.referrer) return ''
|
|
107
|
+
try {
|
|
108
|
+
const origin = new URL(document.referrer).origin
|
|
109
|
+
return origin === 'null' ? '' : origin
|
|
110
|
+
} catch {
|
|
111
|
+
return ''
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
77
115
|
function readDocumentLayout() {
|
|
78
116
|
const fallback = {
|
|
79
|
-
sidebarHidden: false,
|
|
80
|
-
sidebarWidth: 236,
|
|
117
|
+
sidebarHidden: false,
|
|
118
|
+
sidebarWidth: 236,
|
|
81
119
|
}
|
|
82
120
|
try {
|
|
83
121
|
const value = JSON.parse(localStorage.getItem(DOCUMENT_LAYOUT_KEY) || '{}')
|
|
84
122
|
return {
|
|
85
123
|
sidebarHidden: value.sidebarHidden === true,
|
|
86
|
-
libraryHidden: value.libraryHidden === true,
|
|
87
|
-
documentListHidden: value.documentListHidden === true,
|
|
88
124
|
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
125
|
}
|
|
92
126
|
} catch { return fallback }
|
|
93
127
|
}
|
|
@@ -96,11 +130,7 @@ function saveDocumentLayout() {
|
|
|
96
130
|
try {
|
|
97
131
|
localStorage.setItem(DOCUMENT_LAYOUT_KEY, JSON.stringify({
|
|
98
132
|
sidebarHidden: state.documentView.sidebarHidden,
|
|
99
|
-
libraryHidden: state.documentView.libraryHidden,
|
|
100
|
-
documentListHidden: state.documentView.documentListHidden,
|
|
101
133
|
sidebarWidth: state.documentView.sidebarWidth,
|
|
102
|
-
libraryWidth: state.documentView.libraryWidth,
|
|
103
|
-
documentListWidth: state.documentView.documentListWidth,
|
|
104
134
|
}))
|
|
105
135
|
} catch {}
|
|
106
136
|
}
|
|
@@ -217,7 +247,7 @@ async function boot() {
|
|
|
217
247
|
if (AUTH_MODE === 'same-origin') {
|
|
218
248
|
state.token = ''
|
|
219
249
|
try { state.service = await api('service') } catch {}
|
|
220
|
-
await navigate('
|
|
250
|
+
await navigate('entries')
|
|
221
251
|
return
|
|
222
252
|
}
|
|
223
253
|
if (!state.token) {
|
|
@@ -226,7 +256,7 @@ async function boot() {
|
|
|
226
256
|
}
|
|
227
257
|
try {
|
|
228
258
|
await api('entries?limit=1')
|
|
229
|
-
await navigate('
|
|
259
|
+
await navigate('entries')
|
|
230
260
|
} catch (error) {
|
|
231
261
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
232
262
|
state.token = ''
|
|
@@ -253,7 +283,7 @@ function renderLogin(message = '') {
|
|
|
253
283
|
try {
|
|
254
284
|
await api('entries?limit=1')
|
|
255
285
|
sessionStorage.setItem(TOKEN_KEY, token)
|
|
256
|
-
await navigate('
|
|
286
|
+
await navigate('entries')
|
|
257
287
|
} catch (requestError) {
|
|
258
288
|
state.token = ''
|
|
259
289
|
error.textContent = requestError.status === 401 ? '令牌无效或已被撤销。' : '连接失败,请检查服务状态后重试。'
|
|
@@ -377,6 +407,8 @@ async function loadDocuments() {
|
|
|
377
407
|
: bases.find(base => base.status === 'active')?.id || bases[0]?.id || ''
|
|
378
408
|
}
|
|
379
409
|
selectDefaultDocument()
|
|
410
|
+
if (view.documentId) await loadDocumentEditor(view.documentId)
|
|
411
|
+
else view.editor = null
|
|
380
412
|
if (!state.stats) await refreshStats()
|
|
381
413
|
}
|
|
382
414
|
|
|
@@ -384,10 +416,107 @@ function selectDefaultDocument() {
|
|
|
384
416
|
const view = state.documentView
|
|
385
417
|
const documents = state.documents.filter(document => document.knowledgeBaseId === view.knowledgeBaseId)
|
|
386
418
|
if (!documents.some(document => document.id === view.documentId)) {
|
|
387
|
-
view.documentId = documents
|
|
419
|
+
view.documentId = documents[0]?.id || ''
|
|
420
|
+
}
|
|
421
|
+
if (view.knowledgeBaseId) view.expandedBases.add(view.knowledgeBaseId)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
async function loadDocumentEditor(id) {
|
|
425
|
+
const entry = await api(`entries/${encodeURIComponent(id)}`)
|
|
426
|
+
state.documentView.mode = 'preview'
|
|
427
|
+
state.documentView.editor = {
|
|
428
|
+
...entry,
|
|
429
|
+
tagsText: entry.tags.join(', '),
|
|
430
|
+
dirty: false,
|
|
431
|
+
isNew: false,
|
|
432
|
+
saveState: '已保存',
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function createBlankDocument(baseId) {
|
|
437
|
+
const base = state.knowledgeBases.find(item => item.id === baseId && item.status === 'active')
|
|
438
|
+
if (!base) return showToast('请先选择一个可用知识库。', 'error')
|
|
439
|
+
const view = state.documentView
|
|
440
|
+
view.knowledgeBaseId = base.id
|
|
441
|
+
view.documentId = ''
|
|
442
|
+
view.mode = 'edit'
|
|
443
|
+
view.treeOpen = false
|
|
444
|
+
view.expandedBases.add(base.id)
|
|
445
|
+
view.editor = {
|
|
446
|
+
id: '', knowledgeBaseId: base.id, title: '', body: '', type: 'fact', tags: [], tagsText: '',
|
|
447
|
+
scope: { kind: 'global' }, confidence: .8, dirty: true, isNew: true, saveState: '新文档',
|
|
448
|
+
}
|
|
449
|
+
renderShell()
|
|
450
|
+
document.querySelector('.note-title-input')?.focus()
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
async function startBlankDocument(baseId) {
|
|
454
|
+
const editor = state.documentView.editor
|
|
455
|
+
const emptyDraft = editor?.isNew && !editor.title.trim() && !editor.body.trim()
|
|
456
|
+
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor()) return
|
|
457
|
+
createBlankDocument(baseId)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function selectDocument(id) {
|
|
461
|
+
const editor = state.documentView.editor
|
|
462
|
+
const emptyDraft = editor?.isNew && !editor.title.trim() && !editor.body.trim()
|
|
463
|
+
if (editor?.dirty && !emptyDraft && !await saveDocumentEditor()) return
|
|
464
|
+
state.documentView.documentId = id
|
|
465
|
+
state.documentView.treeOpen = false
|
|
466
|
+
await loadDocumentEditor(id)
|
|
467
|
+
renderShell()
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function editorDraft(editor) {
|
|
471
|
+
return {
|
|
472
|
+
knowledgeBaseId: editor.knowledgeBaseId,
|
|
473
|
+
title: editor.title.trim(),
|
|
474
|
+
body: editor.body.trim(),
|
|
475
|
+
type: editor.type,
|
|
476
|
+
tags: parseTags(editor.tagsText),
|
|
477
|
+
scope: editor.scope,
|
|
478
|
+
confidence: editor.confidence,
|
|
479
|
+
...(editor.source ? { source: editor.source } : {}),
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function saveDocumentEditor() {
|
|
484
|
+
const editor = state.documentView.editor
|
|
485
|
+
if (!editor || !editor.dirty) return true
|
|
486
|
+
if (!editor.title.trim() || !editor.body.trim()) {
|
|
487
|
+
showToast('标题和正文填写完整后才能保存。', 'error')
|
|
488
|
+
return false
|
|
489
|
+
}
|
|
490
|
+
editor.saveState = '正在保存…'
|
|
491
|
+
updateEditorSaveState(editor.saveState)
|
|
492
|
+
try {
|
|
493
|
+
const draft = editorDraft(editor)
|
|
494
|
+
const saved = editor.isNew
|
|
495
|
+
? await api('entries', { method: 'POST', body: { draft } })
|
|
496
|
+
: await api(`entries/${encodeURIComponent(editor.id)}`, { method: 'PUT', body: { draft } })
|
|
497
|
+
editor.id = saved.id
|
|
498
|
+
editor.isNew = false
|
|
499
|
+
editor.dirty = false
|
|
500
|
+
editor.updatedAt = saved.updatedAt
|
|
501
|
+
editor.saveState = '已保存'
|
|
502
|
+
state.documentView.documentId = saved.id
|
|
503
|
+
updateEditorSaveState(editor.saveState)
|
|
504
|
+
const documents = await api('documents')
|
|
505
|
+
state.documents = documents
|
|
506
|
+
return true
|
|
507
|
+
} catch (error) {
|
|
508
|
+
editor.saveState = '保存失败'
|
|
509
|
+
updateEditorSaveState(editor.saveState)
|
|
510
|
+
showToast(friendlyError(error), 'error')
|
|
511
|
+
return false
|
|
388
512
|
}
|
|
389
513
|
}
|
|
390
514
|
|
|
515
|
+
function updateEditorSaveState(label) {
|
|
516
|
+
const node = document.querySelector('.editor-save-status')
|
|
517
|
+
if (node) node.textContent = label
|
|
518
|
+
}
|
|
519
|
+
|
|
391
520
|
async function loadCandidates() {
|
|
392
521
|
const [candidates] = await Promise.all([
|
|
393
522
|
api(`candidates?status=${state.candidateStatus}&limit=100`),
|
|
@@ -408,9 +537,9 @@ function renderShell() {
|
|
|
408
537
|
captureScrollPosition()
|
|
409
538
|
const titles = {
|
|
410
539
|
overview: ['概览', '知识库运行状态与最近活动'],
|
|
411
|
-
bases: ['
|
|
412
|
-
entries: ['
|
|
413
|
-
candidates: ['
|
|
540
|
+
bases: ['知识库与挂载', '管理知识目录,并限定项目与会话的召回和写入范围'],
|
|
541
|
+
entries: ['知识文档', '在知识目录中阅读、整理和维护 Markdown 文档'],
|
|
542
|
+
candidates: ['待审核', '确认 AI 提取结果后再写入知识文档'],
|
|
414
543
|
tokens: ['访问管理', '管理其他客户端连接中央知识库的权限'],
|
|
415
544
|
}
|
|
416
545
|
const [title, subtitle] = titles[state.view]
|
|
@@ -427,6 +556,10 @@ function renderShell() {
|
|
|
427
556
|
element('div', { class: 'topbar-title' },
|
|
428
557
|
actionButton('☰', () => { state.menuOpen = !state.menuOpen; renderShell() }, 'ghost mobile-menu', { 'aria-label': '打开导航菜单' }),
|
|
429
558
|
paneToggleButton('main', !state.documentView.sidebarHidden, () => setSidebarHidden(!state.documentView.sidebarHidden), '主导航栏'),
|
|
559
|
+
state.view === 'entries' ? paneToggleButton('library', state.documentView.treeOpen, () => {
|
|
560
|
+
state.documentView.treeOpen = !state.documentView.treeOpen
|
|
561
|
+
renderShell()
|
|
562
|
+
}, '知识目录') : null,
|
|
430
563
|
element('div', {}, element('h1', {}, title), element('p', {}, subtitle)),
|
|
431
564
|
),
|
|
432
565
|
),
|
|
@@ -508,8 +641,8 @@ function setSidebarHidden(hidden) {
|
|
|
508
641
|
function renderSidebar() {
|
|
509
642
|
const pending = state.stats?.candidates.pending
|
|
510
643
|
const navGroups = [
|
|
511
|
-
['
|
|
512
|
-
['
|
|
644
|
+
['知识工作区', [['entries', '文档'], ['candidates', '待审核'], ['bases', '知识库与挂载']]],
|
|
645
|
+
['连接', [['tokens', '访问管理']].filter(([id]) => id !== 'tokens' || !state.service.remote)],
|
|
513
646
|
].filter(([, items]) => items.length)
|
|
514
647
|
return element('aside', { class: 'sidebar', 'aria-label': '知识库导航' },
|
|
515
648
|
element('div', { class: 'brand' },
|
|
@@ -724,7 +857,6 @@ function renderKnowledgeBaseCard(base) {
|
|
|
724
857
|
state.entryFilters.knowledgeBaseId = base.id
|
|
725
858
|
state.documentView.knowledgeBaseId = base.id
|
|
726
859
|
state.documentView.documentId = ''
|
|
727
|
-
state.documentView.mode = 'documents'
|
|
728
860
|
void navigate('entries')
|
|
729
861
|
}, 'ghost small'),
|
|
730
862
|
archived ? actionButton('恢复', () => confirmRestoreKnowledgeBase(base), 'small') : actionButton('编辑', () => openKnowledgeBaseEditor(base), 'small'),
|
|
@@ -883,347 +1015,182 @@ function compactEmpty(message) {
|
|
|
883
1015
|
}
|
|
884
1016
|
|
|
885
1017
|
function renderEntries() {
|
|
886
|
-
if (state.documentView.mode === 'entries') return renderLegacyEntries()
|
|
887
1018
|
const view = state.documentView
|
|
888
1019
|
const query = view.query.trim().toLocaleLowerCase()
|
|
889
1020
|
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)
|
|
1021
|
+
const selectedBase = activeBases.find(base => base.id === view.knowledgeBaseId)
|
|
895
1022
|
const search = element('input', {
|
|
896
|
-
class: '
|
|
1023
|
+
class: 'note-tree-search', type: 'search', value: view.query, placeholder: '搜索文档', 'aria-label': '搜索知识库文档',
|
|
897
1024
|
onInput: (event) => {
|
|
898
1025
|
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
1026
|
renderShell()
|
|
904
|
-
document.querySelector('.
|
|
1027
|
+
const input = document.querySelector('.note-tree-search')
|
|
1028
|
+
input?.focus()
|
|
1029
|
+
input?.setSelectionRange(input.value.length, input.value.length)
|
|
905
1030
|
},
|
|
906
1031
|
})
|
|
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 ? '没有匹配的文档' : '这个知识库还没有文档'),
|
|
1032
|
+
const tree = activeBases.map(base => {
|
|
1033
|
+
const expanded = view.expandedBases.has(base.id) || Boolean(query)
|
|
1034
|
+
const documents = state.documents.filter(document => document.knowledgeBaseId === base.id
|
|
1035
|
+
&& (!query || [document.title, document.relPath, document.content].some(value => value.toLocaleLowerCase().includes(query))))
|
|
1036
|
+
return element('section', { class: 'note-tree-group', 'data-expanded': String(expanded) },
|
|
1037
|
+
element('button', {
|
|
1038
|
+
type: 'button', class: 'note-tree-base', 'aria-expanded': String(expanded),
|
|
1039
|
+
onClick: () => {
|
|
1040
|
+
view.knowledgeBaseId = base.id
|
|
1041
|
+
if (expanded && !query) view.expandedBases.delete(base.id)
|
|
1042
|
+
else view.expandedBases.add(base.id)
|
|
1043
|
+
renderShell()
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
element('span', { class: 'tree-disclosure', 'aria-hidden': 'true' }),
|
|
1047
|
+
element('span', { class: 'tree-folder-icon', 'aria-hidden': 'true' }),
|
|
1048
|
+
element('span', { class: 'tree-base-name' }, base.name),
|
|
1049
|
+
element('span', { class: 'tree-count' }, documents.length)),
|
|
1050
|
+
expanded ? element('div', { class: 'note-tree-documents', role: 'group', 'aria-label': `${base.name}文档` },
|
|
1051
|
+
documents.map(document => element('button', {
|
|
1052
|
+
type: 'button', class: 'note-tree-document', 'aria-current': document.id === view.documentId ? 'page' : undefined,
|
|
1053
|
+
onClick: () => { void selectDocument(document.id) },
|
|
1054
|
+
}, element('span', { class: 'tree-document-icon', 'aria-hidden': 'true' }), element('span', { class: 'tree-document-copy' },
|
|
1055
|
+
element('strong', {}, document.title), element('small', {}, document.relPath)))),
|
|
1056
|
+
!query ? element('button', { type: 'button', class: 'note-tree-new', onClick: () => { void startBlankDocument(base.id) } },
|
|
1057
|
+
element('span', { 'aria-hidden': 'true' }, '+'), '新建文档') : null,
|
|
1058
|
+
) : null,
|
|
1059
|
+
)
|
|
1060
|
+
})
|
|
1061
|
+
return element('section', {
|
|
1062
|
+
class: 'note-workspace', 'aria-labelledby': 'documents-heading',
|
|
1063
|
+
'data-tree-open': String(view.treeOpen),
|
|
1064
|
+
},
|
|
1065
|
+
element('aside', { class: 'note-tree-panel', 'aria-label': '知识目录' },
|
|
1066
|
+
element('header', { class: 'note-tree-header' },
|
|
1067
|
+
element('div', {}, element('h2', { id: 'documents-heading' }, '知识目录'), element('span', {}, `${state.documents.length} 篇文档`)),
|
|
1068
|
+
actionButton('+', () => { void startBlankDocument(view.knowledgeBaseId || activeBases[0]?.id) }, 'ghost note-add-button', { 'aria-label': '新建文档', title: '新建文档' }),
|
|
959
1069
|
),
|
|
960
|
-
|
|
961
|
-
|
|
1070
|
+
element('div', { class: 'note-tree-search-wrap' }, interfaceIcon('search', 'search-symbol'), search),
|
|
1071
|
+
element('nav', { class: 'note-tree', 'data-scroll-key': 'note-tree' }, tree.length ? tree : element('div', { class: 'note-tree-empty' }, '还没有知识库')),
|
|
1072
|
+
element('footer', { class: 'note-tree-footer' }, actionButton('新建知识库', () => openKnowledgeBaseEditor(), 'ghost small')),
|
|
962
1073
|
),
|
|
1074
|
+
view.treeOpen ? element('button', {
|
|
1075
|
+
type: 'button', class: 'note-tree-scrim', 'aria-label': '关闭知识目录',
|
|
1076
|
+
onClick: () => { view.treeOpen = false; renderShell() },
|
|
1077
|
+
}) : null,
|
|
1078
|
+
renderNoteEditor(view.editor, selectedBase),
|
|
963
1079
|
)
|
|
964
1080
|
}
|
|
965
1081
|
|
|
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)
|
|
1082
|
+
function renderNoteEditor(editor, base) {
|
|
1083
|
+
if (!editor) return element('main', { class: 'note-editor note-editor-empty' },
|
|
1084
|
+
element('div', { class: 'note-empty-content' },
|
|
1085
|
+
element('span', { class: 'empty-document-mark', 'aria-hidden': 'true' }),
|
|
1086
|
+
element('h3', {}, '选择或新建一篇文档'),
|
|
1087
|
+
element('p', {}, '文档保存后会立即参与当前知识库的搜索与召回。'),
|
|
1088
|
+
actionButton('新建文档', () => { void startBlankDocument(state.documentView.knowledgeBaseId || state.knowledgeBases.find(item => item.status === 'active')?.id) }, 'primary'),
|
|
1089
|
+
))
|
|
1090
|
+
const saveShortcut = event => {
|
|
1091
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLocaleLowerCase() === 's') {
|
|
1092
|
+
event.preventDefault()
|
|
1093
|
+
void saveDocumentEditor().then(saved => { if (saved) renderShell() })
|
|
1094
|
+
}
|
|
1023
1095
|
}
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
handle.removeEventListener('pointercancel', finish)
|
|
1030
|
-
saveDocumentLayout()
|
|
1096
|
+
const update = (key, value) => {
|
|
1097
|
+
editor[key] = value
|
|
1098
|
+
editor.dirty = true
|
|
1099
|
+
editor.saveState = '未保存'
|
|
1100
|
+
updateEditorSaveState(editor.saveState)
|
|
1031
1101
|
}
|
|
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
|
-
|
|
1102
|
+
const title = element('input', {
|
|
1103
|
+
class: 'note-title-input', value: editor.title, maxlength: 200, placeholder: '无标题文档', 'aria-label': '文档标题',
|
|
1104
|
+
onInput: event => update('title', event.target.value), onKeyDown: saveShortcut,
|
|
1105
|
+
})
|
|
1106
|
+
const body = element('textarea', {
|
|
1107
|
+
class: 'note-body-editor', maxlength: 50000, placeholder: '从这里开始记录…', 'aria-label': '文档正文',
|
|
1108
|
+
onInput: event => { update('body', event.target.value); resizeDocumentEditor(event.target) }, onKeyDown: saveShortcut,
|
|
1109
|
+
})
|
|
1110
|
+
body.value = editor.body
|
|
1111
|
+
const mode = state.documentView.mode === 'edit' ? 'edit' : 'preview'
|
|
1112
|
+
if (mode === 'edit') window.requestAnimationFrame(() => resizeDocumentEditor(body))
|
|
1113
|
+
return element('main', { class: 'note-editor', 'aria-label': '文档编辑器' },
|
|
1114
|
+
element('header', { class: 'note-editor-toolbar' },
|
|
1115
|
+
element('div', { class: 'note-breadcrumb' },
|
|
1116
|
+
element('span', {}, base?.name || '知识库'),
|
|
1117
|
+
element('span', { 'aria-hidden': 'true' }, '/'),
|
|
1118
|
+
element('strong', {}, editor.isNew ? '新文档' : state.documents.find(item => item.id === editor.id)?.relPath || editor.title)),
|
|
1119
|
+
element('div', { class: 'note-editor-actions' },
|
|
1120
|
+
element('div', { class: 'note-mode-switch', role: 'tablist', 'aria-label': '文档视图' }, [
|
|
1121
|
+
['edit', '编辑'],
|
|
1122
|
+
['preview', '预览'],
|
|
1123
|
+
].map(([value, label]) => element('button', {
|
|
1124
|
+
type: 'button', role: 'tab', 'aria-selected': String(mode === value),
|
|
1125
|
+
onClick: () => switchDocumentMode(value),
|
|
1126
|
+
}, label))),
|
|
1127
|
+
element('span', { class: 'editor-save-status', role: 'status' }, editor.saveState),
|
|
1128
|
+
!editor.isNew ? actionButton('删除', () => confirmDeleteDocument(editor), 'ghost small') : null,
|
|
1129
|
+
actionButton(editor.isNew ? '创建文档' : '保存', () => { void saveDocumentEditor().then(saved => { if (saved) renderShell() }) }, 'primary small'),
|
|
1130
|
+
),
|
|
1131
|
+
),
|
|
1132
|
+
element('div', { class: 'note-editor-scroll', 'data-scroll-key': 'note-editor' },
|
|
1133
|
+
element('article', { class: `note-paper is-${mode}`, 'data-document-mode': mode },
|
|
1134
|
+
mode === 'edit' ? title : element('h1', { class: 'note-preview-title' }, editor.title || '无标题文档'),
|
|
1135
|
+
element('div', { class: 'note-document-meta' },
|
|
1136
|
+
element('span', {}, editor.isNew ? '尚未保存' : `更新于 ${formatDate(editor.updatedAt)}`),
|
|
1137
|
+
element('span', {}, editor.scope.kind === 'global' ? '全局知识' : `项目 · ${editor.scope.id}`)),
|
|
1138
|
+
mode === 'edit' ? body : renderMarkdownPreview(editor.body),
|
|
1139
|
+
),
|
|
1140
|
+
),
|
|
1141
|
+
element('footer', { class: 'note-inspector' },
|
|
1142
|
+
element('label', {}, element('span', {}, '类型'), element('select', {
|
|
1143
|
+
class: 'note-meta-select', onChange: event => update('type', event.target.value),
|
|
1144
|
+
}, TYPES.map(type => element('option', { value: type, selected: type === editor.type }, TYPE_LABELS[type])))),
|
|
1145
|
+
element('label', { class: 'note-tags-field' }, element('span', {}, '标签'), element('input', {
|
|
1146
|
+
value: editor.tagsText, placeholder: '用逗号分隔', onInput: event => update('tagsText', event.target.value),
|
|
1147
|
+
})),
|
|
1148
|
+
element('span', { class: 'note-format-hint' }, mode === 'edit' ? 'Markdown · Ctrl/⌘ S 保存' : 'Markdown 预览'),
|
|
1149
|
+
),
|
|
1062
1150
|
)
|
|
1063
1151
|
}
|
|
1064
1152
|
|
|
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]
|
|
1153
|
+
function switchDocumentMode(mode) {
|
|
1154
|
+
if (mode !== 'edit' && mode !== 'preview') return
|
|
1155
|
+
state.documentView.mode = mode
|
|
1091
1156
|
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
|
-
)
|
|
1157
|
+
if (mode === 'edit') window.requestAnimationFrame(() => document.querySelector('.note-body-editor')?.focus())
|
|
1114
1158
|
}
|
|
1115
1159
|
|
|
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
|
|
1160
|
+
function renderMarkdownPreview(markdown) {
|
|
1161
|
+
if (!markdown.trim()) return element('div', { class: 'markdown-preview is-empty', role: 'document' }, '暂无正文')
|
|
1162
|
+
const preview = element('div', { class: 'markdown-preview', role: 'document' })
|
|
1163
|
+
preview.innerHTML = window.DshKnowledgeMarkdown.renderMarkdown(markdown)
|
|
1164
|
+
return preview
|
|
1163
1165
|
}
|
|
1164
1166
|
|
|
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
|
-
)
|
|
1167
|
+
function resizeDocumentEditor(editor) {
|
|
1168
|
+
editor.style.height = 'auto'
|
|
1169
|
+
editor.style.height = `${Math.max(420, editor.scrollHeight)}px`
|
|
1199
1170
|
}
|
|
1200
1171
|
|
|
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
|
-
)
|
|
1172
|
+
function confirmDeleteDocument(editor) {
|
|
1173
|
+
openConfirm({
|
|
1174
|
+
title: `删除“${editor.title}”?`,
|
|
1175
|
+
message: '文档和对应的召回知识会被永久删除,此操作无法撤销。',
|
|
1176
|
+
confirmLabel: '永久删除', danger: true,
|
|
1177
|
+
onConfirm: async () => {
|
|
1178
|
+
await api(`entries/${encodeURIComponent(editor.id)}`, { method: 'DELETE' })
|
|
1179
|
+
state.documentView.documentId = ''
|
|
1180
|
+
state.documentView.editor = null
|
|
1181
|
+
state.stats = null
|
|
1182
|
+
await loadDocuments()
|
|
1183
|
+
renderShell()
|
|
1184
|
+
showToast('文档已删除。')
|
|
1185
|
+
},
|
|
1186
|
+
})
|
|
1220
1187
|
}
|
|
1221
1188
|
|
|
1222
1189
|
function renderCandidates() {
|
|
1223
1190
|
const statuses = [['pending', '待审核'], ['approved', '已通过'], ['rejected', '已拒绝']]
|
|
1224
1191
|
return element('section', { 'aria-labelledby': 'candidates-heading' },
|
|
1225
1192
|
element('div', { class: 'section-heading' },
|
|
1226
|
-
element('div', {}, element('h2', { id: 'candidates-heading' }, 'AI 提取候选'), element('p', {}, '
|
|
1193
|
+
element('div', {}, element('h2', { id: 'candidates-heading' }, 'AI 提取候选'), element('p', {}, '审核写入、模型推断、低置信度结果和冲突项会在这里等待确认;只有高置信度且证据明确的结果才能直接写入。')),
|
|
1227
1194
|
element('div', { class: 'tabs', role: 'tablist', 'aria-label': '候选状态' }, statuses.map(([value, label]) => element('button', {
|
|
1228
1195
|
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.candidateStatus === value),
|
|
1229
1196
|
onClick: async () => { state.candidateStatus = value; await navigate('candidates') },
|
|
@@ -1236,11 +1203,17 @@ function renderCandidates() {
|
|
|
1236
1203
|
|
|
1237
1204
|
function renderCandidateCard(candidate) {
|
|
1238
1205
|
const pending = candidate.status === 'pending'
|
|
1239
|
-
const targetDocument =
|
|
1206
|
+
const targetDocument = candidate.action === 'create'
|
|
1207
|
+
? `新文档“${candidate.draft.title}”`
|
|
1208
|
+
: `文档“${candidate.draft.title}”`
|
|
1240
1209
|
return element('article', { class: 'candidate' },
|
|
1241
1210
|
element('div', { class: 'candidate-header' },
|
|
1242
1211
|
element('div', {},
|
|
1243
|
-
element('div', {},
|
|
1212
|
+
element('div', {},
|
|
1213
|
+
badge(knowledgeBaseName(candidate.draft.knowledgeBaseId)), ' ',
|
|
1214
|
+
badge(ACTION_LABELS[candidate.action], candidate.action === 'conflict' ? 'warning' : 'accent'), ' ',
|
|
1215
|
+
badge(TYPE_LABELS[candidate.draft.type]), ' ',
|
|
1216
|
+
candidate.draft.source?.evidence ? badge(EVIDENCE_LABELS[candidate.draft.source.evidence] || candidate.draft.source.evidence) : null),
|
|
1244
1217
|
element('h3', {}, candidate.draft.title),
|
|
1245
1218
|
),
|
|
1246
1219
|
badge(STATUS_LABELS[candidate.status], candidate.status === 'approved' ? 'success' : candidate.status === 'rejected' ? 'danger' : 'warning'),
|
|
@@ -1249,7 +1222,7 @@ function renderCandidateCard(candidate) {
|
|
|
1249
1222
|
element('p', { class: 'candidate-content' }, candidate.draft.body),
|
|
1250
1223
|
element('div', { class: 'candidate-reason' },
|
|
1251
1224
|
element('strong', {}, '文档变更'),
|
|
1252
|
-
element('span', { class: 'candidate-target' },
|
|
1225
|
+
element('span', { class: 'candidate-target' }, `${candidate.action === 'create' ? '创建' : '更新'}${targetDocument}`),
|
|
1253
1226
|
element('strong', {}, '模型判断依据'), candidate.reason || '未提供判断说明'),
|
|
1254
1227
|
),
|
|
1255
1228
|
element('div', { class: 'candidate-footer' },
|
|
@@ -1373,7 +1346,7 @@ function openKnowledgeBaseEditor(base) {
|
|
|
1373
1346
|
)),
|
|
1374
1347
|
provider.wrapper, model.wrapper, modelHint,
|
|
1375
1348
|
)
|
|
1376
|
-
|
|
1349
|
+
openSheet({
|
|
1377
1350
|
title: base ? '编辑知识库' : '创建知识库',
|
|
1378
1351
|
description: '匹配描述先判断对话是否属于该库;提取要求再规定应该收录什么。',
|
|
1379
1352
|
body: form,
|
|
@@ -1421,7 +1394,7 @@ function openBulkMountEditor() {
|
|
|
1421
1394
|
includeTags.wrapper,
|
|
1422
1395
|
excludeTags.wrapper,
|
|
1423
1396
|
)
|
|
1424
|
-
|
|
1397
|
+
openSheet({
|
|
1425
1398
|
title: `批量挂载到${manager.targetKind === 'project' ? '项目' : '会话'}`,
|
|
1426
1399
|
description: targetId,
|
|
1427
1400
|
body: form,
|
|
@@ -1520,7 +1493,7 @@ function openMountEditor(base, targetKind, targetId, explicit, inherited) {
|
|
|
1520
1493
|
}
|
|
1521
1494
|
enabled.addEventListener('change', updateAvailability)
|
|
1522
1495
|
updateAvailability()
|
|
1523
|
-
|
|
1496
|
+
openSheet({
|
|
1524
1497
|
title: `${targetKind === 'project' ? '项目' : '会话'}挂载 · ${base.name}`,
|
|
1525
1498
|
description: targetId,
|
|
1526
1499
|
body: form,
|
|
@@ -1646,8 +1619,8 @@ function openEntryEditor(entry, candidate) {
|
|
|
1646
1619
|
)
|
|
1647
1620
|
body.wrapper.classList.add('span-2')
|
|
1648
1621
|
tags.wrapper.classList.add('span-2')
|
|
1649
|
-
const modeTitle = candidate ? '编辑并通过候选' : entry ? '
|
|
1650
|
-
const modal =
|
|
1622
|
+
const modeTitle = candidate ? '编辑并通过候选' : entry ? '编辑知识文档' : '新建知识文档'
|
|
1623
|
+
const modal = openSheet({ title: modeTitle, description: candidate ? '保存后,这条候选会立即通过并写入知识库。' : '文档保存后会立即参与后续召回。', body: form, primaryLabel: candidate ? '通过并保存' : '保存文档', onPrimary: async () => {
|
|
1651
1624
|
if (!form.reportValidity()) return false
|
|
1652
1625
|
const draft = {
|
|
1653
1626
|
knowledgeBaseId: knowledgeBase.input.value,
|
|
@@ -1777,10 +1750,15 @@ function openConfirm({ title, message, confirmLabel, danger, onConfirm }) {
|
|
|
1777
1750
|
return openModal({ title, body: element('p', {}, message), primaryLabel: confirmLabel, primaryVariant: danger ? 'danger' : 'primary', onPrimary: async () => { await onConfirm(); return true } })
|
|
1778
1751
|
}
|
|
1779
1752
|
|
|
1780
|
-
function
|
|
1753
|
+
function openSheet(options) {
|
|
1754
|
+
return openModal({ ...options, presentation: 'sheet' })
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal' }) {
|
|
1781
1758
|
const previouslyFocused = document.activeElement
|
|
1782
|
-
const
|
|
1783
|
-
const
|
|
1759
|
+
const isSheet = presentation === 'sheet'
|
|
1760
|
+
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
1761
|
+
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
1784
1762
|
let busy = false
|
|
1785
1763
|
const close = () => {
|
|
1786
1764
|
if (busy) return
|
|
@@ -1851,4 +1829,5 @@ function friendlyError(error) {
|
|
|
1851
1829
|
return error.message || '操作失败,请稍后重试。'
|
|
1852
1830
|
}
|
|
1853
1831
|
|
|
1832
|
+
installHostThemeBridge()
|
|
1854
1833
|
void boot()
|