@p-dsh-market/conversation-knowledge-map 0.1.11 → 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 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话和过滤数量。
14
+ 摘要阶段按对话并行处理,并发上限固定为 3;同一对话的超长分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、重试、跳过、合并、生成及保存过程。结构化生成关闭推理输出以减少等待和 Token 消耗;知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
15
15
 
16
16
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
17
17
 
package/lib/client.js CHANGED
@@ -261,6 +261,7 @@ window.__ModuleLoader__.load({
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
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,
264
265
  !state || (!state.mindMap && !state.knowledgeGraph) ? React.createElement(EmptyState, { key: 'empty', title: '尚未生成知识视图', copy: '从标题栏打开“知识视图”,选择同工作路径下的对话并确认生成。' }) : (mode === 'mind-map'
265
266
  ? React.createElement(MindMapPanel, { key: 'mind', store: store, selectedNodeId: selectedNodeId, setSelectedNodeId: setSelectedNodeId })
266
267
  : React.createElement(KnowledgeGraphPanel, { key: 'graph', store: store }))
@@ -273,16 +274,34 @@ window.__ModuleLoader__.load({
273
274
  ? manifest.sourceSessions
274
275
  : (manifest.sourceSessionIds || []).map(function (sessionId) { return { sessionId: sessionId, title: '' } })
275
276
  var warnings = manifest.sourceWarnings || {}
277
+ var failedSources = manifest.failedSources || []
276
278
  if (!sources.length) return null
277
279
  return React.createElement('section', { className: 'ckm-source-summary', 'aria-label': '本次总结来源' }, [
278
280
  React.createElement('div', { key: 'head', className: 'ckm-source-summary-head' }, [
279
281
  React.createElement('strong', { key: 'title' }, '已总结 ' + sources.length + ' 个对话'),
280
- warnings.skippedItems || warnings.skippedRefs ? React.createElement('span', { key: 'warning', className: 'ckm-source-warning' }, '已过滤 ' + (warnings.skippedRefs || 0) + ' 个无效引用、跳过 ' + (warnings.skippedItems || 0) + ' 个内容项') : null
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
281
283
  ]),
282
284
  React.createElement('div', { key: 'list', className: 'ckm-source-list' }, sources.map(function (source) {
283
285
  var sessionId = String(source.sessionId || '')
284
286
  var label = source.title ? source.title + ' · ' + sessionId.slice(0, 16) + '…' : sessionId
285
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
+ ])
286
305
  }))
287
306
  ])
288
307
  }
@@ -291,16 +310,6 @@ window.__ModuleLoader__.load({
291
310
  var generation = props.store.generation
292
311
  var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
293
312
  var percent = Math.max(0, Math.min(100, Number(progress.percent) || 0))
294
- var seen = Object.create(null)
295
- var activities = []
296
- ;(generation.events || []).slice().reverse().some(function (event) {
297
- var label = event && event.progress && event.progress.label
298
- if (!label || seen[label]) return false
299
- seen[label] = true
300
- activities.push({ label: label, status: event.status })
301
- return activities.length >= 6
302
- })
303
- activities.reverse()
304
313
  return React.createElement('div', { className: 'ckm-generation-strip', 'data-status': generation.status }, [
305
314
  React.createElement('div', { key: 'body', className: 'ckm-generation-body' }, [
306
315
  React.createElement('div', { key: 'status-row', className: 'ckm-generation-status-row' }, [
@@ -308,12 +317,7 @@ window.__ModuleLoader__.load({
308
317
  React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
309
318
  ]),
310
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 + '%' } })),
311
- activities.length ? React.createElement('div', { key: 'activity', className: 'ckm-progress-activity' }, [
312
- React.createElement('strong', { key: 'title' }, '最近处理动态'),
313
- React.createElement('ul', { key: 'list' }, activities.map(function (activity, index) {
314
- return React.createElement('li', { key: activity.status + '-' + activity.label + '-' + index, 'data-status': activity.status }, activity.label)
315
- }))
316
- ]) : null
320
+ React.createElement(GenerationTimeline, { key: 'activity', items: generation.timeline || [] })
317
321
  ]),
318
322
  generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
319
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) }) } }, '取消')
@@ -608,7 +612,7 @@ window.__ModuleLoader__.load({
608
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}' +
609
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}' +
610
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}' +
611
- '.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-progress-activity{display:grid;grid-template-columns:auto 1fr;gap:6px 12px;color:var(--dsw-alias-label-secondary)}.ckm-progress-activity ul{display:flex;flex-wrap:wrap;gap:5px 12px;margin:0;padding:0;list-style:none}.ckm-progress-activity li:before{content:"·";margin-right:5px;color:var(--dsw-alias-state-business-primary)}.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{color:#d69a42}.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}' +
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}' +
612
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}' +
613
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}' +
614
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()
@@ -533,6 +534,7 @@ export class KnowledgeGenerationOrchestrator {
533
534
  revision: task.revision || 0,
534
535
  progress: task.progress ? clone(task.progress) : null,
535
536
  result: task.result ? clone(task.result) : null,
537
+ timeline: Array.isArray(task.timeline) ? clone(task.timeline) : [],
536
538
  events: task.events.slice(-40)
537
539
  }
538
540
  }
@@ -563,6 +565,11 @@ export class KnowledgeGenerationOrchestrator {
563
565
  }
564
566
  }
565
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
+
566
573
  start(request) {
567
574
  const key = String(request.cwd || '').toLowerCase()
568
575
  if (this.busyByWorkspace.has(key)) throw new Error('当前工作路径已有生成任务在运行,请先取消或等待完成。')
@@ -577,7 +584,9 @@ export class KnowledgeGenerationOrchestrator {
577
584
  createdAt: this.now(),
578
585
  updatedAt: this.now(),
579
586
  nextEventId: 1,
587
+ nextTimelineId: 1,
580
588
  events: [],
589
+ timeline: [],
581
590
  listeners: new Set(),
582
591
  controller: new AbortController(),
583
592
  promise: null
@@ -606,6 +615,7 @@ export class KnowledgeGenerationOrchestrator {
606
615
 
607
616
  async run(task, request) {
608
617
  try {
618
+ this.recordTimeline(task, 'start', `开始处理 ${request.selectedSessionIds.length} 个已选对话。`)
609
619
  this.update(task, 'reading-sources', { progress: { percent: 5, current: 0, total: request.selectedSessionIds.length, label: '读取对话记录' } })
610
620
  const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
611
621
  cwd: request.cwd,
@@ -613,95 +623,158 @@ export class KnowledgeGenerationOrchestrator {
613
623
  fallbackSessionId: request.anchorSessionId,
614
624
  includeSubagents: request.includeSubagents === true
615
625
  })
616
- const sourceSessionIds = [...new Set(sources.map((source) => source.sessionId))]
626
+ const loadedSessionIds = [...new Set(sources.map((source) => source.sessionId))]
617
627
  this.assertNotCancelled(task)
618
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 个并行)。`)
619
630
  this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
620
631
  let completedSources = 0
621
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))
622
- 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 })
623
635
  this.update(task, 'summarizing', {
624
636
  sourceCount: sources.length,
625
637
  progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `并行整理:${source.title}` }
626
638
  })
627
639
  const chunkSummaries = []
628
- 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]
629
644
  this.assertNotCancelled(task)
630
- const prompt = summaryPrompt(source, chunk, request.strict === true)
645
+ const basePrompt = summaryPrompt(source, chunk, request.strict === true)
631
646
  const generateSummary = async (summaryInput) => this.runModel({
632
647
  kind: 'summary', prompt: summaryInput, cwd: request.cwd, strict: request.strict === true,
633
- selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
648
+ selectedSessionIds: loadedSessionIds, model: request.model, signal: task.controller.signal
634
649
  })
635
- try {
636
- const value = await generateSummary(prompt)
637
- chunkSummaries.push(normalizeSummary(value, source, chunk))
638
- } catch (error) {
639
- this.assertNotCancelled(task)
640
- logMessage(this.logger, 'warn', 'summary first attempt invalid id=%s session=%s error=%s', logId(task.id), logId(source.sessionId), errorMessage(error))
641
- this.update(task, 'summarizing', {
642
- message: `对话 ${sourceIndex + 1}/${sources.length} 摘要结构无效,正在自动修复…`,
643
- progress: { percent: 10 + Math.floor(60 * sourceIndex / sources.length), current: sourceIndex, total: sources.length, label: `自动修复:${source.title}` }
644
- })
645
- const retryPrompt = `${prompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新整理,缩短内容,只输出严格合法的顶层摘要 JSON。`
646
- const retryValue = await generateSummary(retryPrompt)
647
- 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
+ }
648
669
  }
670
+ if (finalError) break
649
671
  }
650
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 })
651
683
  this.update(task, 'summarizing', {
652
684
  sourceCount: sources.length,
653
685
  progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已完成摘要:${source.title}` }
654
686
  })
655
- return mergeSourceSummaries(source, chunkSummaries)
687
+ return { source, error: '', summary: mergeSourceSummaries(source, chunkSummaries) }
656
688
  })
657
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} 个失败对话` : ''},开始生成最终视图。`)
658
699
  let mindMap = null
659
700
  let knowledgeGraph = null
660
701
  const sourceWarnings = { skippedRefs: 0, skippedItems: 0 }
661
702
  if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
703
+ this.recordTimeline(task, 'view-start', '开始生成思维导图。')
662
704
  this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
663
- const mindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
705
+ const baseMindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
664
706
  const generateMindMap = async (prompt) => this.runModel({
665
707
  kind: 'mind-map', prompt, cwd: request.cwd, strict: request.strict === true,
666
708
  selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
667
709
  })
668
- try {
669
- const value = await generateMindMap(mindMapPrompt)
670
- const sanitized = sanitizeMindMapSources(value, sources, request.strict === true)
671
- mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
672
- sourceWarnings.skippedRefs += sanitized.skippedRefs
673
- sourceWarnings.skippedItems += sanitized.skippedItems
674
- } catch (error) {
675
- this.assertNotCancelled(task)
676
- logMessage(this.logger, 'warn', 'mind map first attempt invalid id=%s error=%s', logId(task.id), errorMessage(error))
677
- this.update(task, 'building-mind-map', {
678
- message: '首次结构无效,正在自动修复思维导图…',
679
- progress: { percent: 82, current: sources.length, total: sources.length, label: '自动修复结构' }
680
- })
681
- const retryPrompt = `${mindMapPrompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新生成,不要复用上一次文本;只输出完整、严格合法的 JSON。`
682
- const retryValue = await generateMindMap(retryPrompt)
683
- const sanitized = sanitizeMindMapSources(retryValue, sources, request.strict === true)
684
- mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
685
- sourceWarnings.skippedRefs += sanitized.skippedRefs
686
- sourceWarnings.skippedItems += sanitized.skippedItems
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
687
738
  }
688
739
  }
689
740
  if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
741
+ this.recordTimeline(task, 'view-start', '开始生成知识图谱。')
690
742
  this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: sources.length, total: sources.length, label: '生成知识图谱' } })
691
- const value = await this.runModel({
692
- kind: 'knowledge-graph',
693
- prompt: outputPrompt('knowledge-graph', summaries, request.prompt, request.strict === true),
694
- cwd: request.cwd,
695
- strict: request.strict === true,
696
- selectedSessionIds: sourceSessionIds,
697
- model: request.model,
698
- signal: task.controller.signal
699
- })
700
- const sanitized = sanitizeKnowledgeGraphSources(value, sources, request.strict === true)
701
- sourceWarnings.skippedRefs += sanitized.skippedRefs
702
- sourceWarnings.skippedItems += sanitized.skippedItems
703
- knowledgeGraph = validateKnowledgeGraph(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
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
+ }
704
776
  }
777
+ if (!mindMap && !knowledgeGraph) throw new Error('思维导图和知识图谱均未能生成有效结果。')
705
778
  if (sourceWarnings.skippedRefs || sourceWarnings.skippedItems) {
706
779
  logMessage(this.logger, 'warn', 'generation filtered invalid sources id=%s skippedRefs=%d skippedItems=%d', logId(task.id), sourceWarnings.skippedRefs, sourceWarnings.skippedItems)
707
780
  this.update(task, 'validating', {
@@ -709,17 +782,20 @@ export class KnowledgeGenerationOrchestrator {
709
782
  progress: { percent: 90, current: sources.length, total: sources.length, label: '过滤无效来源' }
710
783
  })
711
784
  }
712
- assertOutputSourceRefs({ mindMap, knowledgeGraph }, sources)
785
+ assertOutputSourceRefs({ mindMap, knowledgeGraph }, summarizedSources)
713
786
  this.assertNotCancelled(task)
714
787
  this.update(task, 'validating', { progress: { percent: 92, current: sources.length, total: sources.length, label: '校验结构和来源' } })
788
+ this.recordTimeline(task, 'save', '最终结果已通过校验,正在保存知识视图。')
715
789
  this.update(task, 'saving', { progress: { percent: 97, current: sources.length, total: sources.length, label: '保存知识视图' } })
716
790
  const saved = await this.storage.saveBundle({
717
791
  cwd: request.cwd,
718
792
  expectedRevision: request.expectedRevision,
719
793
  generationId: task.id,
720
794
  sourceSessionIds,
721
- sourceSessions: sources.map((source) => ({ sessionId: source.sessionId, title: source.title })),
795
+ sourceSessions: summarizedSources.map((source) => ({ sessionId: source.sessionId, title: source.title })),
796
+ failedSources,
722
797
  sourceWarnings,
798
+ generationTimeline: task.timeline,
723
799
  prompt: request.prompt,
724
800
  strict: request.strict,
725
801
  outputMode: request.outputMode,
@@ -728,19 +804,22 @@ export class KnowledgeGenerationOrchestrator {
728
804
  knowledgeGraph
729
805
  })
730
806
  task.revision = saved.revision
731
- task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, sourceWarnings }
807
+ task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
808
+ this.recordTimeline(task, 'complete', '知识视图已保存,生成流程完成。')
732
809
  logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
733
810
  this.update(task, 'completed', {
734
- message: `知识视图生成完成,已总结 ${sources.length} 个对话${sourceWarnings.skippedItems ? `,跳过 ${sourceWarnings.skippedItems} 个无有效来源的内容项` : ''}。`,
811
+ message: `知识视图生成完成,已总结 ${summarizedSources.length} 个对话${failedSources.length ? `,跳过 ${failedSources.length} 个失败对话` : ''}${sourceWarnings.skippedItems ? `,过滤 ${sourceWarnings.skippedItems} 个无有效来源的内容项` : ''}。`,
735
812
  revision: saved.revision,
736
813
  result: task.result,
737
814
  progress: { percent: 100, current: sources.length, total: sources.length, label: '合并并生成最终报告完成' }
738
815
  })
739
816
  } catch (error) {
740
817
  if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
818
+ this.recordTimeline(task, 'cancelled', '用户取消了知识视图生成。')
741
819
  this.update(task, 'cancelled', { error: '' })
742
820
  } else {
743
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)}`)
744
823
  this.update(task, 'failed', { error: errorMessage(error) })
745
824
  }
746
825
  }
@@ -151,7 +151,7 @@ export class WorkspaceStorage {
151
151
  }
152
152
  }
153
153
 
154
- async saveBundle({ cwd, expectedRevision = 0, generationId, sourceSessionIds, sourceSessions, sourceWarnings, 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()
@@ -174,6 +174,17 @@ export class WorkspaceStorage {
174
174
  skippedRefs: Math.max(0, Number(sourceWarnings?.skippedRefs) || 0),
175
175
  skippedItems: Math.max(0, Number(sourceWarnings?.skippedItems) || 0)
176
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
+ })) : [],
177
188
  promptSummary: shortText(prompt, 500),
178
189
  strict: strict === true,
179
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.11",
3
+ "version": "0.1.12",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",