@lemoncat7/dsh-knowledge 2.2.9 → 2.2.13-next.12
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 +3 -3
- package/docs/architecture.md +6 -2
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +42 -0
- package/lib/api.js.map +1 -1
- package/lib/client.js +22 -22
- package/lib/client.js.map +2 -2
- package/lib/domain.d.ts +15 -0
- package/lib/domain.d.ts.map +1 -1
- package/lib/domain.js.map +1 -1
- package/lib/local-provider.d.ts +11 -2
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +71 -0
- package/lib/local-provider.js.map +1 -1
- package/lib/management-proxy.js +1 -1
- package/lib/management-proxy.js.map +1 -1
- package/lib/notes/domain.d.ts +10 -0
- package/lib/notes/domain.d.ts.map +1 -1
- package/lib/notes/domain.js.map +1 -1
- package/lib/notes/store.d.ts +15 -1
- package/lib/notes/store.d.ts.map +1 -1
- package/lib/notes/store.js +277 -28
- package/lib/notes/store.js.map +1 -1
- package/lib/provider.d.ts +10 -2
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +10 -2
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +27 -0
- package/lib/remote-provider.js.map +1 -1
- package/lib/theme-bridge.js +7 -7
- package/lib/theme-bridge.js.map +1 -1
- package/lib/tool-authorization.js +11 -16
- package/lib/tool-authorization.js.map +1 -1
- package/lib/web-note-editor-chrome.d.ts +17 -0
- package/lib/web-note-editor-chrome.d.ts.map +1 -0
- package/lib/web-note-editor-chrome.js +38 -0
- package/lib/web-note-editor-chrome.js.map +1 -0
- package/lib/web-note-editor-find.d.ts +17 -0
- package/lib/web-note-editor-find.d.ts.map +1 -0
- package/lib/web-note-editor-find.js +225 -0
- package/lib/web-note-editor-find.js.map +1 -0
- package/lib/web-note-editor-outline.d.ts +15 -0
- package/lib/web-note-editor-outline.d.ts.map +1 -0
- package/lib/web-note-editor-outline.js +169 -0
- package/lib/web-note-editor-outline.js.map +1 -0
- package/lib/web-note-editor-search.d.ts +30 -0
- package/lib/web-note-editor-search.d.ts.map +1 -0
- package/lib/web-note-editor-search.js +143 -0
- package/lib/web-note-editor-search.js.map +1 -0
- package/lib/web-note-editor-selection.d.ts +15 -0
- package/lib/web-note-editor-selection.d.ts.map +1 -0
- package/lib/web-note-editor-selection.js +255 -0
- package/lib/web-note-editor-selection.js.map +1 -0
- package/lib/web-note-editor.d.ts +7 -0
- package/lib/web-note-editor.d.ts.map +1 -1
- package/lib/web-note-editor.js +36 -6
- package/lib/web-note-editor.js.map +1 -1
- package/lib/web-note-history.d.ts +28 -0
- package/lib/web-note-history.d.ts.map +1 -0
- package/lib/web-note-history.js +163 -0
- package/lib/web-note-history.js.map +1 -0
- package/lib/web-workspace-effects.d.ts.map +1 -1
- package/lib/web-workspace-effects.js +1 -2
- package/lib/web-workspace-effects.js.map +1 -1
- package/lib/web.d.ts.map +1 -1
- package/lib/web.js +4 -0
- package/lib/web.js.map +1 -1
- package/package.json +1 -1
- package/web/api-client.js +94 -0
- package/web/app.js +344 -235
- package/web/host-theme.js +63 -0
- package/web/index.html +1 -0
- package/web/note-editor.js +70 -70
- package/web/note-history.js +1 -0
- package/web/styles.css +374 -154
- package/web/ui-primitives.js +77 -0
- package/web/workspace-effects.js +1 -1
package/web/app.js
CHANGED
|
@@ -2,16 +2,15 @@ const API_BASE = document.querySelector('meta[name="dsh-knowledge-api"]')?.conte
|
|
|
2
2
|
const AUTH_MODE = document.querySelector('meta[name="dsh-knowledge-auth-mode"]')?.content || 'bearer'
|
|
3
3
|
const WEB_PATH = document.querySelector('meta[name="dsh-knowledge-web"]')?.content || '/knowledge'
|
|
4
4
|
const ASSET_VERSION = document.querySelector('meta[name="dsh-knowledge-asset-version"]')?.content || ''
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'
|
|
10
|
-
'--text', '--text-secondary', '--text-tertiary', '--border', '--border-strong',
|
|
11
|
-
'--accent', '--accent-hover', '--accent-soft', '--on-accent',
|
|
12
|
-
'--success', '--success-soft', '--warning', '--warning-soft', '--danger', '--danger-soft',
|
|
5
|
+
const moduleUrl = name => `./${name}.js${ASSET_VERSION ? `?v=${encodeURIComponent(ASSET_VERSION)}` : ''}`
|
|
6
|
+
const [apiModule, themeModule, uiModule] = await Promise.all([
|
|
7
|
+
import(moduleUrl('api-client')),
|
|
8
|
+
import(moduleUrl('host-theme')),
|
|
9
|
+
import(moduleUrl('ui-primitives')),
|
|
13
10
|
])
|
|
14
|
-
const
|
|
11
|
+
const { createApiClient } = apiModule
|
|
12
|
+
const { installHostThemeBridge } = themeModule
|
|
13
|
+
const { actionButton, badge, createToastPresenter, element, interfaceIcon, paneToggleButton } = uiModule
|
|
15
14
|
const TOKEN_KEY = 'dsh-knowledge.session-token'
|
|
16
15
|
const TYPES = ['preference', 'fact', 'decision', 'procedure', 'lesson']
|
|
17
16
|
const TYPE_LABELS = { preference: '偏好', fact: '事实', decision: '决策', procedure: '流程', lesson: '经验' }
|
|
@@ -33,6 +32,7 @@ const mountContext = {
|
|
|
33
32
|
}
|
|
34
33
|
const app = document.querySelector('#app')
|
|
35
34
|
const toastRegion = document.querySelector('#toast-region')
|
|
35
|
+
const showToast = createToastPresenter(toastRegion)
|
|
36
36
|
const savedDocumentLayout = readDocumentLayout()
|
|
37
37
|
|
|
38
38
|
function createDocumentViewState(overrides = {}) {
|
|
@@ -81,6 +81,7 @@ const state = {
|
|
|
81
81
|
candidates: [],
|
|
82
82
|
candidateTargets: new Map(),
|
|
83
83
|
candidateStatus: 'pending',
|
|
84
|
+
candidateBatchRunning: false,
|
|
84
85
|
settings: { writebackPolicy: 'conservative', updatedAt: '' },
|
|
85
86
|
modelCatalog: null,
|
|
86
87
|
settingsSaving: false,
|
|
@@ -120,62 +121,10 @@ let noteSelectionRequest = 0
|
|
|
120
121
|
let knowledgeDocumentDrag = null
|
|
121
122
|
let movingDocumentId = ''
|
|
122
123
|
|
|
123
|
-
function installHostThemeBridge() {
|
|
124
|
-
if (window.parent === window) return Promise.resolve()
|
|
125
|
-
const parentOrigin = referrerOrigin()
|
|
126
|
-
return new Promise(resolve => {
|
|
127
|
-
let initialThemeSettled = false
|
|
128
|
-
const settleInitialTheme = () => {
|
|
129
|
-
if (initialThemeSettled) return
|
|
130
|
-
initialThemeSettled = true
|
|
131
|
-
window.clearTimeout(fallback)
|
|
132
|
-
resolve()
|
|
133
|
-
}
|
|
134
|
-
const fallback = window.setTimeout(settleInitialTheme, 160)
|
|
135
|
-
window.addEventListener('message', event => {
|
|
136
|
-
if (event.source !== window.parent) return
|
|
137
|
-
if (parentOrigin && event.origin !== parentOrigin) return
|
|
138
|
-
const message = event.data
|
|
139
|
-
if (!message || message.type !== HOST_THEME_MESSAGE || message.version !== HOST_THEME_PROTOCOL_VERSION) return
|
|
140
|
-
if (message.colorScheme !== 'light' && message.colorScheme !== 'dark') return
|
|
141
|
-
if (!message.tokens || typeof message.tokens !== 'object' || Array.isArray(message.tokens)) return
|
|
142
|
-
|
|
143
|
-
const root = document.documentElement
|
|
144
|
-
for (const name of [...HOST_THEME_COLOR_TOKENS, ...HOST_THEME_STYLE_TOKENS]) root.style.removeProperty(name)
|
|
145
|
-
for (const [name, value] of Object.entries(message.tokens)) {
|
|
146
|
-
if (typeof value !== 'string' || value.length === 0 || value.length > 512) continue
|
|
147
|
-
if (HOST_THEME_COLOR_TOKENS.has(name) && CSS.supports('color', value)) root.style.setProperty(name, value)
|
|
148
|
-
else if (HOST_THEME_STYLE_TOKENS.has(name) && CSS.supports('box-shadow', value)) root.style.setProperty(name, value)
|
|
149
|
-
}
|
|
150
|
-
root.dataset.dshHostTheme = 'true'
|
|
151
|
-
root.dataset.colorScheme = message.colorScheme
|
|
152
|
-
root.style.colorScheme = message.colorScheme
|
|
153
|
-
document.querySelector('meta[name="color-scheme"]')?.setAttribute('content', message.colorScheme)
|
|
154
|
-
const background = root.style.getPropertyValue('--bg')
|
|
155
|
-
if (background) document.querySelector('meta[name="theme-color"]')?.setAttribute('content', background)
|
|
156
|
-
settleInitialTheme()
|
|
157
|
-
})
|
|
158
|
-
window.parent.postMessage({
|
|
159
|
-
type: HOST_THEME_READY_MESSAGE,
|
|
160
|
-
version: HOST_THEME_PROTOCOL_VERSION,
|
|
161
|
-
}, parentOrigin || '*')
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function referrerOrigin() {
|
|
166
|
-
if (!document.referrer) return ''
|
|
167
|
-
try {
|
|
168
|
-
const origin = new URL(document.referrer).origin
|
|
169
|
-
return origin === 'null' ? '' : origin
|
|
170
|
-
} catch {
|
|
171
|
-
return ''
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
124
|
function readDocumentLayout() {
|
|
176
125
|
const fallback = {
|
|
177
126
|
sidebarHidden: false,
|
|
178
|
-
sidebarWidth:
|
|
127
|
+
sidebarWidth: 214,
|
|
179
128
|
}
|
|
180
129
|
try {
|
|
181
130
|
const value = JSON.parse(localStorage.getItem(DOCUMENT_LAYOUT_KEY) || '{}')
|
|
@@ -235,153 +184,15 @@ function restoreScrollPosition(view) {
|
|
|
235
184
|
})
|
|
236
185
|
}
|
|
237
186
|
|
|
238
|
-
function element(tag, attributes = {}, ...children) {
|
|
239
|
-
const node = document.createElement(tag)
|
|
240
|
-
for (const [key, value] of Object.entries(attributes)) {
|
|
241
|
-
if (value === undefined || value === null || value === false) continue
|
|
242
|
-
if (key === 'class') node.className = value
|
|
243
|
-
else if (key === 'text') node.textContent = value
|
|
244
|
-
else if (key.startsWith('on') && typeof value === 'function') node.addEventListener(key.slice(2).toLowerCase(), value)
|
|
245
|
-
else if (key === 'checked' || key === 'selected' || key === 'disabled') node[key] = Boolean(value)
|
|
246
|
-
else node.setAttribute(key, String(value))
|
|
247
|
-
}
|
|
248
|
-
for (const child of children.flat(Infinity)) {
|
|
249
|
-
if (child === undefined || child === null || child === false) continue
|
|
250
|
-
node.append(child instanceof Node ? child : document.createTextNode(String(child)))
|
|
251
|
-
}
|
|
252
|
-
return node
|
|
253
|
-
}
|
|
254
|
-
|
|
255
187
|
function refreshWorkspaceEffects(root = document) {
|
|
256
188
|
window.DshKnowledgeEffects?.refresh(root)
|
|
257
189
|
}
|
|
258
190
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const action = `${visible ? '隐藏' : '显示'}${label}`
|
|
265
|
-
return element('button', {
|
|
266
|
-
type: 'button', class: 'pane-toggle-button', 'data-pane': pane,
|
|
267
|
-
'aria-label': action, 'aria-pressed': String(visible), title: action, onClick,
|
|
268
|
-
}, element('span', { class: `pane-icon pane-icon-${pane}`, 'aria-hidden': 'true' }))
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
function interfaceIcon(name, className = 'interface-icon') {
|
|
272
|
-
const paths = {
|
|
273
|
-
search: 'M10.8 4.5a6.3 6.3 0 1 0 0 12.6 6.3 6.3 0 0 0 0-12.6Zm4.6 11 4.1 4',
|
|
274
|
-
}
|
|
275
|
-
return element('svg', {
|
|
276
|
-
class: className, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor',
|
|
277
|
-
'stroke-width': '1.8', 'stroke-linecap': 'round', 'stroke-linejoin': 'round', 'aria-hidden': 'true',
|
|
278
|
-
}, element('path', { d: paths[name] }))
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function badge(label, variant = '') {
|
|
282
|
-
return element('span', { class: `badge ${variant}`.trim() }, label)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function showToast(message, kind = '') {
|
|
286
|
-
const toast = element('div', { class: `toast ${kind}`.trim(), role: kind === 'error' ? 'alert' : 'status' },
|
|
287
|
-
element('span', {}, message),
|
|
288
|
-
kind === 'error' ? actionButton('关闭', () => toast.remove(), 'ghost small toast-close', { 'aria-label': '关闭错误提示' }) : null,
|
|
289
|
-
)
|
|
290
|
-
toastRegion.append(toast)
|
|
291
|
-
if (kind !== 'error') window.setTimeout(() => toast.remove(), 4200)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function api(path, options = {}) {
|
|
295
|
-
const headers = { accept: 'application/json', ...(options.body === undefined ? {} : { 'content-type': 'application/json' }) }
|
|
296
|
-
if (AUTH_MODE === 'same-origin') headers['x-dsh-knowledge-client'] = 'management-web'
|
|
297
|
-
if (state.token) headers.authorization = `Bearer ${state.token}`
|
|
298
|
-
const response = await fetch(`${API_BASE}/${path.replace(/^\/+/, '')}`, {
|
|
299
|
-
method: options.method || 'GET',
|
|
300
|
-
headers,
|
|
301
|
-
body: options.body === undefined ? undefined : JSON.stringify(options.body),
|
|
302
|
-
signal: options.signal,
|
|
303
|
-
})
|
|
304
|
-
const text = await response.text()
|
|
305
|
-
let payload
|
|
306
|
-
if (text) {
|
|
307
|
-
try { payload = JSON.parse(text) } catch { throw new Error('服务返回了无法识别的数据') }
|
|
308
|
-
}
|
|
309
|
-
if (!response.ok) {
|
|
310
|
-
const error = new Error(payload?.error || `请求失败(HTTP ${response.status})`)
|
|
311
|
-
error.status = response.status
|
|
312
|
-
throw error
|
|
313
|
-
}
|
|
314
|
-
return payload
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function binaryRequest(path, options = {}) {
|
|
318
|
-
const headers = { accept: options.accept || 'application/json' }
|
|
319
|
-
if (AUTH_MODE === 'same-origin') headers['x-dsh-knowledge-client'] = 'management-web'
|
|
320
|
-
if (state.token) headers.authorization = `Bearer ${state.token}`
|
|
321
|
-
if (options.contentType) headers['content-type'] = options.contentType
|
|
322
|
-
const response = await fetch(`${API_BASE}/${path.replace(/^\/+/, '')}`, {
|
|
323
|
-
method: options.method || 'GET',
|
|
324
|
-
headers,
|
|
325
|
-
body: options.body,
|
|
326
|
-
signal: options.signal,
|
|
327
|
-
})
|
|
328
|
-
if (options.responseType === 'blob') {
|
|
329
|
-
if (!response.ok) throw await responseError(response)
|
|
330
|
-
return response.blob()
|
|
331
|
-
}
|
|
332
|
-
const text = await response.text()
|
|
333
|
-
let payload
|
|
334
|
-
if (text) {
|
|
335
|
-
try { payload = JSON.parse(text) } catch { throw new Error('服务返回了无法识别的数据') }
|
|
336
|
-
}
|
|
337
|
-
if (!response.ok) {
|
|
338
|
-
const error = new Error(payload?.error || `请求失败(HTTP ${response.status})`)
|
|
339
|
-
error.status = response.status
|
|
340
|
-
throw error
|
|
341
|
-
}
|
|
342
|
-
return payload
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
function binaryUploadRequest(path, body, options = {}) {
|
|
346
|
-
return new Promise((resolve, reject) => {
|
|
347
|
-
const request = new XMLHttpRequest()
|
|
348
|
-
request.open(options.method || 'POST', `${API_BASE}/${path.replace(/^\/+/, '')}`)
|
|
349
|
-
request.responseType = 'text'
|
|
350
|
-
request.setRequestHeader('accept', 'application/json')
|
|
351
|
-
if (AUTH_MODE === 'same-origin') request.setRequestHeader('x-dsh-knowledge-client', 'management-web')
|
|
352
|
-
if (state.token) request.setRequestHeader('authorization', `Bearer ${state.token}`)
|
|
353
|
-
if (options.contentType) request.setRequestHeader('content-type', options.contentType)
|
|
354
|
-
request.upload.addEventListener('progress', event => {
|
|
355
|
-
if (event.lengthComputable) options.onProgress?.(event.loaded, event.total)
|
|
356
|
-
})
|
|
357
|
-
request.addEventListener('load', () => {
|
|
358
|
-
let payload
|
|
359
|
-
if (request.responseText) {
|
|
360
|
-
try { payload = JSON.parse(request.responseText) }
|
|
361
|
-
catch { return reject(new Error('服务返回了无法识别的数据')) }
|
|
362
|
-
}
|
|
363
|
-
if (request.status < 200 || request.status >= 300) {
|
|
364
|
-
const error = new Error(payload?.error || `请求失败(HTTP ${request.status})`)
|
|
365
|
-
error.status = request.status
|
|
366
|
-
reject(error)
|
|
367
|
-
return
|
|
368
|
-
}
|
|
369
|
-
resolve(payload)
|
|
370
|
-
})
|
|
371
|
-
request.addEventListener('error', () => reject(new Error('上传连接中断,请检查网络后重试')))
|
|
372
|
-
request.addEventListener('abort', () => reject(new DOMException('上传已取消', 'AbortError')))
|
|
373
|
-
request.send(body)
|
|
374
|
-
})
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
async function responseError(response) {
|
|
378
|
-
let message = `请求失败(HTTP ${response.status})`
|
|
379
|
-
try {
|
|
380
|
-
const payload = await response.json()
|
|
381
|
-
if (payload?.error) message = payload.error
|
|
382
|
-
} catch {}
|
|
383
|
-
return Object.assign(new Error(message), { status: response.status })
|
|
384
|
-
}
|
|
191
|
+
const { api, binaryRequest, binaryUploadRequest } = createApiClient({
|
|
192
|
+
apiBase: API_BASE,
|
|
193
|
+
authMode: AUTH_MODE,
|
|
194
|
+
getToken: () => state.token,
|
|
195
|
+
})
|
|
385
196
|
|
|
386
197
|
async function boot() {
|
|
387
198
|
if (AUTH_MODE === 'same-origin') {
|
|
@@ -2222,8 +2033,8 @@ function openNoteAgentGuide() {
|
|
|
2222
2033
|
element('section', { class: 'notes-agent-guide-rule' },
|
|
2223
2034
|
element('span', { class: 'notes-agent-guide-index', 'aria-hidden': 'true' }, '01'),
|
|
2224
2035
|
element('div', {},
|
|
2225
|
-
element('h3', {}, '
|
|
2226
|
-
element('p', {}, '
|
|
2036
|
+
element('h3', {}, '在当前消息里指定笔记文档'),
|
|
2037
|
+
element('p', {}, '只要当前消息明确提到“笔记文档”“笔记目录”或“笔记工作区”,会话就可以按你的要求查看和维护;无需套用固定句式。为了避免误操作,删除仍需明确说出删除,授权也不会从上一轮延续。'))),
|
|
2227
2038
|
element('section', { class: 'notes-agent-guide-rule' },
|
|
2228
2039
|
element('span', { class: 'notes-agent-guide-index', 'aria-hidden': 'true' }, '02'),
|
|
2229
2040
|
element('div', {},
|
|
@@ -2233,7 +2044,7 @@ function openNoteAgentGuide() {
|
|
|
2233
2044
|
element('section', { class: 'notes-agent-examples' },
|
|
2234
2045
|
element('h3', {}, '可以直接这样说'),
|
|
2235
2046
|
element('ul', {}, examples)),
|
|
2236
|
-
element('p', { class: 'notes-agent-guide-note' }, '只说“新建 Markdown
|
|
2047
|
+
element('p', { class: 'notes-agent-guide-note' }, '只说“新建 Markdown”或“创建本地目录”仍不会获得笔记权限;在当前消息中带上“笔记文档”即可。'))
|
|
2237
2048
|
return openSheet({
|
|
2238
2049
|
title: '让会话整理笔记',
|
|
2239
2050
|
description: '会话可以操作笔记工作区,但每次写入都需要当前用户消息明确授权。',
|
|
@@ -2366,9 +2177,10 @@ function renderNoteFolderContent(folder) {
|
|
|
2366
2177
|
renderNoteIcon(node, true),
|
|
2367
2178
|
element('span', {}, element('strong', {}, node.name), element('small', {}, noteKindLabel(node))),
|
|
2368
2179
|
),
|
|
2369
|
-
element('time', { datetime: node.updatedAt }, formatDate(node.updatedAt)),
|
|
2370
|
-
element('span', { class: 'notes-file-size' }, node.kind === 'folder' ? '' : formatBytes(node.size)),
|
|
2180
|
+
element('time', { datetime: node.updatedAt, 'data-label': '更新' }, formatDate(node.updatedAt)),
|
|
2181
|
+
element('span', { class: 'notes-file-size', 'data-label': node.kind === 'folder' ? '类型' : '大小' }, node.kind === 'folder' ? '目录' : formatBytes(node.size)),
|
|
2371
2182
|
element('div', { class: 'notes-file-actions' },
|
|
2183
|
+
node.kind !== 'folder' ? noteDownloadButton(node, 'ghost tiny') : null,
|
|
2372
2184
|
actionButton('重命名', () => openRenameNoteNode(node), 'ghost tiny'),
|
|
2373
2185
|
actionButton('复制', () => { void copyNoteNode(node) }, 'ghost tiny'),
|
|
2374
2186
|
actionButton('删除', () => { void confirmDeleteNoteNode(node) }, 'ghost tiny danger-text'),
|
|
@@ -2411,10 +2223,7 @@ function renderEditableNote(node) {
|
|
|
2411
2223
|
? element('div', { class: 'notes-live-editor', role: 'status', 'aria-label': `正在打开 ${node.name}` },
|
|
2412
2224
|
element('div', { class: 'notes-editor-loading' }, '正在打开文档…'))
|
|
2413
2225
|
: createPlainTextNoteEditor(node)
|
|
2414
|
-
|
|
2415
|
-
else mountPlainTextNoteEditor(editor, node)
|
|
2416
|
-
return element('main', { class: `notes-content is-document${markdown ? '' : ' has-line-numbers'}` },
|
|
2417
|
-
renderNoteFileToolbar(node, { editable: true }),
|
|
2226
|
+
const scrollHost = element('div', { class: 'notes-document-scroll', 'data-scroll-key': `notes-document:${node.id}` },
|
|
2418
2227
|
element('h1', {
|
|
2419
2228
|
class: 'notes-document-title', contenteditable: 'plaintext-only', spellcheck: 'false',
|
|
2420
2229
|
'aria-label': `修改 ${node.name} 的标题`, title: '点击修改标题',
|
|
@@ -2426,6 +2235,17 @@ function renderEditableNote(node) {
|
|
|
2426
2235
|
}, title),
|
|
2427
2236
|
editor,
|
|
2428
2237
|
)
|
|
2238
|
+
const outlineHost = markdown ? element('aside', { id: 'dsh-note-outline', class: 'notes-editor-outline', 'aria-label': '文档大纲', 'aria-hidden': 'true' }) : null
|
|
2239
|
+
const editorFrame = markdown
|
|
2240
|
+
? element('div', { class: 'notes-editor-frame', 'data-outline-open': 'false', 'data-find-open': 'false' }, scrollHost, outlineHost)
|
|
2241
|
+
: scrollHost
|
|
2242
|
+
if (markdown) mountMarkdownNoteEditor(editor, node, editorFrame, scrollHost, outlineHost)
|
|
2243
|
+
else mountPlainTextNoteEditor(editor, node)
|
|
2244
|
+
return element('main', { class: `notes-content is-document${markdown ? '' : ' has-line-numbers'}` },
|
|
2245
|
+
renderNoteFileToolbar(node, { editable: true, enhanced: markdown }),
|
|
2246
|
+
editorFrame,
|
|
2247
|
+
renderNoteFileStatusbar(node),
|
|
2248
|
+
)
|
|
2429
2249
|
}
|
|
2430
2250
|
|
|
2431
2251
|
function editableNoteTitle(node) {
|
|
@@ -2478,8 +2298,11 @@ function updateNoteDraft(value) {
|
|
|
2478
2298
|
syncNoteEditorChrome()
|
|
2479
2299
|
}
|
|
2480
2300
|
|
|
2481
|
-
function mountMarkdownNoteEditor(host, node) {
|
|
2301
|
+
function mountMarkdownNoteEditor(host, node, frame, scrollHost, outlineHost) {
|
|
2482
2302
|
mountMarkdownEditor(host, {
|
|
2303
|
+
frame,
|
|
2304
|
+
scrollHost,
|
|
2305
|
+
outlineHost,
|
|
2483
2306
|
markdown: state.notes.draft,
|
|
2484
2307
|
label: `编辑 ${node.name}`,
|
|
2485
2308
|
isCurrent: () => state.notes.selectedNode?.id === node.id,
|
|
@@ -2525,6 +2348,13 @@ function mountMarkdownEditor(host, options) {
|
|
|
2525
2348
|
host.removeAttribute('role')
|
|
2526
2349
|
markdownEditorHandle = runtime.createMarkdownEditor({
|
|
2527
2350
|
host,
|
|
2351
|
+
...(options.frame ? {
|
|
2352
|
+
frame: options.frame,
|
|
2353
|
+
scrollHost: options.scrollHost,
|
|
2354
|
+
outlineHost: options.outlineHost,
|
|
2355
|
+
findButton: host.closest('.notes-content')?.querySelector('[data-note-find]') || null,
|
|
2356
|
+
outlineButton: host.closest('.notes-content')?.querySelector('[data-note-outline]') || null,
|
|
2357
|
+
} : {}),
|
|
2528
2358
|
markdown: options.markdown,
|
|
2529
2359
|
label: options.label,
|
|
2530
2360
|
onChange: options.onChange,
|
|
@@ -2600,12 +2430,13 @@ function renderNoteFile(node) {
|
|
|
2600
2430
|
renderNoteIcon(node, true),
|
|
2601
2431
|
element('h2', {}, node.name),
|
|
2602
2432
|
element('p', {}, '该格式不能在浏览器中直接编辑,可以下载后使用本地应用打开。'),
|
|
2603
|
-
|
|
2433
|
+
noteDownloadButton(node, 'primary small', '下载文件'),
|
|
2604
2434
|
)
|
|
2605
2435
|
}
|
|
2606
2436
|
return element('main', { class: 'notes-content is-file' },
|
|
2607
2437
|
renderNoteFileToolbar(node),
|
|
2608
2438
|
element('section', { class: `notes-inline-viewer is-${previewKind}`, 'aria-label': `${node.name}内容` }, preview),
|
|
2439
|
+
renderNoteFileStatusbar(node),
|
|
2609
2440
|
)
|
|
2610
2441
|
}
|
|
2611
2442
|
|
|
@@ -2613,19 +2444,89 @@ function renderNoteFileToolbar(node, options = {}) {
|
|
|
2613
2444
|
return element('header', { class: 'notes-document-toolbar' },
|
|
2614
2445
|
element('div', { class: 'notes-toolbar-leading' },
|
|
2615
2446
|
renderNoteDocumentBreadcrumb(node),
|
|
2616
|
-
element('div', { class: 'notes-file-info', 'aria-label': '文件信息' },
|
|
2617
|
-
noteInfoItem('编号', shortNoteId(node.id), node.id),
|
|
2618
|
-
noteInfoItem('类型', node.kind === 'document' ? 'Markdown' : node.mediaType || '未知'),
|
|
2619
|
-
noteInfoItem('大小', formatBytes(node.size), '', 'size'),
|
|
2620
|
-
noteInfoItem('更新', formatDate(node.updatedAt), '', 'updated'),
|
|
2621
|
-
),
|
|
2622
2447
|
),
|
|
2623
2448
|
element('div', { class: 'notes-document-actions' },
|
|
2624
2449
|
options.editable ? element('span', { class: 'notes-save-state', role: 'status', 'data-note-save-state': '', 'data-dirty': String(state.notes.dirty) }, state.notes.dirty ? '未保存' : '已保存') : null,
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2450
|
+
options.enhanced ? actionButton('查找', () => markdownEditorHandle?.openFind(), 'ghost small', { 'data-note-find': '', 'data-note-mobile-overflow': '', 'aria-keyshortcuts': 'Control+F Meta+F', 'aria-pressed': 'false' }) : null,
|
|
2451
|
+
options.enhanced ? actionButton([
|
|
2452
|
+
interfaceIcon('outline', 'notes-toolbar-action-icon'),
|
|
2453
|
+
element('span', {}, '大纲'),
|
|
2454
|
+
], () => 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
|
+
options.editable ? actionButton('历史', () => { void openNoteHistory(node) }, 'ghost small', { 'data-note-history': '', 'data-note-mobile-overflow': '' }) : null,
|
|
2456
|
+
noteDownloadButton(node, 'ghost small', '下载', { 'data-note-mobile-overflow': '' }),
|
|
2457
|
+
actionButton('复制引用', () => { void copyNoteReference(node) }, 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2458
|
+
actionButton('重命名', () => openRenameNoteNode(node), 'ghost small', { 'data-note-mobile-overflow': '' }),
|
|
2459
|
+
options.editable ? actionButton('保存', () => { void saveNoteDocument() }, 'primary small', { 'data-note-save': '', 'data-note-mobile-overflow': '', disabled: !state.notes.dirty }) : null,
|
|
2460
|
+
renderNoteFileOverflowMenu(node, options),
|
|
2461
|
+
),
|
|
2462
|
+
)
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
function renderNoteFileOverflowMenu(node, options) {
|
|
2466
|
+
const details = element('details', {
|
|
2467
|
+
class: 'notes-document-more',
|
|
2468
|
+
onKeyDown: event => {
|
|
2469
|
+
if (event.key !== 'Escape') return
|
|
2470
|
+
event.preventDefault()
|
|
2471
|
+
details.open = false
|
|
2472
|
+
summary.focus()
|
|
2473
|
+
},
|
|
2474
|
+
onFocusOut: () => {
|
|
2475
|
+
window.setTimeout(() => {
|
|
2476
|
+
if (!details.contains(document.activeElement)) details.open = false
|
|
2477
|
+
}, 0)
|
|
2478
|
+
},
|
|
2479
|
+
})
|
|
2480
|
+
const summary = element('summary', { class: 'button ghost small', title: '更多操作', 'aria-label': `打开 ${node.name} 的更多操作` },
|
|
2481
|
+
interfaceIcon('more', 'notes-document-more-icon'),
|
|
2482
|
+
)
|
|
2483
|
+
details.addEventListener('toggle', () => {
|
|
2484
|
+
summary.setAttribute('aria-expanded', String(details.open))
|
|
2485
|
+
summary.setAttribute('aria-label', `${details.open ? '关闭' : '打开'} ${node.name} 的更多操作`)
|
|
2486
|
+
})
|
|
2487
|
+
const closeThen = action => event => {
|
|
2488
|
+
details.open = false
|
|
2489
|
+
action(event)
|
|
2490
|
+
}
|
|
2491
|
+
const menuItems = []
|
|
2492
|
+
if (options.editable && state.notes.dirty) {
|
|
2493
|
+
menuItems.push(noteMenuAction('保存修改', 'save', closeThen(() => { void saveNoteDocument() })))
|
|
2494
|
+
menuItems.push(noteMenuDivider())
|
|
2495
|
+
}
|
|
2496
|
+
if (options.enhanced) {
|
|
2497
|
+
menuItems.push(noteMenuAction('查找', 'search', closeThen(() => markdownEditorHandle?.openFind())))
|
|
2498
|
+
}
|
|
2499
|
+
if (options.editable) menuItems.push(noteMenuAction('页面历史', 'history', closeThen(() => { void openNoteHistory(node) })))
|
|
2500
|
+
if (options.enhanced || options.editable) menuItems.push(noteMenuDivider())
|
|
2501
|
+
menuItems.push(
|
|
2502
|
+
noteMenuAction('下载', 'download', closeThen(event => { void downloadNoteFile(node, event.currentTarget) })),
|
|
2503
|
+
noteMenuAction('复制引用', 'link', closeThen(() => { void copyNoteReference(node) })),
|
|
2504
|
+
noteMenuAction('重命名', 'rename', closeThen(() => openRenameNoteNode(node))),
|
|
2505
|
+
)
|
|
2506
|
+
const menu = element('div', { class: 'notes-document-more-menu', role: 'menu', 'aria-label': `${node.name} 的更多操作` }, menuItems)
|
|
2507
|
+
summary.setAttribute('aria-expanded', 'false')
|
|
2508
|
+
details.append(summary, menu)
|
|
2509
|
+
return details
|
|
2510
|
+
}
|
|
2511
|
+
|
|
2512
|
+
function noteMenuAction(label, icon, onClick, attributes = {}) {
|
|
2513
|
+
return actionButton([
|
|
2514
|
+
interfaceIcon(icon, 'notes-document-menu-icon'),
|
|
2515
|
+
element('span', { class: 'notes-document-menu-label' }, label),
|
|
2516
|
+
], onClick, 'ghost small notes-document-menu-item', { role: 'menuitem', ...attributes })
|
|
2517
|
+
}
|
|
2518
|
+
|
|
2519
|
+
function noteMenuDivider() {
|
|
2520
|
+
return element('div', { class: 'notes-document-menu-divider', role: 'separator' })
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function renderNoteFileStatusbar(node) {
|
|
2524
|
+
return element('footer', { class: 'notes-document-statusbar', 'aria-label': '文档信息' },
|
|
2525
|
+
element('div', { class: 'notes-file-info' },
|
|
2526
|
+
noteInfoItem('编号', shortNoteId(node.id), node.id, 'id'),
|
|
2527
|
+
noteInfoItem('类型', node.kind === 'document' ? 'Markdown' : node.mediaType || '未知'),
|
|
2528
|
+
noteInfoItem('大小', formatBytes(node.size), '', 'size'),
|
|
2529
|
+
noteInfoItem('更新', formatDate(node.updatedAt), '', 'updated'),
|
|
2629
2530
|
),
|
|
2630
2531
|
)
|
|
2631
2532
|
}
|
|
@@ -3153,14 +3054,132 @@ async function saveNoteDocument() {
|
|
|
3153
3054
|
}
|
|
3154
3055
|
}
|
|
3155
3056
|
|
|
3156
|
-
async function
|
|
3057
|
+
async function openNoteHistory(node) {
|
|
3058
|
+
if (state.notes.selectedNode?.id === node.id && state.notes.dirty && !await saveNoteDocument()) return
|
|
3059
|
+
try {
|
|
3060
|
+
const current = state.notes.selectedNode?.id === node.id ? state.notes.selectedNode : await api(`notes/${encodeURIComponent(node.id)}`)
|
|
3061
|
+
if (!current?.editable) throw new Error('只有可编辑的笔记文档支持页面历史。')
|
|
3062
|
+
const versions = await api(`notes/${encodeURIComponent(node.id)}/versions?limit=100`)
|
|
3063
|
+
if (!versions.length) {
|
|
3064
|
+
showToast('这个文档还没有可查看的保存记录。')
|
|
3065
|
+
return
|
|
3066
|
+
}
|
|
3067
|
+
let modal
|
|
3068
|
+
const view = window.DshKnowledgeNoteHistory.createNoteHistoryView({
|
|
3069
|
+
versions,
|
|
3070
|
+
currentVersion: current.version,
|
|
3071
|
+
currentContent: state.notes.selectedNode?.id === node.id ? state.notes.content : '',
|
|
3072
|
+
loadContent: async (version, signal) => {
|
|
3073
|
+
const blob = await binaryRequest(`notes/${encodeURIComponent(node.id)}/versions/${version.version}/content`, {
|
|
3074
|
+
responseType: 'blob', accept: version.mediaType || 'text/plain', signal,
|
|
3075
|
+
})
|
|
3076
|
+
return blob.text()
|
|
3077
|
+
},
|
|
3078
|
+
renderPreview: content => renderHistoricalNotePreview(content, isMarkdownNote(current)),
|
|
3079
|
+
renderDiff: renderNoteHistoryDiff,
|
|
3080
|
+
formatDate,
|
|
3081
|
+
formatBytes,
|
|
3082
|
+
onRestore: async (version, content) => {
|
|
3083
|
+
const updated = await api(`notes/${encodeURIComponent(node.id)}/versions/${version.version}/restore`, {
|
|
3084
|
+
method: 'POST', body: { expectedVersion: current.version },
|
|
3085
|
+
})
|
|
3086
|
+
if (state.notes.selectedNode?.id === node.id) {
|
|
3087
|
+
state.notes.selectedNode = updated
|
|
3088
|
+
state.notes.content = content
|
|
3089
|
+
state.notes.draft = content
|
|
3090
|
+
state.notes.dirty = false
|
|
3091
|
+
await loadNoteChildren(updated.parentId, true)
|
|
3092
|
+
}
|
|
3093
|
+
modal?.close(true)
|
|
3094
|
+
renderShell()
|
|
3095
|
+
showToast(`已将版本 ${version.version} 恢复为新的版本 ${updated.version}。`)
|
|
3096
|
+
},
|
|
3097
|
+
onError: error => showToast(friendlyError(error), 'error'),
|
|
3098
|
+
})
|
|
3099
|
+
modal = openModal({
|
|
3100
|
+
title: `${current.name} · 页面历史`,
|
|
3101
|
+
description: '每次内容保存都会形成不可变快照;恢复历史不会删除当前版本。',
|
|
3102
|
+
body: view.element,
|
|
3103
|
+
cancelLabel: '关闭',
|
|
3104
|
+
onClose: () => view.destroy(),
|
|
3105
|
+
})
|
|
3106
|
+
modal.dialog.classList.add('note-history-dialog')
|
|
3107
|
+
modal.dialog.classList.remove('narrow')
|
|
3108
|
+
} catch (error) {
|
|
3109
|
+
showToast(friendlyError(error), 'error')
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
function renderHistoricalNotePreview(content, markdown) {
|
|
3114
|
+
if (!markdown) return element('pre', { class: 'note-history-plain-preview', role: 'document' }, content || '(空文档)')
|
|
3115
|
+
const rendered = renderMarkdownPreview(content).cloneNode(true)
|
|
3116
|
+
rendered.classList.add('note-history-markdown-preview')
|
|
3117
|
+
rendered.querySelectorAll('a').forEach(link => {
|
|
3118
|
+
link.removeAttribute('href')
|
|
3119
|
+
link.removeAttribute('role')
|
|
3120
|
+
link.removeAttribute('tabindex')
|
|
3121
|
+
link.removeAttribute('title')
|
|
3122
|
+
})
|
|
3123
|
+
return rendered
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
function renderNoteHistoryDiff(historical, current) {
|
|
3127
|
+
const diff = window.DshKnowledgeReview.createLineDiff(historical, current)
|
|
3128
|
+
const lines = window.DshKnowledgeReview.compactDiffLines(diff.lines, 3)
|
|
3129
|
+
return element('section', { class: 'note-history-diff', 'aria-label': '历史版本与当前版本的逐行差异' },
|
|
3130
|
+
element('div', { class: 'note-history-diff-summary' },
|
|
3131
|
+
element('strong', {}, '历史版本 → 当前版本'),
|
|
3132
|
+
element('div', { class: 'diff-summary', 'aria-label': `新增 ${diff.additions} 行,删除 ${diff.deletions} 行` },
|
|
3133
|
+
element('span', { class: 'diff-stat additions' }, `+${diff.additions}`),
|
|
3134
|
+
element('span', { class: 'diff-stat deletions' }, `-${diff.deletions}`),
|
|
3135
|
+
),
|
|
3136
|
+
),
|
|
3137
|
+
diff.simplified ? element('div', { class: 'diff-notice' }, '内容较长,已使用有界的简化差异视图。') : null,
|
|
3138
|
+
element('div', { class: 'diff-viewer', role: 'table' },
|
|
3139
|
+
element('div', { class: 'diff-column-headings', role: 'row' },
|
|
3140
|
+
element('span', { role: 'columnheader' }, '旧'),
|
|
3141
|
+
element('span', { role: 'columnheader' }, '新'),
|
|
3142
|
+
element('span', { 'aria-hidden': 'true' }),
|
|
3143
|
+
element('span', { role: 'columnheader' }, '正文'),
|
|
3144
|
+
),
|
|
3145
|
+
lines.length ? lines.map(renderDiffLine) : element('div', { class: 'diff-empty' }, '所选版本与当前内容相同。'),
|
|
3146
|
+
),
|
|
3147
|
+
)
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function noteDownloadButton(node, variant = 'ghost small', label = '下载', attributes = {}) {
|
|
3151
|
+
return actionButton(label, event => { void downloadNoteFile(node, event.currentTarget) }, variant, {
|
|
3152
|
+
'aria-label': `下载 ${node.name}`,
|
|
3153
|
+
title: `下载 ${node.name}`,
|
|
3154
|
+
...attributes,
|
|
3155
|
+
})
|
|
3156
|
+
}
|
|
3157
|
+
|
|
3158
|
+
async function downloadNoteFile(node, button) {
|
|
3159
|
+
const originalLabel = button?.textContent || ''
|
|
3160
|
+
if (button) {
|
|
3161
|
+
button.disabled = true
|
|
3162
|
+
button.setAttribute('aria-busy', 'true')
|
|
3163
|
+
button.textContent = '下载中'
|
|
3164
|
+
}
|
|
3157
3165
|
try {
|
|
3158
|
-
|
|
3166
|
+
if (state.notes.selectedNode?.id === node.id && state.notes.dirty && !await saveNoteDocument()) return
|
|
3167
|
+
const current = state.notes.selectedNode?.id === node.id ? state.notes.selectedNode : node
|
|
3168
|
+
const blob = await binaryRequest(`notes/${encodeURIComponent(current.id)}/content?download=1`, { responseType: 'blob', accept: current.mediaType || 'application/octet-stream' })
|
|
3159
3169
|
const url = URL.createObjectURL(blob)
|
|
3160
|
-
const anchor = element('a', { href: url, download:
|
|
3170
|
+
const anchor = element('a', { href: url, download: current.name })
|
|
3161
3171
|
document.body.append(anchor); anchor.click(); anchor.remove()
|
|
3162
3172
|
window.setTimeout(() => URL.revokeObjectURL(url), 1000)
|
|
3163
|
-
|
|
3173
|
+
showToast(`已下载 ${current.name}。`)
|
|
3174
|
+
} catch (error) {
|
|
3175
|
+
showToast(`下载失败:${friendlyError(error)}`, 'error')
|
|
3176
|
+
} finally {
|
|
3177
|
+
if (button?.isConnected) {
|
|
3178
|
+
button.disabled = false
|
|
3179
|
+
button.removeAttribute('aria-busy')
|
|
3180
|
+
button.textContent = originalLabel
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3164
3183
|
}
|
|
3165
3184
|
|
|
3166
3185
|
async function copyNoteReference(node) {
|
|
@@ -3274,13 +3293,21 @@ function formatBytes(value) {
|
|
|
3274
3293
|
|
|
3275
3294
|
function renderCandidates() {
|
|
3276
3295
|
const statuses = [['pending', '待审核'], ['approved', '已通过'], ['rejected', '已拒绝']]
|
|
3277
|
-
|
|
3296
|
+
const pendingCount = state.stats?.candidates.pending ?? state.candidates.filter(candidate => candidate.status === 'pending').length
|
|
3297
|
+
return element('section', { class: 'candidate-page', 'aria-labelledby': 'candidates-heading' },
|
|
3278
3298
|
element('div', { class: 'section-heading' },
|
|
3279
3299
|
element('div', {}, element('h2', { id: 'candidates-heading' }, 'AI 提取候选'), element('p', {}, '审核写入、模型推断、低置信度结果和冲突项会在这里等待确认;只有高置信度且证据明确的结果才能直接写入。')),
|
|
3280
|
-
element('div', { class: '
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3300
|
+
element('div', { class: 'candidate-heading-actions' },
|
|
3301
|
+
state.candidateStatus === 'pending' ? actionButton(state.candidateBatchRunning ? '正在通过…' : '一键通过', openBulkApprove, 'primary', {
|
|
3302
|
+
disabled: state.candidateBatchRunning || pendingCount === 0,
|
|
3303
|
+
title: pendingCount === 0 ? '当前没有待审核候选' : '分批通过新增与可安全合并项,冲突项会保留',
|
|
3304
|
+
'aria-busy': String(state.candidateBatchRunning),
|
|
3305
|
+
}) : null,
|
|
3306
|
+
element('div', { class: 'tabs', role: 'tablist', 'aria-label': '候选状态' }, statuses.map(([value, label]) => element('button', {
|
|
3307
|
+
type: 'button', role: 'tab', class: 'tab', 'aria-selected': String(state.candidateStatus === value),
|
|
3308
|
+
onClick: async () => { state.candidateStatus = value; await navigate('candidates') },
|
|
3309
|
+
}, label, state.stats ? ` ${state.stats.candidates[value]}` : ''))),
|
|
3310
|
+
),
|
|
3284
3311
|
),
|
|
3285
3312
|
state.candidates.length ? element('div', { class: 'candidate-list' }, state.candidates.map(renderCandidateCard))
|
|
3286
3313
|
: emptyState(`没有${STATUS_LABELS[state.candidateStatus]}候选`, state.candidateStatus === 'pending' ? '新的对话结束后,插件会在后台判断是否产生候选。' : '切换其他状态查看历史记录。'),
|
|
@@ -3953,6 +3980,88 @@ async function reviewCandidate(candidate, decision, resolution) {
|
|
|
3953
3980
|
})
|
|
3954
3981
|
}
|
|
3955
3982
|
|
|
3983
|
+
function openBulkApprove() {
|
|
3984
|
+
if (state.candidateBatchRunning) return
|
|
3985
|
+
const pendingCount = state.stats?.candidates.pending ?? state.candidates.length
|
|
3986
|
+
const summary = element('strong', {}, `准备处理 ${pendingCount} 条待审核候选`)
|
|
3987
|
+
const detail = element('p', {}, '开始后会按创建时间分批处理,并实时显示结果。')
|
|
3988
|
+
const metrics = element('div', { class: 'bulk-review-metrics' })
|
|
3989
|
+
const status = element('div', {
|
|
3990
|
+
class: 'bulk-review-status', role: 'status', 'aria-live': 'polite', 'aria-atomic': 'true', hidden: true,
|
|
3991
|
+
}, summary, detail, metrics)
|
|
3992
|
+
const body = element('div', { class: 'bulk-review-confirm' },
|
|
3993
|
+
element('p', {}, '将分批通过待审核中的新增与可安全合并项。冲突项、目标不可用项,以及审核期间发生变化的候选会保留,供你逐条处理。'),
|
|
3994
|
+
element('div', { class: 'bulk-review-safety' },
|
|
3995
|
+
element('strong', {}, '不会自动处理冲突'),
|
|
3996
|
+
element('span', {}, '同一文档的候选会按时间顺序写入,避免并发覆盖。'),
|
|
3997
|
+
),
|
|
3998
|
+
status,
|
|
3999
|
+
)
|
|
4000
|
+
openModal({
|
|
4001
|
+
title: '一键通过待审核候选?',
|
|
4002
|
+
description: '适合集中处理积压候选;操作完成后无法批量撤销。',
|
|
4003
|
+
body,
|
|
4004
|
+
primaryLabel: '开始一键通过',
|
|
4005
|
+
onPrimary: async () => {
|
|
4006
|
+
state.candidateBatchRunning = true
|
|
4007
|
+
status.hidden = false
|
|
4008
|
+
const excluded = new Set()
|
|
4009
|
+
const totals = { approved: 0, deferred: 0, failed: 0, selected: 0, remainingReviewable: pendingCount, remainingManual: 0 }
|
|
4010
|
+
const updateProgress = (message = '正在按顺序审核候选…') => {
|
|
4011
|
+
summary.textContent = message
|
|
4012
|
+
detail.textContent = `已检查 ${totals.selected} 条;每批最多 25 条。`
|
|
4013
|
+
metrics.replaceChildren(
|
|
4014
|
+
bulkReviewMetric('已通过', totals.approved, 'success'),
|
|
4015
|
+
bulkReviewMetric('转为冲突', totals.deferred, 'warning'),
|
|
4016
|
+
bulkReviewMetric('失败保留', totals.failed, totals.failed ? 'danger' : ''),
|
|
4017
|
+
bulkReviewMetric('剩余可处理', totals.remainingReviewable),
|
|
4018
|
+
)
|
|
4019
|
+
}
|
|
4020
|
+
updateProgress()
|
|
4021
|
+
try {
|
|
4022
|
+
for (let pass = 0; pass < 1000; pass += 1) {
|
|
4023
|
+
const result = await api('candidates/bulk-review', {
|
|
4024
|
+
method: 'POST', body: { limit: 25, excludeIds: [...excluded] },
|
|
4025
|
+
})
|
|
4026
|
+
totals.selected += result.selected
|
|
4027
|
+
totals.approved += result.approved
|
|
4028
|
+
totals.deferred += result.deferred
|
|
4029
|
+
totals.failed += result.failed.length
|
|
4030
|
+
totals.remainingReviewable = result.remainingReviewable
|
|
4031
|
+
totals.remainingManual = result.remainingManual
|
|
4032
|
+
result.failed.forEach(item => excluded.add(item.id))
|
|
4033
|
+
updateProgress()
|
|
4034
|
+
if (result.remainingReviewable === 0) break
|
|
4035
|
+
if (result.selected === 0) throw new Error('批量审核没有继续推进;剩余候选已保留,请刷新后重试。')
|
|
4036
|
+
}
|
|
4037
|
+
if (totals.remainingReviewable > 0) throw new Error('待审核候选过多,本次处理已达到安全上限,请再次执行。')
|
|
4038
|
+
summary.textContent = totals.approved > 0 ? `已通过 ${totals.approved} 条候选` : '没有可自动通过的候选'
|
|
4039
|
+
detail.textContent = totals.remainingManual > 0
|
|
4040
|
+
? `仍有 ${totals.remainingManual} 条冲突或失败项保留,需逐条确认。`
|
|
4041
|
+
: '待审核队列已处理完成。'
|
|
4042
|
+
state.stats = null
|
|
4043
|
+
await loadCandidates()
|
|
4044
|
+
state.candidateBatchRunning = false
|
|
4045
|
+
renderShell()
|
|
4046
|
+
const parts = [`已通过 ${totals.approved} 条`]
|
|
4047
|
+
if (totals.deferred) parts.push(`${totals.deferred} 条转为冲突`)
|
|
4048
|
+
if (totals.failed) parts.push(`${totals.failed} 条失败并保留`)
|
|
4049
|
+
if (totals.remainingManual) parts.push(`${totals.remainingManual} 条待人工处理`)
|
|
4050
|
+
showToast(`${parts.join(',')}。`, totals.failed ? 'error' : '')
|
|
4051
|
+
return true
|
|
4052
|
+
} finally {
|
|
4053
|
+
state.candidateBatchRunning = false
|
|
4054
|
+
}
|
|
4055
|
+
},
|
|
4056
|
+
})
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function bulkReviewMetric(label, value, variant = '') {
|
|
4060
|
+
return element('span', { class: `bulk-review-metric ${variant}`.trim() },
|
|
4061
|
+
element('small', {}, label), element('strong', {}, String(value)),
|
|
4062
|
+
)
|
|
4063
|
+
}
|
|
4064
|
+
|
|
3956
4065
|
function confirmArchive(entry) {
|
|
3957
4066
|
openConfirm({ title: '归档这条知识?', message: '归档后它不会再参与召回,版本历史仍会保留。', confirmLabel: '确认归档', danger: true, onConfirm: async () => {
|
|
3958
4067
|
await api(`entries/${encodeURIComponent(entry.id)}/archive`, { method: 'POST' })
|