@lemoncat7/dsh-knowledge 2.6.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/lib/api.d.ts.map +1 -1
- package/lib/api.js +13 -2
- package/lib/api.js.map +1 -1
- package/lib/client.d.ts +3 -0
- package/lib/client.d.ts.map +1 -1
- package/lib/client.js +102 -7
- package/lib/client.js.map +4 -4
- package/lib/extraction.d.ts.map +1 -1
- package/lib/extraction.js +1 -0
- package/lib/extraction.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +16 -1
- package/lib/index.js.map +1 -1
- package/lib/local-provider.d.ts +2 -2
- package/lib/local-provider.d.ts.map +1 -1
- package/lib/local-provider.js +12 -5
- package/lib/local-provider.js.map +1 -1
- package/lib/note-recording.d.ts +21 -0
- package/lib/note-recording.d.ts.map +1 -0
- package/lib/note-recording.js +32 -0
- package/lib/note-recording.js.map +1 -0
- package/lib/note-reference-tools.js +3 -2
- package/lib/note-reference-tools.js.map +1 -1
- package/lib/note-tools.d.ts +3 -2
- package/lib/note-tools.d.ts.map +1 -1
- package/lib/note-tools.js +35 -12
- package/lib/note-tools.js.map +1 -1
- package/lib/notes/store.d.ts +5 -1
- package/lib/notes/store.d.ts.map +1 -1
- package/lib/notes/store.js +11 -2
- package/lib/notes/store.js.map +1 -1
- package/lib/provider.d.ts +2 -2
- package/lib/provider.d.ts.map +1 -1
- package/lib/remote-provider.d.ts +2 -2
- package/lib/remote-provider.d.ts.map +1 -1
- package/lib/remote-provider.js +6 -5
- package/lib/remote-provider.js.map +1 -1
- package/lib/tools.js +1 -1
- package/lib/tools.js.map +1 -1
- package/lib/web.js +1 -1
- package/lib/web.js.map +1 -1
- package/lib/writeback/control.d.ts +5 -0
- package/lib/writeback/control.d.ts.map +1 -0
- package/lib/writeback/control.js +39 -0
- package/lib/writeback/control.js.map +1 -0
- package/lib/writeback/live-control.d.ts +5 -0
- package/lib/writeback/live-control.d.ts.map +1 -0
- package/lib/writeback/live-control.js +63 -0
- package/lib/writeback/live-control.js.map +1 -0
- package/lib/writeback/queue.d.ts +27 -1
- package/lib/writeback/queue.d.ts.map +1 -1
- package/lib/writeback/queue.js +97 -7
- package/lib/writeback/queue.js.map +1 -1
- package/lib/writeback/status-client.d.ts +3 -0
- package/lib/writeback/status-client.d.ts.map +1 -1
- package/lib/writeback/status-client.js +30 -4
- package/lib/writeback/status-client.js.map +1 -1
- package/package.json +1 -1
- package/web/app.js +186 -26
- package/web/document-sync-ui.js +23 -0
- package/web/document-sync.js +43 -0
- package/web/styles.css +23 -0
- package/web/writeback-live.d.ts +1 -0
- package/web/writeback-live.js +52 -0
- package/web/writeback-workspace.js +76 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** One bounded metadata poll for the active document; no full-library reloads. */
|
|
2
|
+
export function createDocumentSync({ current, check, refresh, notify, visible = () => !document.hidden, interval = 5000 }) {
|
|
3
|
+
let timer, controller, stopped = false, failures = 0
|
|
4
|
+
const same = target => {
|
|
5
|
+
const active = current()
|
|
6
|
+
return active && active.identity === target.identity && active.version === target.version && active.key === target.key
|
|
7
|
+
}
|
|
8
|
+
async function tick() {
|
|
9
|
+
clearTimeout(timer)
|
|
10
|
+
if (stopped || controller) return
|
|
11
|
+
const target = visible() ? current() : null
|
|
12
|
+
if (!target) return schedule()
|
|
13
|
+
controller = new AbortController()
|
|
14
|
+
const timeout = setTimeout(() => controller?.abort(), 10000)
|
|
15
|
+
try {
|
|
16
|
+
const remote = await check(target, controller.signal)
|
|
17
|
+
if (stopped || !visible() || !same(target)) return
|
|
18
|
+
failures = 0
|
|
19
|
+
if (remote.version !== target.version || remote.updatedAt !== target.updatedAt) {
|
|
20
|
+
if (current().busy()) notify(target, 'changed')
|
|
21
|
+
else await refresh(target, controller.signal, () => !stopped && visible() && same(target) && !current().busy())
|
|
22
|
+
} else notify(target, 'current')
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (!stopped && same(target)) {
|
|
25
|
+
failures++
|
|
26
|
+
notify(target, error.status === 404 ? 'missing' : 'offline')
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
clearTimeout(timeout)
|
|
30
|
+
controller = null
|
|
31
|
+
schedule()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function schedule() {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
if (!stopped && visible()) timer = setTimeout(tick, Math.min(30000, interval * 2 ** Math.min(failures, 3)))
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
wake() { if (!stopped) void tick() },
|
|
40
|
+
pause() { clearTimeout(timer); controller?.abort() },
|
|
41
|
+
stop() { stopped = true; clearTimeout(timer); controller?.abort() },
|
|
42
|
+
}
|
|
43
|
+
}
|
package/web/styles.css
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
.document-sync-notice {
|
|
2
|
+
flex: 1 0 100%; display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
|
|
3
|
+
padding: 8px 0 0; color: var(--text); font-size: 12px;
|
|
4
|
+
border-top: 1px solid var(--separator); overflow-wrap: anywhere;
|
|
5
|
+
}
|
|
6
|
+
.note-editor-toolbar:has(.document-sync-notice), .notes-document-toolbar:has(.document-sync-notice) { flex-wrap: wrap; }
|
|
7
|
+
.sync-conflict-dialog { width: min(980px, calc(100vw - 32px)); max-width: 980px; }
|
|
8
|
+
.sync-conflict-body { display: grid; gap: 16px; }
|
|
9
|
+
.sync-merge-columns { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; }
|
|
10
|
+
.sync-merge-columns section { min-width: 0; display: flex; flex-direction: column; gap: 8px; }
|
|
11
|
+
.sync-merge-columns h3, .sync-merge-columns p { margin: 0; overflow-wrap: anywhere; }
|
|
12
|
+
.sync-conflict-dialog .sync-merge-content { width: 100%; height: 260px; min-height: 200px; resize: vertical; line-height: 1.6; }
|
|
13
|
+
@media (max-width: 640px) { .sync-merge-columns { grid-template-columns: minmax(0, 1fr); } }
|
|
14
|
+
|
|
1
15
|
:root {
|
|
2
16
|
color-scheme: light dark;
|
|
3
17
|
font-family: var(--font-ui);
|
|
@@ -2028,3 +2042,12 @@ html,
|
|
|
2028
2042
|
@media (forced-colors: active) {
|
|
2029
2043
|
.status-dot, .history-dot { forced-color-adjust: none; }
|
|
2030
2044
|
}
|
|
2045
|
+
/* Durable outbox: reuse workspace controls and surfaces, no animated cards. */
|
|
2046
|
+
.writeback-workspace { display: grid; gap: 12px; min-width: 0; }
|
|
2047
|
+
.writeback-toolbar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
2048
|
+
.writeback-toolbar input { flex: 1 1 220px; min-width: 0; }
|
|
2049
|
+
.writeback-job-list { display: grid; gap: 12px; }
|
|
2050
|
+
.writeback-job { padding: 12px; border: 1px solid var(--border); border-radius: 12px; min-width: 0; }
|
|
2051
|
+
.writeback-job p { margin: 8px 0; overflow-wrap: anywhere; }
|
|
2052
|
+
.writeback-job-key { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 8px; }
|
|
2053
|
+
.writeback-job pre { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function subscribeWritebackChanges(client: 'conversation-web' | 'management-web', listener: () => void): () => void
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Shared by visible conversation turns; the iframe owns its own single channel.
|
|
2
|
+
const groups = new Map()
|
|
3
|
+
export function subscribeWritebackChanges(client, listener) {
|
|
4
|
+
let group = groups.get(client)
|
|
5
|
+
if (!group) {
|
|
6
|
+
const listeners = new Set()
|
|
7
|
+
let stopped = false, request, timer, revision = '', failures = 0
|
|
8
|
+
function emit() { for (const fn of listeners) fn() }
|
|
9
|
+
async function poll() {
|
|
10
|
+
if (stopped || document.hidden) return
|
|
11
|
+
const controller = new AbortController()
|
|
12
|
+
request = controller
|
|
13
|
+
try {
|
|
14
|
+
const response = await fetch(`/knowledge-control/v1/writeback-changes?since=${encodeURIComponent(revision)}`, {
|
|
15
|
+
credentials: 'same-origin', cache: 'no-store', headers: { 'x-dsh-knowledge-client': client },
|
|
16
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(25000)]),
|
|
17
|
+
})
|
|
18
|
+
if (!response.ok) throw new Error('notification transport unavailable')
|
|
19
|
+
const data = await response.json()
|
|
20
|
+
if (typeof data.revision !== 'string') throw new Error('invalid revision')
|
|
21
|
+
if (stopped || controller.signal.aborted) return
|
|
22
|
+
const changed = revision !== data.revision || failures > 0
|
|
23
|
+
revision = data.revision
|
|
24
|
+
failures = 0
|
|
25
|
+
if (changed) emit()
|
|
26
|
+
} catch { if (!controller.signal.aborted) failures++ }
|
|
27
|
+
finally {
|
|
28
|
+
if (request === controller) {
|
|
29
|
+
request = undefined
|
|
30
|
+
if (!stopped && !document.hidden) timer = setTimeout(poll, failures ? Math.min(30000, 1000 * 2 ** Math.min(failures, 5)) : 0)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function resume() {
|
|
35
|
+
clearTimeout(timer)
|
|
36
|
+
request?.abort()
|
|
37
|
+
request = undefined
|
|
38
|
+
if (!document.hidden && !stopped) { emit(); void poll() }
|
|
39
|
+
}
|
|
40
|
+
document.addEventListener('visibilitychange', resume)
|
|
41
|
+
window.addEventListener('online', resume)
|
|
42
|
+
group = { listeners, close() { stopped = true; clearTimeout(timer); request?.abort(); document.removeEventListener('visibilitychange', resume); window.removeEventListener('online', resume) } }
|
|
43
|
+
groups.set(client, group)
|
|
44
|
+
// Let the first caller attach before the initial revision arrives.
|
|
45
|
+
timer = setTimeout(poll, 0)
|
|
46
|
+
}
|
|
47
|
+
group.listeners.add(listener)
|
|
48
|
+
return () => {
|
|
49
|
+
group.listeners.delete(listener)
|
|
50
|
+
if (!group.listeners.size) { group.close(); groups.delete(client) }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { subscribeWritebackChanges } from './writeback-live.js'
|
|
2
|
+
/** Local outbox UI. Poll only this view, never reload the knowledge workspace. */
|
|
3
|
+
export function createWritebackWorkspace({ element, actionButton, openConfirm, sessionId = '' }) {
|
|
4
|
+
const labels = { queued: '排队中', running: '回写中', failed: '失败', completed: '已完成', cancelled: '已取消' }
|
|
5
|
+
let disposed = false, timer, request, offset = 0, busy = false, previous = '', dirty = false
|
|
6
|
+
const message = element('p', { role: 'status' })
|
|
7
|
+
const list = element('div', { class: 'writeback-job-list' })
|
|
8
|
+
const page = element('span', {})
|
|
9
|
+
const filter = element('input', { class: 'input', value: sessionId, placeholder: '按会话编号筛选(可留空)', 'aria-label': '会话编号' })
|
|
10
|
+
const previousButton = actionButton('上一页', () => { offset = Math.max(0, offset - 50); previous = ''; void refresh() }, 'ghost small')
|
|
11
|
+
const nextButton = actionButton('下一页', () => { offset += 50; previous = ''; void refresh() }, 'ghost small')
|
|
12
|
+
const root = element('section', { class: 'writeback-workspace' },
|
|
13
|
+
element('p', {}, '本机回写队列。同一会话按顺序执行;网络中断会自动重试,连续失败可手动重试或取消。取消不会撤销已经写入的内容。'),
|
|
14
|
+
element('div', { class: 'writeback-toolbar' }, filter, actionButton('筛选', () => { sessionId = filter.value.trim(); offset = 0; previous = ''; void refresh() }, 'small'), actionButton('刷新', () => { void refresh() }, 'ghost small')),
|
|
15
|
+
message, list, element('div', { class: 'writeback-toolbar' }, previousButton, page, nextButton))
|
|
16
|
+
async function fetchJson(params, method = 'GET', signal) {
|
|
17
|
+
const response = await fetch(`/knowledge-control/v1/writeback-jobs?${params}`, { method, credentials: 'same-origin', headers: { 'x-dsh-knowledge-client': 'management-web' }, signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15000)]) : AbortSignal.timeout(15000) })
|
|
18
|
+
const data = await response.json().catch(() => ({}))
|
|
19
|
+
if (!response.ok) throw new Error(data.error || `读取回写队列失败(${response.status})`)
|
|
20
|
+
return data
|
|
21
|
+
}
|
|
22
|
+
async function operate(job, action) {
|
|
23
|
+
if (busy || disposed) return
|
|
24
|
+
busy = true
|
|
25
|
+
request?.abort()
|
|
26
|
+
message.textContent = action === 'cancel' ? '正在请求取消…' : '正在提交重试…'
|
|
27
|
+
try {
|
|
28
|
+
const result = await fetchJson(new URLSearchParams({ action, sourceKey: job.sourceKey }), 'POST')
|
|
29
|
+
message.textContent = result.summary
|
|
30
|
+
previous = ''
|
|
31
|
+
} catch (error) { message.textContent = error.message }
|
|
32
|
+
finally { busy = false; if (!disposed) void refresh(true) }
|
|
33
|
+
}
|
|
34
|
+
function renderJob(job) {
|
|
35
|
+
const actions = element('div', { class: 'writeback-toolbar' })
|
|
36
|
+
if (job.status === 'failed') actions.append(actionButton('重试', () => operate(job, 'retry'), 'small'))
|
|
37
|
+
if (['queued', 'running', 'failed'].includes(job.status)) actions.append(actionButton(job.cancelRequested ? '取消中…' : '取消回写', () => openConfirm({ title: '取消这条回写?', message: '取消后不再重试,并允许后续轮次继续。已经写入的内容不会撤销。', confirmLabel: '取消回写', danger: true, onConfirm: () => operate(job, 'cancel') }), 'ghost small', { disabled: job.cancelRequested }))
|
|
38
|
+
return element('article', { class: 'writeback-job' },
|
|
39
|
+
element('div', { class: 'writeback-toolbar' }, element('strong', {}, labels[job.status] || job.status), element('span', {}, `已尝试 ${job.attempts} 次`), actions),
|
|
40
|
+
element('div', { class: 'writeback-job-key', title: job.sourceKey }, job.sourceKey),
|
|
41
|
+
job.createdAt ? element('p', {}, '创建于 ', element('time', { datetime: new Date(job.createdAt).toISOString() }, new Date(job.createdAt).toLocaleString())) : element('p', {}, '历史任务 · 创建时间未记录'),
|
|
42
|
+
element('p', {}, job.summary),
|
|
43
|
+
job.blockedBy ? element('p', {}, `阻塞任务:${job.blockedBy}`) : null,
|
|
44
|
+
job.nextAttemptAt ? element('p', {}, `下次自动重试:${new Date(job.nextAttemptAt).toLocaleString()}`) : null,
|
|
45
|
+
job.error ? element('details', {}, element('summary', {}, '失败原因'), element('pre', {}, job.error)) : null,
|
|
46
|
+
...(job.destinations || []).map(destination => element('p', {}, `${destination.knowledgeBaseName} / ${destination.documentTitle || destination.documentId || ''}`)))
|
|
47
|
+
}
|
|
48
|
+
async function refresh(preserveMessage = false) {
|
|
49
|
+
clearTimeout(timer)
|
|
50
|
+
if (disposed || document.hidden || busy) return
|
|
51
|
+
dirty = false
|
|
52
|
+
request?.abort()
|
|
53
|
+
const controller = new AbortController()
|
|
54
|
+
request = controller
|
|
55
|
+
try {
|
|
56
|
+
const data = await fetchJson(new URLSearchParams({ sessionId, offset: String(offset) }), 'GET', controller.signal)
|
|
57
|
+
if (disposed || request !== controller) return
|
|
58
|
+
const fingerprint = JSON.stringify(data)
|
|
59
|
+
if (fingerprint !== previous) {
|
|
60
|
+
list.replaceChildren(...(data.items.length ? data.items.map(renderJob) : [element('p', {}, '没有回写任务')]))
|
|
61
|
+
previous = fingerprint
|
|
62
|
+
}
|
|
63
|
+
page.textContent = `共 ${data.total} 条 · 第 ${Math.floor(offset / 50) + 1} 页`
|
|
64
|
+
previousButton.disabled = offset === 0
|
|
65
|
+
nextButton.disabled = offset + 50 >= data.total
|
|
66
|
+
if (!preserveMessage) message.textContent = '状态自动更新'
|
|
67
|
+
} catch (error) { if (!controller.signal.aborted && !disposed) message.textContent = `${error.message};将自动重连` }
|
|
68
|
+
finally { if (request === controller) { request = undefined; if (!disposed) timer = setTimeout(() => refresh(), dirty ? 0 : 5000) } }
|
|
69
|
+
}
|
|
70
|
+
function visibility() { if (document.hidden) { clearTimeout(timer); request?.abort() } else void refresh() }
|
|
71
|
+
document.addEventListener('visibilitychange', visibility)
|
|
72
|
+
window.addEventListener('online', visibility)
|
|
73
|
+
const unsubscribeChanges = subscribeWritebackChanges('management-web', () => { if (request || busy) dirty = true; else void refresh() })
|
|
74
|
+
void refresh()
|
|
75
|
+
return { root, dispose() { disposed = true; clearTimeout(timer); request?.abort(); unsubscribeChanges(); document.removeEventListener('visibilitychange', visibility); window.removeEventListener('online', visibility) } }
|
|
76
|
+
}
|