@lemoncat7/dsh-knowledge 2.3.0 → 2.3.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 +11 -4
- package/docs/architecture.md +1 -1
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +93 -0
- package/lib/api.js.map +1 -1
- package/lib/client.js +1 -1
- package/lib/client.js.map +1 -1
- package/lib/extraction.js +9 -2
- package/lib/extraction.js.map +1 -1
- package/lib/notes/domain.d.ts +8 -0
- package/lib/notes/domain.d.ts.map +1 -1
- package/lib/notes/domain.js.map +1 -1
- package/lib/notes/share-import.d.ts +36 -0
- package/lib/notes/share-import.d.ts.map +1 -0
- package/lib/notes/share-import.js +325 -0
- package/lib/notes/share-import.js.map +1 -0
- package/lib/notes/share-page.d.ts +12 -0
- package/lib/notes/share-page.d.ts.map +1 -0
- package/lib/notes/share-page.js +81 -0
- package/lib/notes/share-page.js.map +1 -0
- package/lib/notes/store.d.ts +15 -1
- package/lib/notes/store.d.ts.map +1 -1
- package/lib/notes/store.js +177 -6
- package/lib/notes/store.js.map +1 -1
- package/lib/web-note-editor-selection.d.ts.map +1 -1
- package/lib/web-note-editor-selection.js +23 -0
- package/lib/web-note-editor-selection.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +7 -6
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +303 -29
- package/web/index.html +1 -2
- package/web/note-editor.js +64 -64
- package/web/styles.css +168 -291
- package/lib/web-workspace-effects.d.ts +0 -13
- package/lib/web-workspace-effects.d.ts.map +0 -1
- package/lib/web-workspace-effects.js +0 -366
- package/lib/web-workspace-effects.js.map +0 -1
- package/web/workspace-effects.js +0 -16
package/web/app.js
CHANGED
|
@@ -90,7 +90,7 @@ const state = {
|
|
|
90
90
|
children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(),
|
|
91
91
|
selectedId: '', selectedNode: null, currentFolderId: null, breadcrumbs: [],
|
|
92
92
|
content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [],
|
|
93
|
-
transfer: null, loadingNodeId: '',
|
|
93
|
+
transfer: null, loadingNodeId: '', shares: [], shareError: '',
|
|
94
94
|
},
|
|
95
95
|
service: { publicApiEnabled: false, publicApiPrefix: '/knowledge-api/v1', remote: false },
|
|
96
96
|
scrollPositions: new Map(),
|
|
@@ -184,10 +184,6 @@ function restoreScrollPosition(view) {
|
|
|
184
184
|
})
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
-
function refreshWorkspaceEffects(root = document) {
|
|
188
|
-
window.DshKnowledgeEffects?.refresh(root)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
187
|
const { api, binaryRequest, binaryUploadRequest } = createApiClient({
|
|
192
188
|
apiBase: API_BASE,
|
|
193
189
|
authMode: AUTH_MODE,
|
|
@@ -259,7 +255,6 @@ function renderLogin(message = '') {
|
|
|
259
255
|
element('p', {}, '管理对话中沉淀的长期知识与 AI 提取候选。'),
|
|
260
256
|
form,
|
|
261
257
|
)))
|
|
262
|
-
refreshWorkspaceEffects(app)
|
|
263
258
|
}
|
|
264
259
|
|
|
265
260
|
function signOut() {
|
|
@@ -268,7 +263,7 @@ function signOut() {
|
|
|
268
263
|
releaseNoteAsset()
|
|
269
264
|
sessionStorage.removeItem(TOKEN_KEY)
|
|
270
265
|
Object.assign(state, { token: '', stats: null, overview: null, knowledgeBases: [], mounts: [], resolvedMounts: [], entries: [], documents: [], candidates: [], candidateTargets: new Map(), settings: { writebackPolicy: 'conservative', updatedAt: '' }, tokens: [] })
|
|
271
|
-
state.notes = { children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(), selectedId: '', selectedNode: null, currentFolderId: null, breadcrumbs: [], content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [], transfer: null, loadingNodeId: '' }
|
|
266
|
+
state.notes = { children: new Map(), loadedFolders: new Set(), expandedFolders: new Set(), selectedId: '', selectedNode: null, currentFolderId: null, breadcrumbs: [], content: '', draft: '', dirty: false, assetUrl: '', query: '', searchResults: [], transfer: null, loadingNodeId: '', shares: [], shareError: '' }
|
|
272
267
|
if (AUTH_MODE === 'same-origin') void boot()
|
|
273
268
|
else renderLogin()
|
|
274
269
|
}
|
|
@@ -298,6 +293,7 @@ async function navigate(view) {
|
|
|
298
293
|
if (view === 'entries') await loadDocuments(controller.signal, (label, progress) => updateLoadingPhase(request, label, progress))
|
|
299
294
|
if (view === 'candidates') await loadCandidates(controller.signal)
|
|
300
295
|
if (view === 'notes') await loadNotes(controller.signal, (label, progress) => updateLoadingPhase(request, label, progress))
|
|
296
|
+
if (view === 'shares') await loadNoteShares(controller.signal)
|
|
301
297
|
if (view === 'tokens') await loadTokens(controller.signal)
|
|
302
298
|
} catch (error) {
|
|
303
299
|
if (controller.signal.aborted) return
|
|
@@ -329,7 +325,7 @@ function cancelPendingSearches() {
|
|
|
329
325
|
function loadingPhaseForView(view) {
|
|
330
326
|
return ({
|
|
331
327
|
overview: '正在汇总知识活动', bases: '正在读取知识库配置', entries: '正在读取知识目录',
|
|
332
|
-
notes: '正在读取笔记目录', candidates: '正在准备审核队列', tokens: '正在读取访问权限',
|
|
328
|
+
notes: '正在读取笔记目录', shares: '正在读取分享列表', candidates: '正在准备审核队列', tokens: '正在读取访问权限',
|
|
333
329
|
})[view] || '正在准备工作区'
|
|
334
330
|
}
|
|
335
331
|
|
|
@@ -476,7 +472,11 @@ async function loadDocuments(signal, onPhase = () => {}) {
|
|
|
476
472
|
|
|
477
473
|
async function loadNotes(signal, onPhase = () => {}) {
|
|
478
474
|
onPhase('正在读取顶层目录', .36)
|
|
479
|
-
|
|
475
|
+
const [rootNodes] = await Promise.all([
|
|
476
|
+
api('notes?limit=500', { signal }),
|
|
477
|
+
loadNoteShares(signal),
|
|
478
|
+
])
|
|
479
|
+
state.notes.children.set('root', rootNodes)
|
|
480
480
|
state.notes.loadedFolders.add('root')
|
|
481
481
|
if (state.notes.selectedId) {
|
|
482
482
|
onPhase('正在恢复上次打开的笔记', .74)
|
|
@@ -944,11 +944,12 @@ function renderShell() {
|
|
|
944
944
|
bases: ['知识库与挂载', '管理知识目录,并限定项目与会话的召回和写入范围'],
|
|
945
945
|
entries: ['知识文档', '在知识目录中阅读、整理和维护 Markdown 文档'],
|
|
946
946
|
notes: ['笔记文档', '像本地目录一样整理笔记和资料,并按需关联到知识文档'],
|
|
947
|
+
shares: ['已分享', '管理只读分享链接,也可以导入别人分享的笔记和目录'],
|
|
947
948
|
candidates: ['待审核', '确认 AI 提取结果后再写入知识文档'],
|
|
948
949
|
tokens: ['访问管理', '管理其他客户端连接中央知识库的权限'],
|
|
949
950
|
}
|
|
950
951
|
const [title, subtitle] = titles[state.view]
|
|
951
|
-
const viewIndexes = { overview: '00', notes: '01',
|
|
952
|
+
const viewIndexes = { overview: '00', notes: '01', shares: '02', entries: '03', candidates: '04', bases: '05', tokens: '06' }
|
|
952
953
|
const shell = element('div', {
|
|
953
954
|
class: 'app-shell', 'data-menu-open': String(state.menuOpen),
|
|
954
955
|
'data-view': state.view, 'data-loading': String(state.loading),
|
|
@@ -993,7 +994,6 @@ function renderShell() {
|
|
|
993
994
|
)
|
|
994
995
|
app.replaceChildren(shell)
|
|
995
996
|
applySidebarVisibility(shell, state.documentView.sidebarHidden)
|
|
996
|
-
refreshWorkspaceEffects(shell)
|
|
997
997
|
restoreScrollPosition(currentScrollState())
|
|
998
998
|
}
|
|
999
999
|
|
|
@@ -1095,7 +1095,7 @@ function applySidebarVisibility(shell, hidden) {
|
|
|
1095
1095
|
function renderSidebar() {
|
|
1096
1096
|
const pending = state.stats?.candidates.pending
|
|
1097
1097
|
const navGroups = [
|
|
1098
|
-
['笔记工作区', [['notes', '笔记文档']]],
|
|
1098
|
+
['笔记工作区', [['notes', '笔记文档'], ['shares', '已分享']]],
|
|
1099
1099
|
['知识工作区', [['entries', '知识文档'], ['candidates', '待审核'], ['bases', '知识库与挂载']]],
|
|
1100
1100
|
['连接', [['tokens', '访问管理']].filter(([id]) => id !== 'tokens' || !state.service.remote)],
|
|
1101
1101
|
].filter(([, items]) => items.length)
|
|
@@ -1126,6 +1126,7 @@ function renderCurrentView() {
|
|
|
1126
1126
|
if (state.view === 'bases') return renderKnowledgeBases()
|
|
1127
1127
|
if (state.view === 'entries') return renderEntries()
|
|
1128
1128
|
if (state.view === 'notes') return renderNotes()
|
|
1129
|
+
if (state.view === 'shares') return renderShares()
|
|
1129
1130
|
if (state.view === 'candidates') return renderCandidates()
|
|
1130
1131
|
return renderTokens()
|
|
1131
1132
|
}
|
|
@@ -1379,7 +1380,6 @@ function renderKnowledgeBaseCard(base) {
|
|
|
1379
1380
|
const hiddenTagCount = Math.max(0, base.defaultTags.length - visibleTags.length)
|
|
1380
1381
|
return element('article', {
|
|
1381
1382
|
class: `base-card${archived ? ' is-archived' : ''}`,
|
|
1382
|
-
'data-knowledge-motion-key': `knowledge-library:${base.id}`,
|
|
1383
1383
|
},
|
|
1384
1384
|
element('div', { class: 'base-card-header' },
|
|
1385
1385
|
element('div', { class: 'base-card-identity' },
|
|
@@ -1515,7 +1515,6 @@ function renderMountListRow(base, view, targetKind, targetId) {
|
|
|
1515
1515
|
return element('article', {
|
|
1516
1516
|
class: `mount-list-row${selected ? ' is-selected' : ''}`,
|
|
1517
1517
|
role: 'listitem',
|
|
1518
|
-
'data-knowledge-motion-key': `knowledge-mount:${targetKind}:${base.id}`,
|
|
1519
1518
|
},
|
|
1520
1519
|
element('label', { class: 'mount-select' }, checkbox, element('span', { class: 'visually-hidden' }, `选择 ${base.name}`)),
|
|
1521
1520
|
element('div', { class: 'mount-list-main' },
|
|
@@ -1598,7 +1597,6 @@ function renderDocumentWorkspace(workspace, options = {}) {
|
|
|
1598
1597
|
},
|
|
1599
1598
|
element('button', {
|
|
1600
1599
|
type: 'button', class: 'note-tree-base', 'aria-expanded': String(expanded),
|
|
1601
|
-
'data-knowledge-motion-key': `knowledge-base:${workspace.kind}:${base.id}`,
|
|
1602
1600
|
onClick: () => {
|
|
1603
1601
|
if (query) { view.knowledgeBaseId = base.id; return renderShell() }
|
|
1604
1602
|
void toggleDocumentBase(workspace, base.id)
|
|
@@ -1619,7 +1617,6 @@ function renderDocumentWorkspace(workspace, options = {}) {
|
|
|
1619
1617
|
'aria-busy': movingDocumentId === document.id ? 'true' : undefined,
|
|
1620
1618
|
title: canOrganize ? `${document.title} · 拖到其他知识库以移动` : document.title,
|
|
1621
1619
|
'data-document-id': document.id,
|
|
1622
|
-
'data-knowledge-motion-key': `knowledge-document:${workspace.kind}:${document.id}`,
|
|
1623
1620
|
onDragStart: event => {
|
|
1624
1621
|
knowledgeDocumentDrag = { documentId: document.id, sourceBaseId: base.id, workspaceKind: workspace.kind }
|
|
1625
1622
|
event.dataTransfer.effectAllowed = 'move'
|
|
@@ -1972,9 +1969,7 @@ function renderNotes() {
|
|
|
1972
1969
|
onChange: event => { void uploadNoteFiles([...event.target.files], state.notes.currentFolderId); event.target.value = '' },
|
|
1973
1970
|
})
|
|
1974
1971
|
const rootNodes = state.notes.children.get('root') || []
|
|
1975
|
-
const tree = state.notes.query.trim()
|
|
1976
|
-
? renderNoteSearchResults()
|
|
1977
|
-
: renderNoteTreeBranch(null)
|
|
1972
|
+
const tree = state.notes.query.trim() ? renderNoteSearchResults() : renderNoteTreeBranch(null)
|
|
1978
1973
|
const workspace = element('section', {
|
|
1979
1974
|
class: 'notes-workspace', 'data-drop-active': 'false',
|
|
1980
1975
|
onDragEnter: noteWorkspaceDragEnter, onDragOver: noteWorkspaceDragEnter,
|
|
@@ -2045,12 +2040,213 @@ function openNoteAgentGuide() {
|
|
|
2045
2040
|
element('h3', {}, '可以直接这样说'),
|
|
2046
2041
|
element('ul', {}, examples)),
|
|
2047
2042
|
element('p', { class: 'notes-agent-guide-note' }, '只说“新建 Markdown”或“创建本地目录”仍不会获得笔记权限;在当前消息中带上“笔记文档”即可。'))
|
|
2048
|
-
return
|
|
2043
|
+
return openModal({
|
|
2049
2044
|
title: '让会话整理笔记',
|
|
2050
2045
|
description: '会话可以操作笔记工作区,但每次写入都需要当前用户消息明确授权。',
|
|
2051
2046
|
body,
|
|
2052
2047
|
cancelLabel: '知道了',
|
|
2048
|
+
className: 'notes-agent-guide-dialog',
|
|
2049
|
+
})
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
async function loadNoteShares(signal) {
|
|
2053
|
+
try {
|
|
2054
|
+
state.notes.shares = await api('notes/shares', { signal })
|
|
2055
|
+
state.notes.shareError = ''
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
if (error.name === 'AbortError') throw error
|
|
2058
|
+
state.notes.shareError = friendlyError(error)
|
|
2059
|
+
}
|
|
2060
|
+
return state.notes.shares
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
async function refreshNoteShares() {
|
|
2064
|
+
await loadNoteShares()
|
|
2065
|
+
renderShell()
|
|
2066
|
+
if (!state.notes.shareError) showToast('分享列表已刷新。')
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
function renderShares() {
|
|
2070
|
+
return element('section', { class: 'shares-page', 'aria-label': '已分享笔记' },
|
|
2071
|
+
element('div', { class: 'section-heading shares-heading' },
|
|
2072
|
+
element('div', {}, element('h2', {}, '分享空间'), element('p', {}, '管理本机生成的只读链接,或将别人分享的内容复制到你的笔记工作区。')),
|
|
2073
|
+
element('div', { class: 'shares-heading-actions' },
|
|
2074
|
+
actionButton('从分享导入', openImportNoteShare, 'primary small'),
|
|
2075
|
+
actionButton('刷新', () => { void refreshNoteShares() }, 'ghost small'),
|
|
2076
|
+
),
|
|
2077
|
+
),
|
|
2078
|
+
element('section', { class: 'panel shares-panel' },
|
|
2079
|
+
element('header', { class: 'panel-header shares-panel-header' },
|
|
2080
|
+
element('div', {}, element('h3', {}, '本机已分享'), element('p', {}, `${state.notes.shares.length} 项 · 原内容更新后分享链接自动同步`)),
|
|
2081
|
+
),
|
|
2082
|
+
element('div', { class: 'shares-panel-body' },
|
|
2083
|
+
state.notes.shareError
|
|
2084
|
+
? element('div', { class: 'notes-share-error', role: 'alert' }, element('strong', {}, '分享列表加载失败'), element('p', {}, state.notes.shareError), actionButton('重试', () => { void refreshNoteShares() }, 'small'))
|
|
2085
|
+
: state.notes.shares.length
|
|
2086
|
+
? element('div', { class: 'notes-share-list' }, state.notes.shares.map(renderNoteShareRow))
|
|
2087
|
+
: element('div', { class: 'notes-folder-empty notes-share-empty' },
|
|
2088
|
+
element('span', { class: 'notes-share-empty-mark', 'aria-hidden': 'true' }),
|
|
2089
|
+
element('h3', {}, '还没有分享内容'),
|
|
2090
|
+
element('p', {}, '打开一篇笔记或目录,选择“分享”即可生成只读链接。'),
|
|
2091
|
+
element('div', {}, actionButton('打开笔记文档', () => { void navigate('notes') }, 'primary small')),
|
|
2092
|
+
),
|
|
2093
|
+
),
|
|
2094
|
+
),
|
|
2095
|
+
)
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
function renderNoteShareRow(share) {
|
|
2099
|
+
const url = noteShareUrl(share)
|
|
2100
|
+
return element('article', { class: 'notes-share-row' },
|
|
2101
|
+
element('button', { type: 'button', class: 'notes-share-main', onClick: () => { void openSharedNoteOriginal(share) } },
|
|
2102
|
+
renderNoteIcon(share.node, true),
|
|
2103
|
+
element('span', {}, element('strong', {}, share.node.name), element('small', {}, share.node.kind === 'folder' ? '共享目录及其子项' : noteKindLabel(share.node))),
|
|
2104
|
+
),
|
|
2105
|
+
element('div', { class: 'notes-share-meta' },
|
|
2106
|
+
element('span', {}, '分享于', element('time', { datetime: share.createdAt }, formatDate(share.createdAt))),
|
|
2107
|
+
element('span', {}, '原内容更新后链接自动同步'),
|
|
2108
|
+
),
|
|
2109
|
+
element('div', { class: 'notes-share-actions' },
|
|
2110
|
+
actionButton('打开原文', () => { void openSharedNoteOriginal(share) }, 'ghost small'),
|
|
2111
|
+
actionButton('复制链接', () => copyText(url, '分享链接已复制。'), 'small'),
|
|
2112
|
+
actionButton('管理', () => showNoteShareDialog(share), 'ghost small'),
|
|
2113
|
+
),
|
|
2114
|
+
)
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
function openImportNoteShare() {
|
|
2118
|
+
const urlField = formField('分享链接', 'input', '', {
|
|
2119
|
+
type: 'url', required: true, inputmode: 'url', autocomplete: 'off',
|
|
2120
|
+
placeholder: 'https://example.com/knowledge-api/v1/shared/share_…',
|
|
2121
|
+
})
|
|
2122
|
+
const destination = selectField('导入到', [{ value: '', label: '笔记文档(根目录)' }], '')
|
|
2123
|
+
destination.input.disabled = true
|
|
2124
|
+
const inspectButton = actionButton('读取分享', () => { void inspect() }, 'small')
|
|
2125
|
+
const fieldError = element('p', { class: 'share-import-field-error', role: 'alert', hidden: true })
|
|
2126
|
+
const preview = element('div', { class: 'share-import-preview', 'data-state': 'empty', 'aria-live': 'polite' },
|
|
2127
|
+
element('span', { class: 'share-import-preview-mark', 'aria-hidden': 'true' }),
|
|
2128
|
+
element('div', {}, element('strong', {}, '等待读取分享'), element('p', {}, '读取后会显示类型、文件数量与大小;导入前不会写入任何内容。')),
|
|
2129
|
+
)
|
|
2130
|
+
const destinationHint = element('span', { class: 'field-hint' }, '正在读取你的笔记目录…')
|
|
2131
|
+
destination.wrapper.append(destinationHint)
|
|
2132
|
+
const inputRow = element('div', { class: 'share-import-url-row' }, urlField.input, inspectButton)
|
|
2133
|
+
urlField.wrapper.replaceChildren(element('label', {}, '分享链接'), inputRow, fieldError)
|
|
2134
|
+
const form = element('form', { class: 'share-import-form', onSubmit: event => { event.preventDefault(); void inspect() } },
|
|
2135
|
+
urlField.wrapper,
|
|
2136
|
+
preview,
|
|
2137
|
+
destination.wrapper,
|
|
2138
|
+
element('p', { class: 'share-import-note' }, '导入会创建一份独立副本;之后对方更新或停止分享,都不会改动你已经导入的内容。'),
|
|
2139
|
+
)
|
|
2140
|
+
let inspectedUrl = ''
|
|
2141
|
+
|
|
2142
|
+
const showFieldError = message => {
|
|
2143
|
+
fieldError.textContent = message
|
|
2144
|
+
fieldError.hidden = !message
|
|
2145
|
+
urlField.input.setAttribute('aria-invalid', String(Boolean(message)))
|
|
2146
|
+
}
|
|
2147
|
+
const renderPreview = manifest => {
|
|
2148
|
+
const kind = manifest.share.kind === 'folder' ? '目录' : manifest.share.kind === 'document' ? 'Markdown 文档' : '文件'
|
|
2149
|
+
preview.dataset.state = 'ready'
|
|
2150
|
+
preview.replaceChildren(
|
|
2151
|
+
renderNoteIcon({ kind: manifest.share.kind, name: manifest.share.name, mediaType: null }, true),
|
|
2152
|
+
element('div', {},
|
|
2153
|
+
element('strong', {}, manifest.share.name),
|
|
2154
|
+
element('p', {}, `${kind} · ${manifest.share.fileCount} 个文件 · ${formatBytes(manifest.share.totalSize)}`),
|
|
2155
|
+
),
|
|
2156
|
+
manifest.truncated ? badge('超过导入上限', 'danger') : badge('可以导入', 'success'),
|
|
2157
|
+
)
|
|
2158
|
+
}
|
|
2159
|
+
const inspect = async () => {
|
|
2160
|
+
if (!urlField.input.reportValidity()) return false
|
|
2161
|
+
inspectButton.disabled = true
|
|
2162
|
+
inspectButton.setAttribute('aria-busy', 'true')
|
|
2163
|
+
inspectButton.textContent = '正在读取…'
|
|
2164
|
+
showFieldError('')
|
|
2165
|
+
preview.dataset.state = 'loading'
|
|
2166
|
+
try {
|
|
2167
|
+
const result = await api('notes/import-share/inspect', { method: 'POST', body: { url: urlField.input.value.trim() } })
|
|
2168
|
+
inspectedUrl = urlField.input.value.trim()
|
|
2169
|
+
renderPreview(result.manifest)
|
|
2170
|
+
return !result.manifest.truncated
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
inspectedUrl = ''
|
|
2173
|
+
preview.dataset.state = 'error'
|
|
2174
|
+
preview.replaceChildren(
|
|
2175
|
+
element('span', { class: 'share-import-preview-mark', 'aria-hidden': 'true' }),
|
|
2176
|
+
element('div', {}, element('strong', {}, '无法读取这个分享'), element('p', {}, '请检查链接是否完整、分享是否仍有效。')),
|
|
2177
|
+
)
|
|
2178
|
+
showFieldError(friendlyError(error))
|
|
2179
|
+
return false
|
|
2180
|
+
} finally {
|
|
2181
|
+
inspectButton.disabled = false
|
|
2182
|
+
inspectButton.removeAttribute('aria-busy')
|
|
2183
|
+
inspectButton.textContent = '读取分享'
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
urlField.input.addEventListener('input', () => {
|
|
2187
|
+
if (urlField.input.value.trim() !== inspectedUrl) {
|
|
2188
|
+
inspectedUrl = ''
|
|
2189
|
+
preview.dataset.state = 'empty'
|
|
2190
|
+
}
|
|
2191
|
+
showFieldError('')
|
|
2192
|
+
})
|
|
2193
|
+
|
|
2194
|
+
openModal({
|
|
2195
|
+
title: '从分享导入',
|
|
2196
|
+
description: '把只读分享复制到你的笔记工作区',
|
|
2197
|
+
body: form,
|
|
2198
|
+
primaryLabel: '导入笔记',
|
|
2199
|
+
className: 'share-import-dialog',
|
|
2200
|
+
onPrimary: async () => {
|
|
2201
|
+
const requestedUrl = urlField.input.value.trim()
|
|
2202
|
+
if (!requestedUrl || inspectedUrl !== requestedUrl) {
|
|
2203
|
+
showFieldError('请先读取并确认分享内容。')
|
|
2204
|
+
urlField.input.focus()
|
|
2205
|
+
return false
|
|
2206
|
+
}
|
|
2207
|
+
showFieldError('')
|
|
2208
|
+
try {
|
|
2209
|
+
const result = await api('notes/import-share', {
|
|
2210
|
+
method: 'POST', body: { url: requestedUrl, parentId: destination.input.value || null },
|
|
2211
|
+
})
|
|
2212
|
+
state.notes.children.clear()
|
|
2213
|
+
state.notes.loadedFolders.clear()
|
|
2214
|
+
showToast(`已导入“${result.root.name}”,共 ${result.importedNodes} 项。`)
|
|
2215
|
+
return true
|
|
2216
|
+
} catch (error) {
|
|
2217
|
+
showFieldError(friendlyError(error))
|
|
2218
|
+
return false
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2053
2221
|
})
|
|
2222
|
+
|
|
2223
|
+
void loadShareImportDestinations().then(options => {
|
|
2224
|
+
destination.input.replaceChildren(...options.map(option => element('option', { value: option.value }, option.label)))
|
|
2225
|
+
destination.input.disabled = false
|
|
2226
|
+
destinationHint.textContent = options.length > 1 ? `可选择 ${options.length - 1} 个已有目录` : '当前会导入到笔记根目录'
|
|
2227
|
+
}).catch(error => {
|
|
2228
|
+
destination.input.disabled = false
|
|
2229
|
+
destinationHint.textContent = `目录读取失败,将导入到根目录:${friendlyError(error)}`
|
|
2230
|
+
})
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
async function loadShareImportDestinations() {
|
|
2234
|
+
const options = [{ value: '', label: '笔记文档(根目录)' }]
|
|
2235
|
+
const queue = [{ id: null, path: '' }]
|
|
2236
|
+
while (queue.length && options.length <= 500) {
|
|
2237
|
+
const batch = queue.splice(0, 4)
|
|
2238
|
+
const groups = await Promise.all(batch.map(async parent => ({ parent, nodes: await loadNoteChildren(parent.id) })))
|
|
2239
|
+
for (const { parent, nodes } of groups) {
|
|
2240
|
+
for (const node of nodes) {
|
|
2241
|
+
if (node.kind !== 'folder') continue
|
|
2242
|
+
const path = parent.path ? `${parent.path}/${node.name}` : node.name
|
|
2243
|
+
options.push({ value: node.id, label: path })
|
|
2244
|
+
queue.push({ id: node.id, path })
|
|
2245
|
+
if (options.length > 500) break
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
}
|
|
2249
|
+
return options
|
|
2054
2250
|
}
|
|
2055
2251
|
|
|
2056
2252
|
function renderNoteTransfer() {
|
|
@@ -2091,7 +2287,6 @@ function renderNoteTreeBranch(parentId, depth = 0) {
|
|
|
2091
2287
|
const row = element('div', {
|
|
2092
2288
|
class: 'notes-tree-item', 'data-kind': node.kind, 'data-selected': String(selected),
|
|
2093
2289
|
'data-note-id': node.id,
|
|
2094
|
-
'data-knowledge-motion-key': `note:${node.id}`,
|
|
2095
2290
|
draggable: 'true',
|
|
2096
2291
|
onDragStart: event => {
|
|
2097
2292
|
event.dataTransfer.effectAllowed = 'move'
|
|
@@ -2131,7 +2326,7 @@ function renderNoteTreeBranch(parentId, depth = 0) {
|
|
|
2131
2326
|
function renderNoteSearchResults() {
|
|
2132
2327
|
if (!state.notes.searchResults.length) return element('div', { class: 'notes-tree-empty' }, '没有匹配的笔记文档。')
|
|
2133
2328
|
return element('div', { class: 'notes-search-results' }, state.notes.searchResults.map(node => element('button', {
|
|
2134
|
-
type: 'button', class: 'notes-search-row',
|
|
2329
|
+
type: 'button', class: 'notes-search-row',
|
|
2135
2330
|
onClick: () => { void selectNoteNode(node) },
|
|
2136
2331
|
}, renderNoteIcon(node), element('span', {}, element('strong', {}, node.name), element('small', {}, noteKindLabel(node))))))
|
|
2137
2332
|
}
|
|
@@ -2168,7 +2363,10 @@ function renderNoteFolderContent(folder) {
|
|
|
2168
2363
|
},
|
|
2169
2364
|
renderNoteContentHeader(folder),
|
|
2170
2365
|
children.length
|
|
2171
|
-
? element('div', { class: 'notes-file-
|
|
2366
|
+
? element('div', { class: 'notes-file-table' },
|
|
2367
|
+
element('div', { class: 'notes-file-columns', 'aria-hidden': 'true' },
|
|
2368
|
+
element('span', {}, '名称'), element('span', {}, '更新时间'), element('span', {}, '大小'), element('span', {}, '操作')),
|
|
2369
|
+
element('div', { class: 'notes-file-list', role: 'list' }, children.map(node => element('div', {
|
|
2172
2370
|
class: 'notes-file-row', role: 'listitem', draggable: 'true',
|
|
2173
2371
|
onDragStart: event => { event.dataTransfer.effectAllowed = 'move'; event.dataTransfer.setData('application/x-dsh-note-id', node.id) },
|
|
2174
2372
|
onDblClick: () => { void selectNoteNode(node, { toggleFolder: node.kind === 'folder' }) },
|
|
@@ -2181,11 +2379,12 @@ function renderNoteFolderContent(folder) {
|
|
|
2181
2379
|
element('span', { class: 'notes-file-size', 'data-label': node.kind === 'folder' ? '类型' : '大小' }, node.kind === 'folder' ? '目录' : formatBytes(node.size)),
|
|
2182
2380
|
element('div', { class: 'notes-file-actions' },
|
|
2183
2381
|
node.kind !== 'folder' ? noteDownloadButton(node, 'ghost tiny') : null,
|
|
2382
|
+
actionButton(noteShareForNode(node.id) ? '分享中' : '分享', () => { void openNoteShare(node) }, 'ghost tiny'),
|
|
2184
2383
|
actionButton('重命名', () => openRenameNoteNode(node), 'ghost tiny'),
|
|
2185
2384
|
actionButton('复制', () => { void copyNoteNode(node) }, 'ghost tiny'),
|
|
2186
2385
|
actionButton('删除', () => { void confirmDeleteNoteNode(node) }, 'ghost tiny danger-text'),
|
|
2187
2386
|
),
|
|
2188
|
-
|
|
2387
|
+
))))
|
|
2189
2388
|
: element('div', { class: 'notes-folder-empty' },
|
|
2190
2389
|
element('span', { class: 'notes-empty-folder-mark', 'aria-hidden': 'true' }),
|
|
2191
2390
|
element('h3', {}, '这个目录还是空的'),
|
|
@@ -2205,6 +2404,8 @@ function renderNoteContentHeader(folder) {
|
|
|
2205
2404
|
element('div', { class: 'notes-content-title' },
|
|
2206
2405
|
element('div', {}, element('h2', {}, folder?.name || '全部笔记'), element('p', {}, folder ? `${(state.notes.children.get(folder.id) || []).length} 个项目` : '你的独立笔记与资料目录')),
|
|
2207
2406
|
element('div', { class: 'notes-content-actions' },
|
|
2407
|
+
folder ? actionButton(noteShareForNode(folder.id) ? '分享中' : '分享目录', () => { void openNoteShare(folder) }, 'ghost small') : null,
|
|
2408
|
+
actionButton('已分享', () => { void navigate('shares') }, 'ghost small'),
|
|
2208
2409
|
actionButton('新建文档', () => openCreateNoteDocument(folder?.id || null), 'primary small'),
|
|
2209
2410
|
actionButton('新建目录', () => openCreateNoteFolder(folder?.id || null), 'small'),
|
|
2210
2411
|
),
|
|
@@ -2454,6 +2655,7 @@ function renderNoteFileToolbar(node, options = {}) {
|
|
|
2454
2655
|
], () => markdownEditorHandle?.toggleOutline(), 'ghost small notes-outline-action', { 'data-note-outline': '', 'aria-label': '打开标题大纲', 'aria-controls': 'dsh-note-outline', 'aria-expanded': 'false', 'aria-pressed': 'false' }) : null,
|
|
2455
2656
|
options.editable ? actionButton('历史', () => { void openNoteHistory(node) }, 'ghost small', { 'data-note-history': '', 'data-note-mobile-overflow': '' }) : null,
|
|
2456
2657
|
noteDownloadButton(node, 'ghost small', '下载', { 'data-note-mobile-overflow': '' }),
|
|
2658
|
+
actionButton(noteShareForNode(node.id) ? '分享中' : '分享', () => { void openNoteShare(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2457
2659
|
actionButton('复制引用', () => { void copyNoteReference(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2458
2660
|
actionButton('重命名', () => openRenameNoteNode(node), 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2459
2661
|
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', 'data-note-mobile-overflow': '', disabled: !state.notes.dirty }) : null,
|
|
@@ -2500,8 +2702,11 @@ function renderNoteFileOverflowMenu(node, options) {
|
|
|
2500
2702
|
if (options.enhanced || options.editable) menuItems.push(noteMenuDivider())
|
|
2501
2703
|
menuItems.push(
|
|
2502
2704
|
noteMenuAction('下载', 'download', closeThen(event => { void downloadNoteFile(node, event.currentTarget) })),
|
|
2705
|
+
noteMenuAction(noteShareForNode(node.id) ? '管理分享' : '创建分享', 'link', closeThen(() => { void openNoteShare(node) })),
|
|
2503
2706
|
noteMenuAction('复制引用', 'link', closeThen(() => { void copyNoteReference(node) })),
|
|
2504
2707
|
noteMenuAction('重命名', 'rename', closeThen(() => openRenameNoteNode(node))),
|
|
2708
|
+
noteMenuDivider(),
|
|
2709
|
+
noteMenuAction('查看已分享', 'outline', closeThen(() => { void navigate('shares') })),
|
|
2505
2710
|
)
|
|
2506
2711
|
const menu = element('div', { class: 'notes-document-more-menu', role: 'menu', 'aria-label': `${node.name} 的更多操作` }, menuItems)
|
|
2507
2712
|
summary.setAttribute('aria-expanded', 'false')
|
|
@@ -2524,7 +2729,7 @@ function renderNoteFileStatusbar(node) {
|
|
|
2524
2729
|
return element('footer', { class: 'notes-document-statusbar', 'aria-label': '文档信息' },
|
|
2525
2730
|
element('div', { class: 'notes-file-info' },
|
|
2526
2731
|
noteInfoItem('编号', shortNoteId(node.id), node.id, 'id'),
|
|
2527
|
-
noteInfoItem('
|
|
2732
|
+
noteInfoItem('格式', node.kind === 'document' ? 'Markdown' : node.mediaType || '未知'),
|
|
2528
2733
|
noteInfoItem('大小', formatBytes(node.size), '', 'size'),
|
|
2529
2734
|
noteInfoItem('更新', formatDate(node.updatedAt), '', 'updated'),
|
|
2530
2735
|
),
|
|
@@ -3187,6 +3392,76 @@ async function copyNoteReference(node) {
|
|
|
3187
3392
|
catch { showToast('复制失败,请手动复制文档编号。', 'error') }
|
|
3188
3393
|
}
|
|
3189
3394
|
|
|
3395
|
+
function noteShareForNode(noteId) {
|
|
3396
|
+
return state.notes.shares.find(share => share.noteId === noteId) || null
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function noteShareUrl(share) {
|
|
3400
|
+
const apiBase = state.service.publicApiPrefix || API_BASE
|
|
3401
|
+
return new URL(`${apiBase.replace(/\/$/, '')}/shared/${encodeURIComponent(share.token)}`, location.href).href
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
async function openNoteShare(node) {
|
|
3405
|
+
if (state.notes.selectedNode?.id === node.id && state.notes.dirty && !await saveNoteDocument()) return
|
|
3406
|
+
try {
|
|
3407
|
+
let share = noteShareForNode(node.id)
|
|
3408
|
+
if (!share) {
|
|
3409
|
+
share = await api(`notes/${encodeURIComponent(node.id)}/share`, { method: 'POST' })
|
|
3410
|
+
state.notes.shares = [share, ...state.notes.shares.filter(item => item.noteId !== node.id)]
|
|
3411
|
+
}
|
|
3412
|
+
showNoteShareDialog(share)
|
|
3413
|
+
} catch (error) {
|
|
3414
|
+
showToast(`创建分享失败:${friendlyError(error)}`, 'error')
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
function showNoteShareDialog(share) {
|
|
3419
|
+
const url = noteShareUrl(share)
|
|
3420
|
+
const input = Object.assign(element('input', {
|
|
3421
|
+
class: 'input notes-share-link-input', readonly: true, value: url,
|
|
3422
|
+
'aria-label': `${share.node.name} 的分享链接`, onFocus: event => event.currentTarget.select(),
|
|
3423
|
+
}), { value: url })
|
|
3424
|
+
let modal
|
|
3425
|
+
const body = element('div', { class: 'notes-share-dialog-body' },
|
|
3426
|
+
element('div', { class: 'notes-share-dialog-kind' }, renderNoteIcon(share.node, true), element('span', {}, element('strong', {}, share.node.name), element('small', {}, share.node.kind === 'folder' ? '目录与当前子项均可只读访问' : '只读文档链接'))),
|
|
3427
|
+
element('label', { class: 'notes-share-link-label' }, element('span', {}, '分享链接'), element('div', { class: 'notes-share-link-row' }, input, actionButton('复制', () => copyText(url, '分享链接已复制。'), 'primary small'))),
|
|
3428
|
+
element('p', { class: 'notes-share-dialog-note' }, '链接中的内容会随原笔记更新。任何获得链接的人都可以访问;停止分享后链接立即失效。'),
|
|
3429
|
+
element('div', { class: 'notes-share-dialog-danger' },
|
|
3430
|
+
element('span', {}, element('strong', {}, '停止分享'), element('small', {}, '不会删除原文档或目录。')),
|
|
3431
|
+
actionButton('停止分享', () => confirmRemoveNoteShare(share, () => modal?.close(true)), 'danger small'),
|
|
3432
|
+
),
|
|
3433
|
+
)
|
|
3434
|
+
modal = openModal({
|
|
3435
|
+
title: '分享笔记',
|
|
3436
|
+
description: '生成独立的只读访问链接',
|
|
3437
|
+
body,
|
|
3438
|
+
cancelLabel: '完成',
|
|
3439
|
+
className: 'notes-share-dialog',
|
|
3440
|
+
})
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
function confirmRemoveNoteShare(share, afterRemove) {
|
|
3444
|
+
openConfirm({
|
|
3445
|
+
title: `停止分享“${share.node.name}”?`,
|
|
3446
|
+
message: '现有分享链接会立即失效,原笔记内容不会被删除。',
|
|
3447
|
+
confirmLabel: '停止分享',
|
|
3448
|
+
danger: true,
|
|
3449
|
+
onConfirm: async () => {
|
|
3450
|
+
await api(`notes/${encodeURIComponent(share.noteId)}/share`, { method: 'DELETE' })
|
|
3451
|
+
state.notes.shares = state.notes.shares.filter(item => item.noteId !== share.noteId)
|
|
3452
|
+
afterRemove?.()
|
|
3453
|
+
renderShell()
|
|
3454
|
+
showToast('已停止分享。')
|
|
3455
|
+
},
|
|
3456
|
+
})
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
async function openSharedNoteOriginal(share) {
|
|
3460
|
+
if (state.notes.dirty && !await saveNoteDocument()) return
|
|
3461
|
+
state.view = 'notes'
|
|
3462
|
+
await selectNoteNode(share.node)
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3190
3465
|
async function confirmDeleteNoteNode(node) {
|
|
3191
3466
|
try {
|
|
3192
3467
|
const references = await api(`notes/${encodeURIComponent(node.id)}/references`)
|
|
@@ -3204,6 +3479,7 @@ async function confirmDeleteNoteNode(node) {
|
|
|
3204
3479
|
confirmLabel: '永久删除', danger: true,
|
|
3205
3480
|
onConfirm: async () => {
|
|
3206
3481
|
await api(`notes/${encodeURIComponent(node.id)}`, { method: 'DELETE' })
|
|
3482
|
+
await loadNoteShares()
|
|
3207
3483
|
await loadNoteChildren(node.parentId, true)
|
|
3208
3484
|
if (state.notes.selectedId === node.id || state.notes.breadcrumbs.some(parent => parent.id === node.id)) clearNoteSelection()
|
|
3209
3485
|
renderShell(); showToast('已删除。')
|
|
@@ -4148,11 +4424,11 @@ function openSheet(options) {
|
|
|
4148
4424
|
return openModal({ ...options, presentation: 'sheet' })
|
|
4149
4425
|
}
|
|
4150
4426
|
|
|
4151
|
-
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', onClose }) {
|
|
4427
|
+
function openModal({ title, description = '', body, primaryLabel, primaryVariant = 'primary', onPrimary, cancelLabel = '取消', presentation = 'modal', className = '', onClose }) {
|
|
4152
4428
|
const previouslyFocused = document.activeElement
|
|
4153
4429
|
const isSheet = presentation === 'sheet'
|
|
4154
4430
|
const backdrop = element('div', { class: `dialog-backdrop${isSheet ? ' sheet-backdrop' : ''}` })
|
|
4155
|
-
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
4431
|
+
const dialog = element('section', { class: `dialog${isSheet ? ' sheet' : ''} ${primaryLabel ? '' : 'narrow'} ${className}`.trim(), role: 'dialog', 'aria-modal': 'true', 'aria-labelledby': 'dialog-title' })
|
|
4156
4432
|
let busy = false
|
|
4157
4433
|
let formDirty = false
|
|
4158
4434
|
let closed = false
|
|
@@ -4165,7 +4441,6 @@ function openModal({ title, description = '', body, primaryLabel, primaryVariant
|
|
|
4165
4441
|
document.removeEventListener('keydown', onKeyDown)
|
|
4166
4442
|
closed = true
|
|
4167
4443
|
backdrop.remove()
|
|
4168
|
-
refreshWorkspaceEffects()
|
|
4169
4444
|
onClose?.()
|
|
4170
4445
|
if (previouslyFocused instanceof HTMLElement) previouslyFocused.focus()
|
|
4171
4446
|
}
|
|
@@ -4207,7 +4482,6 @@ function openModal({ title, description = '', body, primaryLabel, primaryVariant
|
|
|
4207
4482
|
body.addEventListener('change', () => { formDirty = true })
|
|
4208
4483
|
}
|
|
4209
4484
|
document.body.append(backdrop)
|
|
4210
|
-
refreshWorkspaceEffects(backdrop)
|
|
4211
4485
|
document.addEventListener('keydown', onKeyDown)
|
|
4212
4486
|
window.setTimeout(() => (dialog.querySelector('input, textarea, select, button') || dialog).focus(), 0)
|
|
4213
4487
|
return { close, dialog }
|
package/web/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="zh-CN">
|
|
2
|
+
<html lang="zh-CN" data-dsh-embed-mode="__DSH_KNOWLEDGE_EMBED_MODE__">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
<script src="__DSH_KNOWLEDGE_WEB_PATH__/markdown-preview.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
15
15
|
<script src="__DSH_KNOWLEDGE_WEB_PATH__/change-review.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
16
16
|
<script src="__DSH_KNOWLEDGE_WEB_PATH__/note-history.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
17
|
-
<script src="__DSH_KNOWLEDGE_WEB_PATH__/workspace-effects.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
18
17
|
<script type="module" src="__DSH_KNOWLEDGE_WEB_PATH__/app.js?v=__DSH_KNOWLEDGE_ASSET_VERSION__"></script>
|
|
19
18
|
</head>
|
|
20
19
|
<body>
|