@lemoncat7/dsh-knowledge 2.9.4 → 2.9.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/docs/architecture.md +6 -0
- package/docs/dsh-015-compatibility.md +21 -0
- package/docs/releases/2.9.5.md +11 -0
- package/docs/releases/2.9.6.md +12 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +14 -0
- package/lib/api.js.map +1 -1
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +298 -100
- package/lib/client.js.map +4 -4
- package/lib/docked-panel-compat.d.ts +13 -0
- package/lib/docked-panel-compat.d.ts.map +1 -0
- package/lib/docked-panel-compat.js +116 -0
- package/lib/docked-panel-compat.js.map +1 -0
- package/lib/knowledge-activity-controller.d.ts.map +1 -1
- package/lib/knowledge-activity-controller.js +36 -0
- package/lib/knowledge-activity-controller.js.map +1 -1
- package/lib/knowledge-activity-state.d.ts +2 -2
- package/lib/knowledge-activity-state.d.ts.map +1 -1
- package/lib/knowledge-activity-state.js +5 -3
- package/lib/knowledge-activity-state.js.map +1 -1
- package/lib/local-provider.d.ts +3 -0
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +50 -0
- package/lib/local-provider.js.map +1 -1
- package/lib/main-panel-compat.d.ts +5 -0
- package/lib/main-panel-compat.d.ts.map +1 -0
- package/lib/main-panel-compat.js +38 -0
- package/lib/main-panel-compat.js.map +1 -0
- package/lib/note-excerpt.d.ts +12 -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/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 +1 -0
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +13 -0
- package/web/note-editor.js +57 -56
- package/web/note-excerpt.js +118 -0
- package/web/styles.css +6 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** Selection is captured before opening the dialog; no editor ownership here. */
|
|
2
|
+
export async function openNoteExcerpt({ node, text, api, element, openSheet, showToast, friendlyError, formField, selectField, knowledgeBasePathLabel }) {
|
|
3
|
+
if (!text.trim() || text.length > 50000) throw new Error('每次请选择 1 到 50000 字的笔记内容。')
|
|
4
|
+
const bases = (await api('knowledge-bases')).filter(base => base.status === 'active')
|
|
5
|
+
if (!bases.length) throw new Error('请先创建一个可用的知识库。')
|
|
6
|
+
const base = selectField('目标知识库', bases.map(item => ({ value: item.id, label: knowledgeBasePathLabel(item) })), bases[0].id)
|
|
7
|
+
const mode = selectField('添加方式', [{ value: 'existing', label: '追加到已有知识文档' }, { value: 'new', label: '新建知识文档' }], 'existing')
|
|
8
|
+
const title = formField('新文档标题', 'text', node.name.replace(/\.md$/i, ''), { maxlength: 200 })
|
|
9
|
+
const search = formField('搜索知识文档', 'search', '', { placeholder: '输入关键词,或输入 / 浏览全部文档' })
|
|
10
|
+
for (const [key, field] of Object.entries({ base, mode, title, search })) {
|
|
11
|
+
field.input.id = `excerpt-${key}-${crypto.randomUUID()}`
|
|
12
|
+
field.wrapper.querySelector('label').htmlFor = field.input.id
|
|
13
|
+
field.wrapper.classList.add('span-2')
|
|
14
|
+
}
|
|
15
|
+
title.wrapper.hidden = true
|
|
16
|
+
const results = element('div', { class: 'note-picker-results span-2', 'aria-live': 'polite' })
|
|
17
|
+
const status = element('p', { class: 'muted span-2', role: 'status' }, '请选择目标文档')
|
|
18
|
+
const fields = element('fieldset', { class: 'form-grid note-excerpt-fields' }, base.wrapper, mode.wrapper, title.wrapper, search.wrapper, results, status)
|
|
19
|
+
// Only bound the visual preview; submission always retains the entire selection.
|
|
20
|
+
const preview = text.length > 1200 ? `${text.slice(0, 1200)}\n…(预览已省略,完整选文仍会添加)` : text
|
|
21
|
+
const form = element('form', {}, fields, element('blockquote', { class: 'note-excerpt-preview' }, preview))
|
|
22
|
+
form.addEventListener('submit', event => event.preventDefault())
|
|
23
|
+
let selected, requestId = crypto.randomUUID(), sequence = 0, timer, closed = false, pending = false
|
|
24
|
+
const controller = new AbortController()
|
|
25
|
+
const modal = openSheet({
|
|
26
|
+
title: '摘录到知识库', description: '追加选中文字并链接到原笔记,同时建立笔记引用。原笔记保留,不自动同步正文。',
|
|
27
|
+
body: form, primaryLabel: '添加摘录',
|
|
28
|
+
onClose: () => { closed = true; clearTimeout(timer); controller.abort() },
|
|
29
|
+
onPrimary: async () => {
|
|
30
|
+
if (mode.input.value === 'existing' && !selected) throw new Error('请先选择一个可编辑的知识文档。')
|
|
31
|
+
if (mode.input.value === 'new' && !title.input.value.trim()) throw new Error('请填写新文档标题。')
|
|
32
|
+
pending = true
|
|
33
|
+
fields.disabled = true
|
|
34
|
+
try {
|
|
35
|
+
const entry = await api('note-excerpts', { method: 'POST', body: {
|
|
36
|
+
requestId, noteId: node.id, text, knowledgeBaseId: base.input.value,
|
|
37
|
+
...(mode.input.value === 'existing' ? { documentId: selected.id, expectedVersion: selected.version } : { title: title.input.value.trim() }),
|
|
38
|
+
} })
|
|
39
|
+
showToast(`已添加到「${entry.title}」,并关联来源笔记。`)
|
|
40
|
+
return true
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (error.status === 409) { selected = undefined; status.textContent = '文档发生变化,请重新选择目标后再添加。'; requestId = crypto.randomUUID() }
|
|
43
|
+
throw error
|
|
44
|
+
} finally { pending = false; fields.disabled = false }
|
|
45
|
+
},
|
|
46
|
+
})
|
|
47
|
+
const primary = modal.dialog.querySelector('.dialog-footer button:last-child')
|
|
48
|
+
function sync() { primary.disabled = pending || (mode.input.value === 'existing' ? !selected : !title.input.value.trim()) }
|
|
49
|
+
async function load(cursor) {
|
|
50
|
+
const current = ++sequence
|
|
51
|
+
if (closed || mode.input.value !== 'existing') return
|
|
52
|
+
const query = search.input.value.trim()
|
|
53
|
+
if (!query) {
|
|
54
|
+
selected = undefined; results.replaceChildren(); results.hidden = true
|
|
55
|
+
status.textContent = '输入关键词搜索,输入 / 按目录顺序浏览全部文档。'
|
|
56
|
+
sync(); return
|
|
57
|
+
}
|
|
58
|
+
results.hidden = false
|
|
59
|
+
if (!cursor) { selected = undefined; results.replaceChildren(); status.textContent = '正在读取文档…'; sync() }
|
|
60
|
+
try {
|
|
61
|
+
const params = new URLSearchParams({ knowledgeBaseId: base.input.value, q: query === '/' ? '' : query, active: '1', limit: '50', ...(cursor ? { cursor } : {}) })
|
|
62
|
+
const page = await api(`document-index?${params}`, { signal: controller.signal })
|
|
63
|
+
if (closed || current !== sequence) return
|
|
64
|
+
results.querySelector('[data-load-more]')?.remove()
|
|
65
|
+
for (const doc of page.items) {
|
|
66
|
+
const button = element('button', { type: 'button', class: 'note-picker-row note-excerpt-document-row', title: doc.title, 'aria-label': doc.title, disabled: doc.documentState !== 'open', 'aria-pressed': 'false' },
|
|
67
|
+
element('span', {}, element('strong', {}, doc.title), element('small', { title: doc.relPath || '' }, `${doc.relPath || '根目录'}${doc.documentState === 'open' ? '' : ' · 已定稿,不能追加'}`)))
|
|
68
|
+
button.addEventListener('click', async () => {
|
|
69
|
+
if (pending) return
|
|
70
|
+
const choice = ++sequence
|
|
71
|
+
selected = undefined; sync(); status.textContent = '正在读取目标版本…'
|
|
72
|
+
try {
|
|
73
|
+
const entry = await api(`entries/${encodeURIComponent(doc.id)}`, { signal: controller.signal })
|
|
74
|
+
if (closed || choice !== sequence) return
|
|
75
|
+
if (entry.status !== 'active' || entry.documentState !== 'open' || entry.knowledgeBaseId !== base.input.value) throw new Error('文档已移动或不可编辑,请重新搜索。')
|
|
76
|
+
selected = entry; requestId = crypto.randomUUID()
|
|
77
|
+
results.querySelectorAll('button[aria-pressed]').forEach(item => item.setAttribute('aria-pressed', String(item === button)))
|
|
78
|
+
status.textContent = `将追加到:${entry.title}`; sync()
|
|
79
|
+
} catch (error) { if (!closed && choice === sequence) status.textContent = friendlyError(error) }
|
|
80
|
+
})
|
|
81
|
+
results.append(button)
|
|
82
|
+
}
|
|
83
|
+
if (page.nextCursor) {
|
|
84
|
+
const more = element('button', { type: 'button', class: 'note-picker-row note-excerpt-document-row', 'data-load-more': '' }, '加载更多')
|
|
85
|
+
more.addEventListener('click', () => { more.disabled = true; void load(page.nextCursor) })
|
|
86
|
+
results.append(more)
|
|
87
|
+
}
|
|
88
|
+
status.textContent = results.children.length ? '请选择目标文档,或切换为新建知识文档。' : '没有匹配的文档,可以新建。'
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (!closed && current === sequence) {
|
|
91
|
+
status.textContent = friendlyError(error)
|
|
92
|
+
const more = results.querySelector('[data-load-more]')
|
|
93
|
+
if (more) more.disabled = false
|
|
94
|
+
else {
|
|
95
|
+
const retry = element('button', { type: 'button', class: 'note-picker-row note-excerpt-document-row' }, '重新加载文档')
|
|
96
|
+
retry.addEventListener('click', () => { void load() })
|
|
97
|
+
results.replaceChildren(retry)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
base.input.addEventListener('change', () => { requestId = crypto.randomUUID(); if (mode.input.value === 'existing') void load(); else sync() })
|
|
103
|
+
mode.input.addEventListener('change', () => {
|
|
104
|
+
clearTimeout(timer); sequence++; requestId = crypto.randomUUID(); selected = undefined
|
|
105
|
+
const creating = mode.input.value === 'new'
|
|
106
|
+
title.wrapper.hidden = !creating; search.wrapper.hidden = creating; results.hidden = creating
|
|
107
|
+
status.textContent = creating ? '将新建文档并自动引用来源笔记。' : '请选择目标文档'
|
|
108
|
+
sync(); if (!creating) void load()
|
|
109
|
+
})
|
|
110
|
+
title.input.addEventListener('input', () => { requestId = crypto.randomUUID(); sync() })
|
|
111
|
+
search.input.addEventListener('input', () => {
|
|
112
|
+
clearTimeout(timer); sequence++; selected = undefined; results.replaceChildren(); results.hidden = true; sync()
|
|
113
|
+
if (!search.input.value.trim()) { void load(); return }
|
|
114
|
+
status.textContent = '正在搜索文档…'
|
|
115
|
+
timer = setTimeout(() => void load(), 200)
|
|
116
|
+
})
|
|
117
|
+
sync(); void load()
|
|
118
|
+
}
|
package/web/styles.css
CHANGED
|
@@ -1201,6 +1201,11 @@ html,
|
|
|
1201
1201
|
.notes-selection-menu { position: fixed; z-index: 70; max-width: calc(100vw - 16px); border: 1px solid var(--panel-border); border-radius: 10px; padding: 4px; background: var(--dialog-surface); box-shadow: var(--shadow); }
|
|
1202
1202
|
.notes-selection-primary { display: flex; align-items: center; gap: 2px; overflow-x: auto; scrollbar-width: none; }
|
|
1203
1203
|
.notes-selection-primary::-webkit-scrollbar { display: none; }
|
|
1204
|
+
.notes-selection-excerpt { grid-column: span 2; }
|
|
1205
|
+
.note-excerpt-fields { border: 0; padding: 0; margin: 0; min-width: 0; }
|
|
1206
|
+
.note-excerpt-fields [hidden] { display: none; }
|
|
1207
|
+
.note-excerpt-preview { margin: 16px 0 0; padding: 12px; max-height: 180px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere; border-inline-start: 2px solid var(--border); }
|
|
1208
|
+
.note-picker-row[aria-pressed="true"] { background: var(--surface-hover); outline: 1px solid var(--border); outline-offset: -1px; }
|
|
1204
1209
|
.notes-selection-block-toggle { max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
|
|
1205
1210
|
.notes-selection-mark { min-width: 34px; padding-inline: 7px; font-family: var(--font-mono); }
|
|
1206
1211
|
.notes-selection-block-menu { display: grid; grid-template-columns: repeat(2, minmax(92px, 1fr)); gap: 3px; border-top: 1px solid var(--separator); margin-top: 4px; padding-top: 4px; }
|
|
@@ -1313,6 +1318,7 @@ html,
|
|
|
1313
1318
|
.note-picker-row:last-child { border-bottom: 0; }
|
|
1314
1319
|
.note-picker-row:hover { background: var(--surface-hover); }
|
|
1315
1320
|
.note-picker-row > span:last-child { min-width: 0; }
|
|
1321
|
+
.note-picker-row.note-excerpt-document-row { grid-template-columns: minmax(0, 1fr); }
|
|
1316
1322
|
.note-picker-row strong, .note-picker-row small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1317
1323
|
.note-picker-row strong { font-size: 12px; }
|
|
1318
1324
|
.note-picker-row small { margin-top: 2px; color: var(--text-tertiary); font-size: 10px; }
|