@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.
Files changed (66) hide show
  1. package/README.md +42 -0
  2. package/lib/api.d.ts.map +1 -1
  3. package/lib/api.js +13 -2
  4. package/lib/api.js.map +1 -1
  5. package/lib/client.d.ts +3 -0
  6. package/lib/client.d.ts.map +1 -1
  7. package/lib/client.js +102 -7
  8. package/lib/client.js.map +4 -4
  9. package/lib/extraction.d.ts.map +1 -1
  10. package/lib/extraction.js +1 -0
  11. package/lib/extraction.js.map +1 -1
  12. package/lib/index.d.ts.map +1 -1
  13. package/lib/index.js +16 -1
  14. package/lib/index.js.map +1 -1
  15. package/lib/local-provider.d.ts +2 -2
  16. package/lib/local-provider.d.ts.map +1 -1
  17. package/lib/local-provider.js +12 -5
  18. package/lib/local-provider.js.map +1 -1
  19. package/lib/note-recording.d.ts +21 -0
  20. package/lib/note-recording.d.ts.map +1 -0
  21. package/lib/note-recording.js +32 -0
  22. package/lib/note-recording.js.map +1 -0
  23. package/lib/note-reference-tools.js +3 -2
  24. package/lib/note-reference-tools.js.map +1 -1
  25. package/lib/note-tools.d.ts +3 -2
  26. package/lib/note-tools.d.ts.map +1 -1
  27. package/lib/note-tools.js +35 -12
  28. package/lib/note-tools.js.map +1 -1
  29. package/lib/notes/store.d.ts +5 -1
  30. package/lib/notes/store.d.ts.map +1 -1
  31. package/lib/notes/store.js +11 -2
  32. package/lib/notes/store.js.map +1 -1
  33. package/lib/provider.d.ts +2 -2
  34. package/lib/provider.d.ts.map +1 -1
  35. package/lib/remote-provider.d.ts +2 -2
  36. package/lib/remote-provider.d.ts.map +1 -1
  37. package/lib/remote-provider.js +6 -5
  38. package/lib/remote-provider.js.map +1 -1
  39. package/lib/tools.js +1 -1
  40. package/lib/tools.js.map +1 -1
  41. package/lib/web.js +1 -1
  42. package/lib/web.js.map +1 -1
  43. package/lib/writeback/control.d.ts +5 -0
  44. package/lib/writeback/control.d.ts.map +1 -0
  45. package/lib/writeback/control.js +39 -0
  46. package/lib/writeback/control.js.map +1 -0
  47. package/lib/writeback/live-control.d.ts +5 -0
  48. package/lib/writeback/live-control.d.ts.map +1 -0
  49. package/lib/writeback/live-control.js +63 -0
  50. package/lib/writeback/live-control.js.map +1 -0
  51. package/lib/writeback/queue.d.ts +27 -1
  52. package/lib/writeback/queue.d.ts.map +1 -1
  53. package/lib/writeback/queue.js +97 -7
  54. package/lib/writeback/queue.js.map +1 -1
  55. package/lib/writeback/status-client.d.ts +3 -0
  56. package/lib/writeback/status-client.d.ts.map +1 -1
  57. package/lib/writeback/status-client.js +30 -4
  58. package/lib/writeback/status-client.js.map +1 -1
  59. package/package.json +1 -1
  60. package/web/app.js +186 -26
  61. package/web/document-sync-ui.js +23 -0
  62. package/web/document-sync.js +43 -0
  63. package/web/styles.css +23 -0
  64. package/web/writeback-live.d.ts +1 -0
  65. package/web/writeback-live.js +52 -0
  66. 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
+ }