@lemoncat7/dsh-knowledge 2.9.5 → 2.10.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 +8 -2
- package/docs/architecture.md +6 -0
- package/docs/document-groups.md +22 -0
- package/docs/releases/2.10.0.md +17 -0
- package/docs/releases/2.9.6.md +12 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +36 -1
- package/lib/api.js.map +1 -1
- package/lib/client.js +34 -11
- package/lib/client.js.map +3 -3
- package/lib/document-group-tools.d.ts +6 -0
- package/lib/document-group-tools.d.ts.map +1 -0
- package/lib/document-group-tools.js +96 -0
- package/lib/document-group-tools.js.map +1 -0
- package/lib/document-groups.d.ts +8 -0
- package/lib/document-groups.d.ts.map +1 -0
- package/lib/document-groups.js +18 -0
- package/lib/document-groups.js.map +1 -0
- package/lib/documents/markdown.d.ts +1 -0
- package/lib/documents/markdown.d.ts.map +1 -1
- package/lib/documents/markdown.js +2 -0
- package/lib/documents/markdown.js.map +1 -1
- package/lib/domain.d.ts +6 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js +2 -0
- package/lib/domain.js.map +1 -1
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +16 -4
- package/lib/extraction.js.map +1 -1
- package/lib/knowledge-activity-panel.d.ts.map +1 -1
- package/lib/knowledge-activity-panel.js +23 -1
- package/lib/knowledge-activity-panel.js.map +1 -1
- package/lib/local-provider.d.ts +9 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +143 -18
- package/lib/local-provider.js.map +1 -1
- package/lib/note-excerpt.d.ts +13 -0
- package/lib/note-excerpt.d.ts.map +1 -0
- package/lib/note-excerpt.js +9 -0
- package/lib/note-excerpt.js.map +1 -0
- package/lib/provider.d.ts +3 -0
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +2 -0
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +6 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/retrieval.d.ts +1 -0
- package/lib/retrieval.d.ts.map +1 -1
- package/lib/retrieval.js +2 -1
- package/lib/retrieval.js.map +1 -1
- package/lib/storage/migrations.d.ts.map +1 -1
- package/lib/storage/migrations.js +18 -1
- package/lib/storage/migrations.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +3 -0
- package/lib/tools.js.map +1 -1
- package/lib/tracking.d.ts.map +1 -1
- package/lib/tracking.js +5 -0
- package/lib/tracking.js.map +1 -1
- package/lib/web-note-editor-chrome.d.ts +1 -0
- package/lib/web-note-editor-chrome.d.ts.map +1 -1
- package/lib/web-note-editor-chrome.js +1 -0
- package/lib/web-note-editor-chrome.js.map +1 -1
- package/lib/web-note-editor-selection.d.ts +1 -0
- package/lib/web-note-editor-selection.d.ts.map +1 -1
- package/lib/web-note-editor-selection.js +21 -1
- package/lib/web-note-editor-selection.js.map +1 -1
- package/lib/web-note-editor.d.ts +2 -0
- package/lib/web-note-editor.d.ts.map +1 -1
- package/lib/web-note-editor.js +12 -0
- package/lib/web-note-editor.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +2 -1
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +101 -12
- package/web/document-groups.js +99 -0
- package/web/note-editor.js +57 -56
- package/web/note-excerpt.js +128 -0
- package/web/styles.css +30 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Document classification controls. No document content or path mutations. */
|
|
2
|
+
export function createDocumentGroupField({ element, api, baseId, value = '', required = false, onChange = () => {} }) {
|
|
3
|
+
const id = `document-group-${crypto.randomUUID()}`
|
|
4
|
+
const newValue = `new-${crypto.randomUUID()}`
|
|
5
|
+
const status = element('small', { class: 'muted', role: 'status' }, '正在读取已有分组…')
|
|
6
|
+
const select = element('select', { id, class: 'select', 'aria-label': '文档分组' }, element('option', { value: '' }, '选择已有分组'))
|
|
7
|
+
const input = element('input', { type: 'text', class: 'input', maxlength: 64, value, placeholder: '例如:部署运维、项目约定', 'aria-label': '新分组名称' })
|
|
8
|
+
input.hidden = true
|
|
9
|
+
let groups = [], current = value
|
|
10
|
+
const notify = () => {
|
|
11
|
+
current = select.value === newValue ? input.value.trim() : select.value
|
|
12
|
+
status.textContent = '优先选择已有分组;没有合适的再新建。'
|
|
13
|
+
status.setAttribute('role', 'status')
|
|
14
|
+
wrapper.querySelectorAll('[aria-invalid]').forEach(control => control.removeAttribute('aria-invalid'))
|
|
15
|
+
onChange(current)
|
|
16
|
+
}
|
|
17
|
+
select.addEventListener('change', () => { input.hidden = select.value !== newValue; notify(); if (!input.hidden) input.focus() })
|
|
18
|
+
input.addEventListener('input', notify)
|
|
19
|
+
const wrapper = element('div', { class: 'document-group-field' }, element('label', { for: id }, `文档分组${required ? ' *' : ''}`), select, input, status)
|
|
20
|
+
const ready = api(`document-groups?${new URLSearchParams({ knowledgeBaseId: baseId })}`).then(result => {
|
|
21
|
+
groups = result.filter(item => item.name)
|
|
22
|
+
select.replaceChildren(element('option', { value: '' }, required ? '请选择分组' : '未分组'),
|
|
23
|
+
...groups.map(item => element('option', { value: item.name }, `${item.name}(${item.count})`)), element('option', { value: newValue }, '+ 新建分组…'))
|
|
24
|
+
const matched = groups.find(item => item.name.normalize('NFKC').toLocaleLowerCase() === value.normalize('NFKC').trim().toLocaleLowerCase())
|
|
25
|
+
select.value = matched?.name || (value ? newValue : '')
|
|
26
|
+
if (matched) current = matched.name
|
|
27
|
+
input.hidden = select.value !== newValue
|
|
28
|
+
status.textContent = '优先选择已有分组;没有合适的再新建。'
|
|
29
|
+
return groups
|
|
30
|
+
}).catch(error => {
|
|
31
|
+
status.textContent = `读取分组失败:${error.message}。请重新打开后重试。`
|
|
32
|
+
select.disabled = true
|
|
33
|
+
throw error
|
|
34
|
+
})
|
|
35
|
+
// Callers may await ready before saving. Avoid unhandled rejections in inline editors.
|
|
36
|
+
void ready.catch(() => {})
|
|
37
|
+
const validate = () => {
|
|
38
|
+
const name = current.normalize('NFKC').trim().replace(/\s+/gu, ' ')
|
|
39
|
+
const error = required && (!name || name === '未分组') ? '请选择已有分组,或填写新分组名称。'
|
|
40
|
+
: name.length > 64 || /[\u0000-\u001f\u007f]/u.test(current) ? '分组限 64 字,不能包含控制字符。' : ''
|
|
41
|
+
if (error) {
|
|
42
|
+
status.textContent = error; status.setAttribute('role', 'alert')
|
|
43
|
+
const control = input.hidden ? wrapper.querySelector('[role="combobox"]') || select : input
|
|
44
|
+
control.setAttribute('aria-invalid', 'true'); control.focus()
|
|
45
|
+
throw new Error(error)
|
|
46
|
+
}
|
|
47
|
+
return name
|
|
48
|
+
}
|
|
49
|
+
return { wrapper, ready, value: () => current, validate }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function renderDocumentGroups({ element, documents, baseId, collapsed, searching, renderRow, onDrop, onNew }) {
|
|
53
|
+
const groups = new Map()
|
|
54
|
+
for (const doc of documents) { const name = doc.group || ''; if (!groups.has(name)) groups.set(name, []); groups.get(name).push(doc) }
|
|
55
|
+
return [...groups].sort(([a], [b]) => !a ? 1 : !b ? -1 : a.localeCompare(b, 'zh-CN')).map(([name, members]) => {
|
|
56
|
+
const key = JSON.stringify([baseId, name])
|
|
57
|
+
let expanded = searching || !collapsed.has(key)
|
|
58
|
+
const body = element('div', { class: 'document-group-body', role: 'group', 'aria-label': `${name || '未分组'}文档` })
|
|
59
|
+
const toggle = element('button', { type: 'button', class: 'document-group-toggle', 'aria-expanded': String(expanded), onClick: () => {
|
|
60
|
+
expanded = !expanded; if (expanded) collapsed.delete(key); else collapsed.add(key); paint()
|
|
61
|
+
} }, element('span', { class: 'tree-disclosure', 'aria-hidden': 'true' }), element('span', { class: 'tree-folder-icon', 'aria-hidden': 'true' }),
|
|
62
|
+
element('span', { class: 'document-group-name', title: name || '未分组' }, name || '未分组'), element('small', { title: '当前已加载的文档数' }, members.length))
|
|
63
|
+
const paint = () => { toggle.setAttribute('aria-expanded', String(expanded)); body.hidden = !expanded; body.replaceChildren(...(expanded ? members.map(renderRow) : [])) }
|
|
64
|
+
paint()
|
|
65
|
+
return element('section', { class: 'document-tree-group', 'data-document-group': name, onDragOver: event => {
|
|
66
|
+
if (!onDrop || !event.dataTransfer.types.includes('application/x-dsh-knowledge-document-id')) return
|
|
67
|
+
event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'move'
|
|
68
|
+
}, onDrop: event => { if (onDrop) { event.stopPropagation(); onDrop(event, name) } } },
|
|
69
|
+
element('div', { class: 'document-group-heading' }, toggle, name && onNew ? element('button', { type: 'button', class: 'button ghost small document-group-add', 'aria-label': `在${name}新建文档`, title: '在此分组新建文档', onClick: () => onNew(name) }, '+') : null), body)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function openDocumentGroupOrganizer({ element, api, openSheet, baseId, onApply }) {
|
|
74
|
+
const field = createDocumentGroupField({ element, api, baseId })
|
|
75
|
+
const selected = new Set()
|
|
76
|
+
let loaded = false
|
|
77
|
+
const status = element('small', { class: 'muted', role: 'status' }, '正在读取文档…')
|
|
78
|
+
const list = element('div', { class: 'document-group-selection' })
|
|
79
|
+
const controller = new AbortController()
|
|
80
|
+
const body = element('div', { class: 'document-group-field' }, field.wrapper, status, list)
|
|
81
|
+
const dialog = openSheet({ title: '整理文档分组', description: '选择文档并调整分组。选择“未分组”只移除归类,不会删除文档。', body, primaryLabel: '应用分组',
|
|
82
|
+
onClose: () => controller.abort(),
|
|
83
|
+
onPrimary: async () => { await field.ready; if (!loaded) throw new Error('请等待文档加载完成;加载失败请重新打开。'); if (!selected.size) throw new Error('请至少选择一篇文档。'); await onApply([...selected], field.validate()); return true },
|
|
84
|
+
})
|
|
85
|
+
try {
|
|
86
|
+
let cursor, count = 0
|
|
87
|
+
do {
|
|
88
|
+
const page = await api(`document-index?${new URLSearchParams({ knowledgeBaseId: baseId, limit: '100', ...(cursor ? { cursor } : {}) })}`, { signal: controller.signal })
|
|
89
|
+
for (const doc of page.items) {
|
|
90
|
+
const check = element('input', { type: 'checkbox', 'aria-label': doc.title, onChange: event => { if (event.target.checked) selected.add(doc.id); else selected.delete(doc.id) } })
|
|
91
|
+
list.append(element('label', { class: 'document-group-choice' }, check, element('span', {}, element('strong', { title: doc.title }, doc.title), element('small', {}, doc.group || '未分组'))))
|
|
92
|
+
}
|
|
93
|
+
count += page.items.length; cursor = page.nextCursor
|
|
94
|
+
status.textContent = `已加载 ${count} / ${page.total} 篇;每次最多整理 500 篇。`
|
|
95
|
+
} while (cursor && count < 500)
|
|
96
|
+
loaded = true
|
|
97
|
+
} catch (error) { if (!controller.signal.aborted) status.textContent = `读取失败:${error.message}` }
|
|
98
|
+
return dialog
|
|
99
|
+
}
|