@p-dsh-market/conversation-knowledge-map 0.1.10 → 0.1.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 CHANGED
@@ -11,7 +11,7 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
11
11
 
12
12
  配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
13
13
 
14
- 摘要阶段按对话并行处理,并发上限固定为 3;同一对话的超长分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化生成关闭推理输出以减少等待和 Token 消耗;知识图谱最多生成 20 个实体、30 条关系。
14
+ 摘要阶段按对话并行处理,并发上限固定为 3;同一对话的超长分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、重试、跳过、合并、生成及保存过程。结构化生成关闭推理输出以减少等待和 Token 消耗;知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
15
15
 
16
16
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
17
17
 
package/lib/client.js CHANGED
@@ -260,12 +260,52 @@ window.__ModuleLoader__.load({
260
260
  ]),
261
261
  store.generation && store.generation.status !== 'completed' ? React.createElement(GenerationStrip, { key: 'generation', store: store }) : null,
262
262
  state && state.compatibility && !state.compatibility.supported ? React.createElement('div', { key: 'compatibility', className: 'ckm-warning ckm-compatibility-warning' }, state.compatibility.message) : null,
263
+ state && state.manifest ? React.createElement(SourceSummary, { key: 'sources', manifest: state.manifest }) : null,
264
+ state && state.manifest && state.manifest.generationTimeline ? React.createElement(GenerationTimeline, { key: 'timeline', items: state.manifest.generationTimeline }) : null,
263
265
  !state || (!state.mindMap && !state.knowledgeGraph) ? React.createElement(EmptyState, { key: 'empty', title: '尚未生成知识视图', copy: '从标题栏打开“知识视图”,选择同工作路径下的对话并确认生成。' }) : (mode === 'mind-map'
264
266
  ? React.createElement(MindMapPanel, { key: 'mind', store: store, selectedNodeId: selectedNodeId, setSelectedNodeId: setSelectedNodeId })
265
267
  : React.createElement(KnowledgeGraphPanel, { key: 'graph', store: store }))
266
268
  ])
267
269
  }
268
270
 
271
+ function SourceSummary(props) {
272
+ var manifest = props.manifest || {}
273
+ var sources = Array.isArray(manifest.sourceSessions) && manifest.sourceSessions.length
274
+ ? manifest.sourceSessions
275
+ : (manifest.sourceSessionIds || []).map(function (sessionId) { return { sessionId: sessionId, title: '' } })
276
+ var warnings = manifest.sourceWarnings || {}
277
+ var failedSources = manifest.failedSources || []
278
+ if (!sources.length) return null
279
+ return React.createElement('section', { className: 'ckm-source-summary', 'aria-label': '本次总结来源' }, [
280
+ React.createElement('div', { key: 'head', className: 'ckm-source-summary-head' }, [
281
+ React.createElement('strong', { key: 'title' }, '已总结 ' + sources.length + ' 个对话'),
282
+ warnings.skippedItems || warnings.skippedRefs || failedSources.length ? React.createElement('span', { key: 'warning', className: 'ckm-source-warning' }, '失败对话 ' + failedSources.length + ' 个 · 已过滤 ' + (warnings.skippedRefs || 0) + ' 个无效引用、跳过 ' + (warnings.skippedItems || 0) + ' 个内容项') : null
283
+ ]),
284
+ React.createElement('div', { key: 'list', className: 'ckm-source-list' }, sources.map(function (source) {
285
+ var sessionId = String(source.sessionId || '')
286
+ var label = source.title ? source.title + ' · ' + sessionId.slice(0, 16) + '…' : sessionId
287
+ return React.createElement('span', { key: sessionId, className: 'ckm-source-chip', title: sessionId }, label)
288
+ })),
289
+ failedSources.length ? React.createElement('div', { key: 'failed', className: 'ckm-source-failed' }, '未纳入最终报告:' + failedSources.map(function (source) { return source.title || source.sessionId }).join('、')) : null
290
+ ])
291
+ }
292
+
293
+ function GenerationTimeline(props) {
294
+ var items = (props.items || []).slice(-80)
295
+ if (!items.length) return null
296
+ return React.createElement('section', { className: 'ckm-timeline', 'aria-label': '生成过程时间线' }, [
297
+ React.createElement('strong', { key: 'title', className: 'ckm-timeline-title' }, '生成过程时间线'),
298
+ React.createElement('ol', { key: 'items' }, items.map(function (item, index) {
299
+ var time = ''
300
+ try { time = new Date(Number(item.at) || 0).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } catch (_) {}
301
+ return React.createElement('li', { key: String(item.id || index), 'data-type': item.type || 'info' }, [
302
+ React.createElement('time', { key: 'time' }, time),
303
+ React.createElement('span', { key: 'message' }, item.message)
304
+ ])
305
+ }))
306
+ ])
307
+ }
308
+
269
309
  function GenerationStrip(props) {
270
310
  var generation = props.store.generation
271
311
  var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
@@ -276,7 +316,8 @@ window.__ModuleLoader__.load({
276
316
  React.createElement('strong', { key: 'status' }, generation.message || generation.status),
277
317
  React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
278
318
  ]),
279
- React.createElement('div', { key: 'progress', className: 'ckm-progress-track', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': percent, 'aria-label': '知识视图生成进度' }, React.createElement('span', { className: 'ckm-progress-value', style: { width: percent + '%' } }))
319
+ React.createElement('div', { key: 'progress', className: 'ckm-progress-track', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': percent, 'aria-label': '知识视图生成进度' }, React.createElement('span', { className: 'ckm-progress-value', style: { width: percent + '%' } })),
320
+ React.createElement(GenerationTimeline, { key: 'activity', items: generation.timeline || [] })
280
321
  ]),
281
322
  generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
282
323
  ['failed', 'cancelled'].includes(generation.status) ? null : Button({ key: 'cancel', className: 'ckm-danger', onClick: function () { request('/generations/' + encodeURIComponent(generation.id) + '/cancel', { method: 'POST' }).then(function (body) { applyGeneration(props.store, body.generation) }).catch(function (error) { props.store.error = error.message; notifyStore(props.store) }) } }, '取消')
@@ -571,7 +612,7 @@ window.__ModuleLoader__.load({
571
612
  '.ckm-page-header h2{margin:0;font-size:16px}.ckm-page-header p{margin:3px 0 0;color:var(--dsw-alias-label-secondary);font-size:11px;word-break:break-all}' +
572
613
  '.ckm-page-actions,.ckm-modal-actions{display:flex;align-items:center;gap:8px}.ckm-tab,.ckm-secondary,.ckm-primary,.ckm-danger,.ckm-header-action{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 11px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;font-size:12px}.ckm-tab[data-active=true],.ckm-primary{border-color:var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}.ckm-tab:hover,.ckm-secondary:hover,.ckm-header-action:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-primary:hover{background:var(--dsw-alias-button-info-hover)}.ckm-danger{border-color:#d36b6b;color:#ffb7b7}.ckm-danger:hover{background:#d94a4a22}.ckm-page button:disabled,.ckm-modal button:disabled{opacity:.5;cursor:not-allowed}' +
573
614
  '.ckm-empty{display:flex;flex:1;min-height:260px;flex-direction:column;align-items:center;justify-content:center;padding:28px;text-align:center}.ckm-empty-icon{display:grid;place-items:center;width:44px;height:44px;margin-bottom:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;color:var(--dsw-alias-state-business-primary);font-size:24px}.ckm-empty h2{margin:0 0 8px;font-size:17px}.ckm-empty p{max-width:520px;margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.7}' +
574
- '.ckm-generation-strip{display:flex;align-items:center;gap:12px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-state-business-tertiary);font-size:12px}.ckm-generation-body{display:grid;gap:7px;flex:1;min-width:0}.ckm-generation-status-row{display:flex;justify-content:space-between;gap:12px}.ckm-generation-status-row span{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ckm-progress-track{height:6px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.ckm-progress-value{display:block;height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.ckm-generation-error{color:#ff9898;max-width:38%;overflow-wrap:anywhere}.ckm-generation-strip .ckm-danger{margin-left:auto}' +
615
+ '.ckm-generation-strip{display:flex;align-items:flex-start;gap:12px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-state-business-tertiary);font-size:12px}.ckm-generation-body{display:grid;gap:7px;flex:1;min-width:0}.ckm-generation-status-row{display:flex;justify-content:space-between;gap:12px}.ckm-generation-status-row span{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ckm-progress-track{height:6px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.ckm-progress-value{display:block;height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.ckm-generation-error{color:#ff9898;max-width:38%;overflow-wrap:anywhere}.ckm-generation-strip .ckm-danger{margin-left:auto}.ckm-source-summary{display:grid;gap:8px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}.ckm-source-summary-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px}.ckm-source-warning,.ckm-source-failed{color:#d69a42}.ckm-source-failed{font-size:11px}.ckm-source-list{display:flex;flex-wrap:wrap;gap:6px}.ckm-source-chip{max-width:360px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-secondary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ckm-timeline{display:grid;gap:9px;padding:12px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);font-size:12px}.ckm-generation-strip .ckm-timeline{max-height:220px;overflow:auto;padding:6px 0 0;border:0}.ckm-timeline-title{color:var(--dsw-alias-label-primary)}.ckm-timeline ol{display:grid;gap:0;margin:0;padding:0;list-style:none}.ckm-timeline li{position:relative;display:grid;grid-template-columns:78px 1fr;gap:10px;padding:5px 0 5px 14px;color:var(--dsw-alias-label-secondary)}.ckm-timeline li:before{content:"";position:absolute;left:2px;top:11px;width:6px;height:6px;border-radius:50%;background:var(--dsw-alias-state-business-primary)}.ckm-timeline li:not(:last-child):after{content:"";position:absolute;left:4px;top:17px;bottom:-5px;width:1px;background:var(--dsw-alias-border-l2)}.ckm-timeline li[data-type="retry"]:before,.ckm-timeline li[data-type="skipped"]:before,.ckm-timeline li[data-type="view-failed"]:before{background:#d69a42}.ckm-timeline li[data-type="failed"]:before{background:#ff7070}.ckm-timeline time{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}' +
575
616
  '.ckm-workspace{display:grid;grid-template-columns:minmax(0,1fr) 330px;min-height:0;flex:1}.ckm-mind-canvas,.ckm-graph-canvas{min-width:0;min-height:0;overflow:auto;padding:20px;background:var(--dsw-alias-bg-base)}.ckm-detail{min-width:0;min-height:0;overflow:auto;padding:20px;border-left:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}.ckm-panel-hint{margin:0 0 14px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.6}.ckm-tree{display:flex;flex-direction:column;gap:8px;max-width:900px;margin:0 auto}.ckm-tree-node{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:calc(100% - 0px);padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer}.ckm-tree-node[data-active=true]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}.ckm-tree-node:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-tree-node strong{font-size:13px}.ckm-tree-node span:last-child{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.65}.ckm-node-type{display:inline-block;color:var(--dsw-alias-state-business-primary);font-size:10px;letter-spacing:.04em;text-transform:uppercase}.ckm-detail-head{display:flex;flex-direction:column;gap:5px}.ckm-detail h3{margin:0;font-size:16px;line-height:1.45}.ckm-narrative{font-size:13px;line-height:1.8}.ckm-source-box{margin:16px 0;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-base);font-size:11px;line-height:1.6}.ckm-source-box strong{display:block;margin-bottom:5px}.ckm-source-box ul{margin:0;padding-left:18px;color:var(--dsw-alias-label-secondary)}.ckm-detail-empty{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.7}' +
576
617
  '.ckm-graph-layout{grid-template-columns:minmax(0,1fr) 330px}.ckm-graph-toolbar{display:flex;gap:8px}.ckm-model-selectors{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.4fr);gap:8px}.ckm-graph-toolbar input,.ckm-graph-toolbar select,.ckm-field textarea,.ckm-field select,.ckm-field input,.ckm-modal textarea,.ckm-modal select{box-sizing:border-box;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:8px 9px;background:var(--dsw-specific-input-major);color:var(--dsw-alias-label-primary);font:inherit;font-size:12px}.ckm-graph-toolbar input{flex:1}.ckm-graph-svg{display:block;width:100%;min-height:420px;margin-top:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-1)}.ckm-edge{stroke:var(--dsw-alias-border-l2);stroke-width:1.5}.ckm-graph-node{cursor:pointer}.ckm-graph-node circle{fill:var(--dsw-alias-state-business-tertiary);stroke:var(--dsw-alias-state-business-primary);stroke-width:1.5}.ckm-graph-node[data-active=true] circle{fill:var(--dsw-alias-button-info-fill);stroke:var(--dsw-alias-label-primary)}.ckm-graph-node text{fill:var(--dsw-alias-label-primary);font-size:11px}' +
577
618
  '.ckm-modal-backdrop{position:fixed;inset:0;z-index:120;display:flex;align-items:center;justify-content:center;padding:20px;background:rgba(0,0,0,.46)}.ckm-modal{width:min(680px,calc(100vw - 40px));max-height:min(760px,calc(100vh - 40px));overflow:auto;padding:22px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;box-shadow:0 18px 56px #0008}.ckm-modal h3{margin:0;font-size:17px}.ckm-modal p{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.65}.ckm-modal-head{display:flex;align-items:center;justify-content:space-between}.ckm-icon-close{border:0;background:transparent;color:var(--dsw-alias-label-secondary);font-size:20px;cursor:pointer}.ckm-workspace-label{padding:9px;border-radius:8px;background:var(--dsw-alias-bg-layer-1);word-break:break-all}.ckm-field{display:flex;flex-direction:column;gap:7px;margin:14px 0;color:var(--dsw-alias-label-secondary);font-size:12px}.ckm-field textarea{resize:vertical}.ckm-session-list{display:flex;max-height:220px;flex-direction:column;gap:5px;overflow:auto}.ckm-session-option{display:flex;align-items:flex-start;gap:8px;padding:8px;border:1px solid transparent;border-radius:8px;background:var(--dsw-alias-bg-layer-1);cursor:pointer}.ckm-session-option:hover{border-color:var(--dsw-alias-border-l2)}.ckm-session-option input,.ckm-inline-field input{margin-top:3px}.ckm-session-option span{display:flex;flex-direction:column;gap:3px}.ckm-session-option small{color:var(--dsw-alias-label-secondary);font-size:10px}.ckm-inline-field{display:flex;align-items:flex-start;gap:7px;margin:10px 0;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:1.5}.ckm-confirm-summary{display:grid;grid-template-columns:100px 1fr;gap:7px 12px;margin:18px 0;font-size:12px}.ckm-confirm-summary dt{color:var(--dsw-alias-label-secondary)}.ckm-confirm-summary dd{margin:0;word-break:break-all}.ckm-warning{padding:10px;border-radius:8px;background:#d29c2518;color:var(--dsw-alias-label-secondary)}.ckm-error{color:#ff9898!important}.ckm-modal-actions{justify-content:flex-end;margin-top:18px}.ckm-header-pending{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}' +
@@ -9,6 +9,7 @@ const MAX_SUMMARY_CHARS = 2400
9
9
  const SUMMARY_MAX_TOKENS = 8000
10
10
  const VIEW_MAX_TOKENS = 12000
11
11
  const SUMMARY_CONCURRENCY = 3
12
+ const MAX_MODEL_RETRIES = 3
12
13
 
13
14
  function modelLabel(selection) {
14
15
  const provider = String(selection?.provider || '').trim()
@@ -198,6 +199,94 @@ async function mapWithConcurrency(items, limit, mapper) {
198
199
  return results
199
200
  }
200
201
 
202
+ function filterSourceRefs(refs, sources) {
203
+ const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
204
+ let skipped = 0
205
+ const filtered = []
206
+ for (const ref of Array.isArray(refs) ? refs : []) {
207
+ const sessionId = String(ref?.sessionId || '')
208
+ const allowedSeqs = allowed.get(sessionId)
209
+ if (!allowedSeqs) {
210
+ skipped += 1
211
+ continue
212
+ }
213
+ const eventSeqs = [...new Set((Array.isArray(ref?.eventSeqs) ? ref.eventSeqs : []).filter((seq) => Number.isInteger(seq) && allowedSeqs.has(seq)))]
214
+ if (!eventSeqs.length) {
215
+ skipped += 1
216
+ continue
217
+ }
218
+ filtered.push({ sessionId, eventSeqs })
219
+ }
220
+ return { refs: filtered.slice(0, 12), skipped }
221
+ }
222
+
223
+ function sanitizeMindMapSources(value, sources, strict) {
224
+ const rawNodes = Array.isArray(value?.nodes) ? value.nodes : []
225
+ const candidates = rawNodes.map((raw, index) => ({ ...raw, id: String(raw?.id || `mind-node-${index + 1}`) }))
226
+ const originalById = new Map(candidates.map((node) => [node.id, node]))
227
+ let skippedRefs = 0
228
+ let skippedItems = 0
229
+ let nodes = candidates.map((node) => {
230
+ const filtered = filterSourceRefs(node.sourceRefs, sources)
231
+ skippedRefs += filtered.skipped
232
+ const primary = filtered.refs.some((ref) => ref.sessionId === node.primarySourceSessionId)
233
+ ? node.primarySourceSessionId
234
+ : (filtered.refs[0]?.sessionId || '')
235
+ return { ...node, sourceRefs: filtered.refs, primarySourceSessionId: primary }
236
+ })
237
+ if (strict) {
238
+ const before = nodes.length
239
+ nodes = nodes.filter((node) => node.sourceRefs.length)
240
+ skippedItems += before - nodes.length
241
+ }
242
+ if (!nodes.length) return { value: { ...value, nodes }, skippedRefs, skippedItems }
243
+ const surviving = new Set(nodes.map((node) => node.id))
244
+ let rootId = surviving.has(String(value?.rootId || '')) ? String(value.rootId) : nodes[0].id
245
+ const nearestParent = (node) => {
246
+ let parentId = node.parentId === null || node.parentId === undefined ? '' : String(node.parentId)
247
+ const visited = new Set([node.id])
248
+ while (parentId && !surviving.has(parentId) && !visited.has(parentId)) {
249
+ visited.add(parentId)
250
+ const parent = originalById.get(parentId)
251
+ parentId = parent?.parentId === null || parent?.parentId === undefined ? '' : String(parent.parentId)
252
+ }
253
+ return surviving.has(parentId) ? parentId : null
254
+ }
255
+ nodes = nodes.map((node) => {
256
+ if (node.id === rootId) return { ...node, parentId: null }
257
+ const parentId = nearestParent(node)
258
+ return { ...node, parentId: parentId && parentId !== node.id ? parentId : rootId }
259
+ })
260
+ return { value: { ...value, rootId, nodes }, skippedRefs, skippedItems }
261
+ }
262
+
263
+ function sanitizeKnowledgeGraphSources(value, sources, strict) {
264
+ let skippedRefs = 0
265
+ let skippedItems = 0
266
+ let entities = (Array.isArray(value?.entities) ? value.entities : []).map((entity) => {
267
+ const filtered = filterSourceRefs(entity?.sourceRefs, sources)
268
+ skippedRefs += filtered.skipped
269
+ return { ...entity, sourceRefs: filtered.refs }
270
+ })
271
+ if (strict) {
272
+ const before = entities.length
273
+ entities = entities.filter((entity) => entity.sourceRefs.length)
274
+ skippedItems += before - entities.length
275
+ }
276
+ const entityIds = new Set(entities.map((entity) => String(entity?.id || '')))
277
+ let relations = []
278
+ for (const relation of Array.isArray(value?.relations) ? value.relations : []) {
279
+ const filtered = filterSourceRefs(relation?.evidence, sources)
280
+ skippedRefs += filtered.skipped
281
+ if (!entityIds.has(String(relation?.from || '')) || !entityIds.has(String(relation?.to || '')) || (strict && !filtered.refs.length)) {
282
+ skippedItems += 1
283
+ continue
284
+ }
285
+ relations.push({ ...relation, evidence: filtered.refs })
286
+ }
287
+ return { value: { ...value, entities, relations }, skippedRefs, skippedItems }
288
+ }
289
+
201
290
  function assertOutputSourceRefs(value, sources) {
202
291
  const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
203
292
  const check = (ref, label) => {
@@ -445,6 +534,7 @@ export class KnowledgeGenerationOrchestrator {
445
534
  revision: task.revision || 0,
446
535
  progress: task.progress ? clone(task.progress) : null,
447
536
  result: task.result ? clone(task.result) : null,
537
+ timeline: Array.isArray(task.timeline) ? clone(task.timeline) : [],
448
538
  events: task.events.slice(-40)
449
539
  }
450
540
  }
@@ -475,6 +565,11 @@ export class KnowledgeGenerationOrchestrator {
475
565
  }
476
566
  }
477
567
 
568
+ recordTimeline(task, type, message, details = {}) {
569
+ task.timeline.push({ id: task.nextTimelineId++, at: this.now(), type, message: shortText(message, 500), ...details })
570
+ if (task.timeline.length > 80) task.timeline.splice(0, task.timeline.length - 80)
571
+ }
572
+
478
573
  start(request) {
479
574
  const key = String(request.cwd || '').toLowerCase()
480
575
  if (this.busyByWorkspace.has(key)) throw new Error('当前工作路径已有生成任务在运行,请先取消或等待完成。')
@@ -489,7 +584,9 @@ export class KnowledgeGenerationOrchestrator {
489
584
  createdAt: this.now(),
490
585
  updatedAt: this.now(),
491
586
  nextEventId: 1,
587
+ nextTimelineId: 1,
492
588
  events: [],
589
+ timeline: [],
493
590
  listeners: new Set(),
494
591
  controller: new AbortController(),
495
592
  promise: null
@@ -518,6 +615,7 @@ export class KnowledgeGenerationOrchestrator {
518
615
 
519
616
  async run(task, request) {
520
617
  try {
618
+ this.recordTimeline(task, 'start', `开始处理 ${request.selectedSessionIds.length} 个已选对话。`)
521
619
  this.update(task, 'reading-sources', { progress: { percent: 5, current: 0, total: request.selectedSessionIds.length, label: '读取对话记录' } })
522
620
  const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
523
621
  cwd: request.cwd,
@@ -525,94 +623,179 @@ export class KnowledgeGenerationOrchestrator {
525
623
  fallbackSessionId: request.anchorSessionId,
526
624
  includeSubagents: request.includeSubagents === true
527
625
  })
528
- const sourceSessionIds = [...new Set(sources.map((source) => source.sessionId))]
626
+ const loadedSessionIds = [...new Set(sources.map((source) => source.sessionId))]
529
627
  this.assertNotCancelled(task)
530
628
  logMessage(this.logger, 'info', 'generation sources loaded id=%s sources=%d events=%d', logId(task.id), sources.length, sources.reduce((total, source) => total + (Array.isArray(source.events) ? source.events.length : 0), 0))
629
+ this.recordTimeline(task, 'read', `已读取 ${sources.length} 个对话,开始并行摘要(最多 3 个并行)。`)
531
630
  this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
532
631
  let completedSources = 0
533
632
  logMessage(this.logger, 'info', 'summary batch start id=%s sources=%d concurrency=%d', logId(task.id), sources.length, Math.min(SUMMARY_CONCURRENCY, sources.length))
534
- const summaries = await mapWithConcurrency(sources, SUMMARY_CONCURRENCY, async (source, sourceIndex) => {
633
+ const summaryResults = await mapWithConcurrency(sources, SUMMARY_CONCURRENCY, async (source, sourceIndex) => {
634
+ this.recordTimeline(task, 'summary-start', `开始摘要:${source.title}`, { sessionId: source.sessionId })
535
635
  this.update(task, 'summarizing', {
536
636
  sourceCount: sources.length,
537
637
  progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `并行整理:${source.title}` }
538
638
  })
539
639
  const chunkSummaries = []
540
- for (const chunk of chunkSourceText(source)) {
640
+ const chunks = chunkSourceText(source)
641
+ let finalError = null
642
+ for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
643
+ const chunk = chunks[chunkIndex]
541
644
  this.assertNotCancelled(task)
542
- const prompt = summaryPrompt(source, chunk, request.strict === true)
645
+ const basePrompt = summaryPrompt(source, chunk, request.strict === true)
543
646
  const generateSummary = async (summaryInput) => this.runModel({
544
647
  kind: 'summary', prompt: summaryInput, cwd: request.cwd, strict: request.strict === true,
545
- selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
648
+ selectedSessionIds: loadedSessionIds, model: request.model, signal: task.controller.signal
546
649
  })
547
- try {
548
- const value = await generateSummary(prompt)
549
- chunkSummaries.push(normalizeSummary(value, source, chunk))
550
- } catch (error) {
551
- this.assertNotCancelled(task)
552
- logMessage(this.logger, 'warn', 'summary first attempt invalid id=%s session=%s error=%s', logId(task.id), logId(source.sessionId), errorMessage(error))
553
- this.update(task, 'summarizing', {
554
- message: `对话 ${sourceIndex + 1}/${sources.length} 摘要结构无效,正在自动修复…`,
555
- progress: { percent: 10 + Math.floor(60 * sourceIndex / sources.length), current: sourceIndex, total: sources.length, label: `自动修复:${source.title}` }
556
- })
557
- const retryPrompt = `${prompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新整理,缩短内容,只输出严格合法的顶层摘要 JSON。`
558
- const retryValue = await generateSummary(retryPrompt)
559
- chunkSummaries.push(normalizeSummary(retryValue, source, chunk))
650
+ for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
651
+ try {
652
+ const prompt = attempt === 0 ? basePrompt : `${basePrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(finalError), 500)}。请完全重置并从头生成,进一步缩短内容,只输出严格合法的顶层摘要 JSON。`
653
+ const value = await generateSummary(prompt)
654
+ chunkSummaries.push(normalizeSummary(value, source, chunk))
655
+ finalError = null
656
+ break
657
+ } catch (error) {
658
+ this.assertNotCancelled(task)
659
+ finalError = error
660
+ logMessage(this.logger, 'warn', 'summary attempt invalid id=%s session=%s chunk=%d attempt=%d error=%s', logId(task.id), logId(source.sessionId), chunkIndex + 1, attempt + 1, errorMessage(error))
661
+ if (attempt < MAX_MODEL_RETRIES) {
662
+ this.recordTimeline(task, 'retry', `摘要失败,重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}:${source.title}`, { sessionId: source.sessionId })
663
+ this.update(task, 'summarizing', {
664
+ message: `对话 ${sourceIndex + 1}/${sources.length} 摘要无效,正在重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}…`,
665
+ progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `重试 ${attempt + 1}/${MAX_MODEL_RETRIES}:${source.title}` }
666
+ })
667
+ }
668
+ }
560
669
  }
670
+ if (finalError) break
561
671
  }
562
672
  completedSources += 1
673
+ if (finalError) {
674
+ this.recordTimeline(task, 'skipped', `连续重试 ${MAX_MODEL_RETRIES} 次后仍失败,已跳过:${source.title}`, { sessionId: source.sessionId })
675
+ this.update(task, 'summarizing', {
676
+ message: `已跳过无法生成合法摘要的对话:${source.title}`,
677
+ sourceCount: sources.length,
678
+ progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已跳过:${source.title}` }
679
+ })
680
+ return { source, error: errorMessage(finalError), summary: null }
681
+ }
682
+ this.recordTimeline(task, 'summary-complete', `摘要完成:${source.title}`, { sessionId: source.sessionId })
563
683
  this.update(task, 'summarizing', {
564
684
  sourceCount: sources.length,
565
- progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: source.title }
685
+ progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已完成摘要:${source.title}` }
566
686
  })
567
- return mergeSourceSummaries(source, chunkSummaries)
687
+ return { source, error: '', summary: mergeSourceSummaries(source, chunkSummaries) }
568
688
  })
569
689
  this.assertNotCancelled(task)
690
+ const successfulResults = summaryResults.filter((result) => result?.summary)
691
+ const failedSources = summaryResults.filter((result) => !result?.summary).map((result) => ({
692
+ sessionId: result.source.sessionId, title: result.source.title, error: result.error
693
+ }))
694
+ if (!successfulResults.length) throw new Error(`所选 ${sources.length} 个对话均未能生成合法摘要,已分别重试 ${MAX_MODEL_RETRIES} 次。`)
695
+ const summarizedSources = successfulResults.map((result) => result.source)
696
+ const summaries = successfulResults.map((result) => result.summary)
697
+ const sourceSessionIds = summarizedSources.map((source) => source.sessionId)
698
+ this.recordTimeline(task, 'merge', `已合并 ${summaries.length} 个对话摘要${failedSources.length ? `,跳过 ${failedSources.length} 个失败对话` : ''},开始生成最终视图。`)
570
699
  let mindMap = null
571
700
  let knowledgeGraph = null
701
+ const sourceWarnings = { skippedRefs: 0, skippedItems: 0 }
572
702
  if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
703
+ this.recordTimeline(task, 'view-start', '开始生成思维导图。')
573
704
  this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
574
- const mindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
705
+ const baseMindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
575
706
  const generateMindMap = async (prompt) => this.runModel({
576
707
  kind: 'mind-map', prompt, cwd: request.cwd, strict: request.strict === true,
577
708
  selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
578
709
  })
579
- try {
580
- const value = await generateMindMap(mindMapPrompt)
581
- mindMap = validateMindMap(value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
582
- } catch (error) {
583
- this.assertNotCancelled(task)
584
- logMessage(this.logger, 'warn', 'mind map first attempt invalid id=%s error=%s', logId(task.id), errorMessage(error))
585
- this.update(task, 'building-mind-map', {
586
- message: '首次结构无效,正在自动修复思维导图…',
587
- progress: { percent: 82, current: sources.length, total: sources.length, label: '自动修复结构' }
588
- })
589
- const retryPrompt = `${mindMapPrompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新生成,不要复用上一次文本;只输出完整、严格合法的 JSON。`
590
- const retryValue = await generateMindMap(retryPrompt)
591
- mindMap = validateMindMap(retryValue, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
710
+ let mindMapError = null
711
+ for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
712
+ try {
713
+ const prompt = attempt === 0 ? baseMindMapPrompt : `${baseMindMapPrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(mindMapError), 500)}。请完全重置并从头生成,只输出完整、严格合法且更精简的 JSON。`
714
+ const value = await generateMindMap(prompt)
715
+ const sanitized = sanitizeMindMapSources(value, summarizedSources, request.strict === true)
716
+ mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
717
+ sourceWarnings.skippedRefs += sanitized.skippedRefs
718
+ sourceWarnings.skippedItems += sanitized.skippedItems
719
+ mindMapError = null
720
+ this.recordTimeline(task, 'view-complete', '思维导图生成完成。')
721
+ break
722
+ } catch (error) {
723
+ this.assertNotCancelled(task)
724
+ mindMapError = error
725
+ logMessage(this.logger, 'warn', 'mind map attempt invalid id=%s attempt=%d error=%s', logId(task.id), attempt + 1, errorMessage(error))
726
+ if (attempt < MAX_MODEL_RETRIES) {
727
+ this.recordTimeline(task, 'retry', `思维导图结构无效,重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}。`)
728
+ this.update(task, 'building-mind-map', {
729
+ message: `思维导图结构无效,正在重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}…`,
730
+ progress: { percent: 82, current: sources.length, total: sources.length, label: `思维导图重试 ${attempt + 1}/${MAX_MODEL_RETRIES}` }
731
+ })
732
+ }
733
+ }
734
+ }
735
+ if (mindMapError) {
736
+ this.recordTimeline(task, 'view-failed', `思维导图连续重试 ${MAX_MODEL_RETRIES} 次后仍失败,已跳过该视图。`)
737
+ if (request.outputMode === 'mind-map') throw mindMapError
592
738
  }
593
739
  }
594
740
  if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
741
+ this.recordTimeline(task, 'view-start', '开始生成知识图谱。')
595
742
  this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: sources.length, total: sources.length, label: '生成知识图谱' } })
596
- const value = await this.runModel({
597
- kind: 'knowledge-graph',
598
- prompt: outputPrompt('knowledge-graph', summaries, request.prompt, request.strict === true),
599
- cwd: request.cwd,
600
- strict: request.strict === true,
601
- selectedSessionIds: sourceSessionIds,
602
- model: request.model,
603
- signal: task.controller.signal
743
+ const baseGraphPrompt = outputPrompt('knowledge-graph', summaries, request.prompt, request.strict === true)
744
+ let graphError = null
745
+ for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
746
+ try {
747
+ const prompt = attempt === 0 ? baseGraphPrompt : `${baseGraphPrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(graphError), 500)}。请完全重置并从头生成,只输出完整、严格合法且更精简的 JSON。`
748
+ const value = await this.runModel({
749
+ kind: 'knowledge-graph', prompt, cwd: request.cwd, strict: request.strict === true,
750
+ selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
751
+ })
752
+ const sanitized = sanitizeKnowledgeGraphSources(value, summarizedSources, request.strict === true)
753
+ knowledgeGraph = validateKnowledgeGraph(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
754
+ sourceWarnings.skippedRefs += sanitized.skippedRefs
755
+ sourceWarnings.skippedItems += sanitized.skippedItems
756
+ graphError = null
757
+ this.recordTimeline(task, 'view-complete', '知识图谱生成完成。')
758
+ break
759
+ } catch (error) {
760
+ this.assertNotCancelled(task)
761
+ graphError = error
762
+ logMessage(this.logger, 'warn', 'knowledge graph attempt invalid id=%s attempt=%d error=%s', logId(task.id), attempt + 1, errorMessage(error))
763
+ if (attempt < MAX_MODEL_RETRIES) {
764
+ this.recordTimeline(task, 'retry', `知识图谱结构无效,重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}。`)
765
+ this.update(task, 'building-knowledge-graph', {
766
+ message: `知识图谱结构无效,正在重置重试 ${attempt + 1}/${MAX_MODEL_RETRIES}…`,
767
+ progress: { percent: 88, current: sources.length, total: sources.length, label: `知识图谱重试 ${attempt + 1}/${MAX_MODEL_RETRIES}` }
768
+ })
769
+ }
770
+ }
771
+ }
772
+ if (graphError) {
773
+ this.recordTimeline(task, 'view-failed', `知识图谱连续重试 ${MAX_MODEL_RETRIES} 次后仍失败,已跳过该视图。`)
774
+ if (request.outputMode === 'knowledge-graph') throw graphError
775
+ }
776
+ }
777
+ if (!mindMap && !knowledgeGraph) throw new Error('思维导图和知识图谱均未能生成有效结果。')
778
+ if (sourceWarnings.skippedRefs || sourceWarnings.skippedItems) {
779
+ logMessage(this.logger, 'warn', 'generation filtered invalid sources id=%s skippedRefs=%d skippedItems=%d', logId(task.id), sourceWarnings.skippedRefs, sourceWarnings.skippedItems)
780
+ this.update(task, 'validating', {
781
+ message: `已跳过 ${sourceWarnings.skippedRefs} 个无效来源引用、${sourceWarnings.skippedItems} 个无有效来源的内容项。`,
782
+ progress: { percent: 90, current: sources.length, total: sources.length, label: '过滤无效来源' }
604
783
  })
605
- knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
606
784
  }
607
- assertOutputSourceRefs({ mindMap, knowledgeGraph }, sources)
785
+ assertOutputSourceRefs({ mindMap, knowledgeGraph }, summarizedSources)
608
786
  this.assertNotCancelled(task)
609
787
  this.update(task, 'validating', { progress: { percent: 92, current: sources.length, total: sources.length, label: '校验结构和来源' } })
788
+ this.recordTimeline(task, 'save', '最终结果已通过校验,正在保存知识视图。')
610
789
  this.update(task, 'saving', { progress: { percent: 97, current: sources.length, total: sources.length, label: '保存知识视图' } })
611
790
  const saved = await this.storage.saveBundle({
612
791
  cwd: request.cwd,
613
792
  expectedRevision: request.expectedRevision,
614
793
  generationId: task.id,
615
794
  sourceSessionIds,
795
+ sourceSessions: summarizedSources.map((source) => ({ sessionId: source.sessionId, title: source.title })),
796
+ failedSources,
797
+ sourceWarnings,
798
+ generationTimeline: task.timeline,
616
799
  prompt: request.prompt,
617
800
  strict: request.strict,
618
801
  outputMode: request.outputMode,
@@ -621,14 +804,22 @@ export class KnowledgeGenerationOrchestrator {
621
804
  knowledgeGraph
622
805
  })
623
806
  task.revision = saved.revision
624
- task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph }
807
+ task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
808
+ this.recordTimeline(task, 'complete', '知识视图已保存,生成流程完成。')
625
809
  logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
626
- this.update(task, 'completed', { revision: saved.revision, result: task.result, progress: { percent: 100, current: sources.length, total: sources.length, label: '生成完成' } })
810
+ this.update(task, 'completed', {
811
+ message: `知识视图生成完成,已总结 ${summarizedSources.length} 个对话${failedSources.length ? `,跳过 ${failedSources.length} 个失败对话` : ''}${sourceWarnings.skippedItems ? `,过滤 ${sourceWarnings.skippedItems} 个无有效来源的内容项` : ''}。`,
812
+ revision: saved.revision,
813
+ result: task.result,
814
+ progress: { percent: 100, current: sources.length, total: sources.length, label: '合并并生成最终报告完成' }
815
+ })
627
816
  } catch (error) {
628
817
  if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
818
+ this.recordTimeline(task, 'cancelled', '用户取消了知识视图生成。')
629
819
  this.update(task, 'cancelled', { error: '' })
630
820
  } else {
631
821
  logMessage(this.logger, 'error', 'generation failed id=%s status=%s error=%s', logId(task.id), task.status, errorMessage(error))
822
+ this.recordTimeline(task, 'failed', `生成失败:${errorMessage(error)}`)
632
823
  this.update(task, 'failed', { error: errorMessage(error) })
633
824
  }
634
825
  }
@@ -151,7 +151,7 @@ export class WorkspaceStorage {
151
151
  }
152
152
  }
153
153
 
154
- async saveBundle({ cwd, expectedRevision = 0, generationId, sourceSessionIds, prompt, strict, outputMode, model, mindMap, knowledgeGraph }) {
154
+ async saveBundle({ cwd, expectedRevision = 0, generationId, sourceSessionIds, sourceSessions, failedSources, sourceWarnings, generationTimeline, prompt, strict, outputMode, model, mindMap, knowledgeGraph }) {
155
155
  const normalizedCwd = normalizeWorkspacePath(cwd)
156
156
  const key = workspaceKey(normalizedCwd)
157
157
  const previous = this.locks.get(key) || Promise.resolve()
@@ -166,6 +166,25 @@ export class WorkspaceStorage {
166
166
  revision,
167
167
  cwd: normalizedCwd,
168
168
  sourceSessionIds: [...new Set((sourceSessionIds || []).map(String))],
169
+ sourceSessions: Array.isArray(sourceSessions) ? sourceSessions.map((source) => ({
170
+ sessionId: String(source?.sessionId || ''),
171
+ title: shortText(source?.title, 200)
172
+ })).filter((source) => source.sessionId) : [],
173
+ sourceWarnings: {
174
+ skippedRefs: Math.max(0, Number(sourceWarnings?.skippedRefs) || 0),
175
+ skippedItems: Math.max(0, Number(sourceWarnings?.skippedItems) || 0)
176
+ },
177
+ failedSources: Array.isArray(failedSources) ? failedSources.map((source) => ({
178
+ sessionId: String(source?.sessionId || ''),
179
+ title: shortText(source?.title, 200),
180
+ error: shortText(source?.error, 500)
181
+ })).filter((source) => source.sessionId) : [],
182
+ generationTimeline: Array.isArray(generationTimeline) ? generationTimeline.slice(-80).map((item, index) => ({
183
+ id: Number(item?.id) || index + 1,
184
+ at: Number(item?.at) || this.now(),
185
+ type: shortText(item?.type, 40) || 'info',
186
+ message: shortText(item?.message, 500)
187
+ })) : [],
169
188
  promptSummary: shortText(prompt, 500),
170
189
  strict: strict === true,
171
190
  outputMode: String(outputMode || 'both'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/conversation-knowledge-map",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",