@p-dsh-market/conversation-knowledge-map 0.1.17 → 0.1.19

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
@@ -13,7 +13,7 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
13
13
 
14
14
  摘要阶段按对话并行处理,并发上限固定为 3;每个对话按约 5000 字、优先在完整段落边界分段,只有单段本身超长时才按句末或硬边界拆分。同一对话的分段保持顺序处理。思维导图使用合并后的会话摘要;知识图谱直接使用保留来源的分段摘要,避免长对话在二次压缩后丢失实体。知识图谱按证据分段多次生成,最多 2 批并行;单批限制在 10–18 个实体、最多 24 条关系,若达到输出 Token 上限则只降低并重试当前批次。所有成功批次由 Host 按实体类型与名称、关系起点/终点/类型确定性去重并合并来源,最终结果再根据已读取文本量和有效分段数裁剪为 30–100 个实体、最多 200 条关系。单个批次连续失败只跳过该批,不会推翻其他成功批次。结构化输出失败时会完全重置并最多重试 3 次;单个对话摘要连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、具体失败原因、重试、跳过、合并、生成及保存过程;知识图谱时间线还会显示批次数、模型返回数量和合并去重后的保留数量。模型可以保留 thinking,但结果提取只读取最终文本并过滤 reasoning 内容;摘要和每次图谱调用分别预留 12000 与 24000 个输出 Token。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
15
15
 
16
- 生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。思维导图使用递归树形布局并绘制父子连接线。知识图谱按关联度使用中心节点与内外双环布局,节点卡片内显示类型和最多两行名称,关系以带方向箭头的弱化曲线呈现,并通过描边区分推测或冲突内容;画布提供 50%–200% 缩放和一键恢复 100%,缩放不会影响实体筛选、点击和详情查看,也不会产生横向滚动条。
16
+ 生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。仅生成一种视图时会保留工作区中已有的另一种视图,不再用 `null` 覆盖其存储。思维导图使用递归树形布局并绘制父子连接线;严格来源过滤后少于 2 个有效节点的退化结果不会再作为成功视图保存,而会进入重试。知识图谱按关联度使用中心节点与自动扩展的多层环形布局,节点卡片内显示类型和最多两行名称,关系以带方向箭头的弱化曲线呈现,并通过描边区分推测或冲突内容;画布首次打开自动居中,可按住空白区域拖动,并提供 50%–200% 缩放、独立的 80%–220% 节点间距以及选中实体的一跳局部视图。
17
17
 
18
18
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
19
19
 
package/lib/client.js CHANGED
@@ -380,6 +380,7 @@ window.__ModuleLoader__.load({
380
380
  return React.createElement('div', { className: 'ckm-workspace' }, [
381
381
  React.createElement('section', { key: 'canvas', className: 'ckm-mind-canvas' }, [
382
382
  React.createElement('div', { key: 'hint', className: 'ckm-panel-hint' }, '节点代表阶段性认知;点击后可形成问题,但不会直接修改脑图。'),
383
+ map && map.nodes.length < 2 ? React.createElement('div', { key: 'coverage-warning', className: 'ckm-warning' }, '本次思维导图仅保留了根节点,没有可展开的子节点。请使用修复后的版本重新生成。') : null,
383
384
  tree ? React.createElement('ul', { key: 'tree', className: 'ckm-tree ckm-tree-root' }, React.createElement(MindTreeBranch, { branch: tree, selectedNodeId: props.selectedNodeId, setSelectedNodeId: props.setSelectedNodeId })) : null
384
385
  ]),
385
386
  React.createElement('aside', { key: 'detail', className: 'ckm-detail' }, [
@@ -424,7 +425,7 @@ window.__ModuleLoader__.load({
424
425
  ]))
425
426
  }
426
427
 
427
- function graphLayout(entities, relations) {
428
+ function graphLayout(entities, relations, spacing) {
428
429
  var degree = Object.create(null)
429
430
  entities.forEach(function (entity) { degree[entity.id] = 0 })
430
431
  relations.forEach(function (relation) {
@@ -434,20 +435,30 @@ window.__ModuleLoader__.load({
434
435
  var ordered = entities.slice().sort(function (left, right) {
435
436
  return (degree[right.id] || 0) - (degree[left.id] || 0) || String(left.name || '').localeCompare(String(right.name || ''))
436
437
  })
437
- if (!ordered.length) return []
438
- var positions = [{ entity: ordered[0], x: 450, y: 310, width: 164, height: 68, central: true }]
438
+ if (!ordered.length) return { positions: [], width: 900, height: 620 }
439
+ var density = Math.max(0.8, Math.min(2.2, Number(spacing) || 1))
440
+ var nodeWidth = 150
441
+ var nodeHeight = 64
439
442
  var remaining = ordered.slice(1)
440
- var inner = remaining.slice(0, Math.min(6, remaining.length))
441
- var outer = remaining.slice(inner.length)
442
- function placeRing(items, radiusX, radiusY, phase) {
443
- items.forEach(function (entity, index) {
444
- var angle = phase + index / Math.max(1, items.length) * Math.PI * 2
445
- positions.push({ entity: entity, x: 450 + Math.cos(angle) * radiusX, y: 310 + Math.sin(angle) * radiusY, width: 138, height: 60, central: false })
446
- })
443
+ var rings = []
444
+ var ringIndex = 1
445
+ while (remaining.length) {
446
+ var radius = ringIndex * 205 * density
447
+ var capacity = Math.max(8, Math.floor(Math.PI * 2 * radius / (nodeWidth + 34 * density)))
448
+ rings.push({ radius: radius, items: remaining.splice(0, capacity) })
449
+ ringIndex += 1
447
450
  }
448
- placeRing(inner, 205, 145, -Math.PI / 2)
449
- placeRing(outer, 350, 255, -Math.PI / 2 + Math.PI / Math.max(2, outer.length))
450
- return positions
451
+ var maxRadius = rings.length ? rings[rings.length - 1].radius : 0
452
+ var size = Math.max(620, Math.ceil((maxRadius + nodeWidth / 2 + 110) * 2))
453
+ var center = size / 2
454
+ var positions = [{ entity: ordered[0], x: center, y: center, width: 174, height: 72, central: true }]
455
+ rings.forEach(function (ring, index) {
456
+ ring.items.forEach(function (entity, itemIndex) {
457
+ var angle = -Math.PI / 2 + (index % 2 ? Math.PI / Math.max(2, ring.items.length) : 0) + itemIndex / Math.max(1, ring.items.length) * Math.PI * 2
458
+ positions.push({ entity: entity, x: center + Math.cos(angle) * ring.radius, y: center + Math.sin(angle) * ring.radius, width: nodeWidth, height: nodeHeight, central: false })
459
+ })
460
+ })
461
+ return { positions: positions, width: size, height: size }
451
462
  }
452
463
 
453
464
  function graphLabelLines(value) {
@@ -493,32 +504,91 @@ window.__ModuleLoader__.load({
493
504
  var zoomPair = React.useState(1)
494
505
  var zoom = zoomPair[0]
495
506
  var setZoom = zoomPair[1]
507
+ var spacingPair = React.useState(1)
508
+ var spacing = spacingPair[0]
509
+ var setSpacing = spacingPair[1]
510
+ var localPair = React.useState(false)
511
+ var localOnly = localPair[0]
512
+ var setLocalOnly = localPair[1]
513
+ var panRef = React.useRef(null)
496
514
  if (!graph) return React.createElement(EmptyState, { title: '尚未生成知识图谱', copy: '从标题栏打开知识视图并选择生成知识图谱。' })
497
- var entities = graph.entities.filter(function (entity) {
515
+ var filteredEntities = graph.entities.filter(function (entity) {
498
516
  return (confidence === 'all' || entity.confidence === confidence) && (!query.trim() || (entity.name + ' ' + entity.summary).toLowerCase().indexOf(query.trim().toLowerCase()) >= 0)
499
517
  })
500
518
  var visible = Object.create(null)
501
- entities.forEach(function (entity) { visible[entity.id] = true })
502
- var relations = graph.relations.filter(function (relation) { return visible[relation.from] && visible[relation.to] })
503
- var positions = graphLayout(entities, relations)
519
+ filteredEntities.forEach(function (entity) { visible[entity.id] = true })
520
+ var filteredRelations = graph.relations.filter(function (relation) { return visible[relation.from] && visible[relation.to] })
521
+ var localIds = Object.create(null)
522
+ if (localOnly && selected && visible[selected.id]) {
523
+ localIds[selected.id] = true
524
+ filteredRelations.forEach(function (relation) {
525
+ if (relation.from === selected.id || relation.to === selected.id) {
526
+ localIds[relation.from] = true
527
+ localIds[relation.to] = true
528
+ }
529
+ })
530
+ }
531
+ var entities = localOnly && selected && visible[selected.id] ? filteredEntities.filter(function (entity) { return localIds[entity.id] }) : filteredEntities
532
+ var shown = Object.create(null)
533
+ entities.forEach(function (entity) { shown[entity.id] = true })
534
+ var relations = filteredRelations.filter(function (relation) { return shown[relation.from] && shown[relation.to] })
535
+ var layout = graphLayout(entities, relations, spacing)
536
+ var positions = layout.positions
504
537
  var byId = Object.create(null)
505
538
  positions.forEach(function (item) { byId[item.entity.id] = item })
506
- var viewWidth = 900 / zoom
507
- var viewHeight = 620 / zoom
508
- var viewBox = [(900 - viewWidth) / 2, (620 - viewHeight) / 2, viewWidth, viewHeight].join(' ')
539
+ var viewBox = ['0', '0', layout.width, layout.height].join(' ')
540
+ var centerSignature = [layout.width, layout.height, zoom, spacing, localOnly ? selected && selected.id : 'all'].join(':')
541
+ function centerCanvas(node) {
542
+ if (!node || node.dataset.centered === centerSignature) return
543
+ node.dataset.centered = centerSignature
544
+ window.requestAnimationFrame(function () {
545
+ var viewport = node.parentElement
546
+ if (!viewport) return
547
+ viewport.scrollLeft = Math.max(0, (viewport.scrollWidth - viewport.clientWidth) / 2)
548
+ viewport.scrollTop = Math.max(0, (viewport.scrollHeight - viewport.clientHeight) / 2)
549
+ })
550
+ }
551
+ function beginPan(event) {
552
+ if (event.button !== 0 || (event.target.closest && event.target.closest('.ckm-graph-node'))) return
553
+ var viewport = event.currentTarget.parentElement
554
+ panRef.current = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: viewport.scrollLeft, top: viewport.scrollTop }
555
+ event.currentTarget.setPointerCapture(event.pointerId)
556
+ event.currentTarget.dataset.dragging = 'true'
557
+ event.preventDefault()
558
+ }
559
+ function movePan(event) {
560
+ var pan = panRef.current
561
+ if (!pan || pan.pointerId !== event.pointerId) return
562
+ var viewport = event.currentTarget.parentElement
563
+ viewport.scrollLeft = pan.left - (event.clientX - pan.x)
564
+ viewport.scrollTop = pan.top - (event.clientY - pan.y)
565
+ }
566
+ function endPan(event) {
567
+ var pan = panRef.current
568
+ if (!pan || pan.pointerId !== event.pointerId) return
569
+ panRef.current = null
570
+ event.currentTarget.dataset.dragging = 'false'
571
+ if (event.currentTarget.hasPointerCapture(event.pointerId)) event.currentTarget.releasePointerCapture(event.pointerId)
572
+ }
509
573
  return React.createElement('div', { className: 'ckm-workspace ckm-graph-layout' }, [
510
574
  React.createElement('section', { key: 'graph', className: 'ckm-graph-canvas' }, [
511
575
  React.createElement('div', { key: 'toolbar', className: 'ckm-graph-toolbar' }, [
512
576
  React.createElement('input', { key: 'search', value: query, placeholder: '搜索实体…', onChange: function (event) { setQuery(event.target.value) } }),
513
577
  React.createElement('select', { key: 'confidence', value: confidence, onChange: function (event) { setConfidence(event.target.value) } }, [React.createElement('option', { key: 'all', value: 'all' }, '全部置信度'), React.createElement('option', { key: 'confirmed', value: 'confirmed' }, '已确认'), React.createElement('option', { key: 'inferred', value: 'inferred' }, '推测'), React.createElement('option', { key: 'conflicted', value: 'conflicted' }, '有冲突')]),
578
+ Button({ key: 'local', className: 'ckm-secondary', disabled: !selected, title: selected ? '只显示选中实体及其直接相邻实体' : '请先选择一个实体', onClick: function () { setLocalOnly(!localOnly) } }, localOnly ? '全部视图' : '局部一跳'),
579
+ React.createElement('div', { key: 'spacing', className: 'ckm-graph-zoom', 'aria-label': '知识图谱节点间距控制' }, [
580
+ Button({ key: 'compact', title: '缩小节点间距', disabled: spacing <= 0.8, onClick: function () { setSpacing(Math.max(0.8, Number((spacing - 0.2).toFixed(1)))) } }, '−'),
581
+ Button({ key: 'value', title: '重置节点间距', className: 'ckm-zoom-value', onClick: function () { setSpacing(1) } }, '间距 ' + Math.round(spacing * 100) + '%'),
582
+ Button({ key: 'loose', title: '增大节点间距', disabled: spacing >= 2.2, onClick: function () { setSpacing(Math.min(2.2, Number((spacing + 0.2).toFixed(1)))) } }, '+')
583
+ ]),
514
584
  React.createElement('div', { key: 'zoom', className: 'ckm-graph-zoom', 'aria-label': '知识图谱缩放控制' }, [
515
585
  Button({ key: 'out', title: '缩小', 'aria-label': '缩小知识图谱', disabled: zoom <= 0.5, onClick: function () { setZoom(Math.max(0.5, Number((zoom - 0.1).toFixed(1)))) } }, '−'),
516
586
  Button({ key: 'reset', title: '重置缩放', className: 'ckm-zoom-value', onClick: function () { setZoom(1) } }, Math.round(zoom * 100) + '%'),
517
587
  Button({ key: 'in', title: '放大', 'aria-label': '放大知识图谱', disabled: zoom >= 2, onClick: function () { setZoom(Math.min(2, Number((zoom + 0.1).toFixed(1)))) } }, '+')
518
588
  ])
519
589
  ]),
520
- React.createElement('p', { key: 'static', className: 'ckm-panel-hint' }, '知识图谱是静态结果,可缩放查看;只能通过菜单栏完整重新生成,不支持直接编辑或节点发散。'),
521
- React.createElement('svg', { key: 'svg', className: 'ckm-graph-svg', viewBox: viewBox, role: 'img', 'aria-label': '知识图谱' }, [
590
+ React.createElement('p', { key: 'static', className: 'ckm-panel-hint' }, '知识图谱是静态结果;可按住空白区域拖动画布,画布缩放与节点间距可分别调整,选择实体后可查看一跳局部子图。只能通过菜单栏完整重新生成,不支持直接编辑或节点发散。'),
591
+ React.createElement('svg', { key: 'svg', ref: centerCanvas, className: 'ckm-graph-svg', viewBox: viewBox, width: layout.width * zoom, height: layout.height * zoom, style: { width: layout.width * zoom + 'px', height: layout.height * zoom + 'px' }, role: 'img', 'aria-label': '知识图谱', onPointerDown: beginPan, onPointerMove: movePan, onPointerUp: endPan, onPointerCancel: endPan }, [
522
592
  React.createElement('defs', { key: 'defs' }, React.createElement('marker', { id: 'ckm-arrow', markerWidth: 7, markerHeight: 7, refX: 6, refY: 3.5, orient: 'auto', markerUnits: 'strokeWidth' }, React.createElement('path', { d: 'M0,0 L7,3.5 L0,7 Z', className: 'ckm-arrow-head' }))),
523
593
  React.createElement('g', { key: 'edges', className: 'ckm-graph-edges' }, relations.map(function (relation) {
524
594
  var from = byId[relation.from]
@@ -714,6 +784,8 @@ window.__ModuleLoader__.load({
714
784
  '.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)}' +
715
785
  '@media(max-width:800px){.ckm-workspace,.ckm-graph-layout{display:flex;flex-direction:column}.ckm-detail{border-top:1px solid var(--dsw-alias-border-l2);border-left:0}.ckm-page-header{align-items:flex-start;flex-direction:column}.ckm-page-actions{width:100%;flex-wrap:wrap}}'
716
786
 
787
+ CSS += '.ckm-graph-canvas{overflow:auto}.ckm-graph-toolbar{position:sticky;left:0;z-index:2;max-width:calc(100vw - 410px);padding-bottom:4px;background:var(--dsw-alias-bg-base)}.ckm-graph-zoom{margin-left:0}.ckm-graph-zoom:last-child{margin-left:auto}.ckm-graph-svg{width:auto;max-width:none;height:auto;min-width:620px;min-height:620px;cursor:grab;touch-action:none}.ckm-graph-svg[data-dragging="true"]{cursor:grabbing;user-select:none}'
788
+
717
789
  function apply(ctx) {
718
790
  var sessions = ctx.sessions || (ctx.get && ctx.get('sessions'))
719
791
  ctx.effect(function () {
@@ -80,7 +80,7 @@ function expectedObject(value, kind = '') {
80
80
  if (!kind) return true
81
81
  if (kind === 'summary') return typeof value?.summary === 'string' || typeof value?.narrative === 'string' || typeof value?.text === 'string'
82
82
  if (kind === 'mind-map') return Array.isArray(value?.nodes)
83
- if (kind === 'knowledge-graph') return Array.isArray(value?.entities) && Array.isArray(value?.relations)
83
+ if (kind === 'knowledge-graph') return Array.isArray(value?.entities) && (value.relations === undefined || Array.isArray(value.relations))
84
84
  if (kind === 'follow-up') return typeof value?.question === 'string'
85
85
  return true
86
86
  }
@@ -197,10 +197,16 @@ function jsonParseDiagnostics(value, kind = '') {
197
197
 
198
198
  export function parseStructuredOutput(value, kind = '') {
199
199
  const object = asObject(value)
200
- if (object && expectedObject(object, kind)) return object
200
+ if (object && expectedObject(object, kind)) {
201
+ if (kind === 'knowledge-graph' && object.relations === undefined) return { ...object, relations: [] }
202
+ return object
203
+ }
201
204
  const text = textFromContent(value).replace(/^\uFEFF/, '').trim()
202
205
  const parsed = findJsonObject(text, kind)
203
- if (parsed) return parsed
206
+ if (parsed) {
207
+ if (kind === 'knowledge-graph' && parsed.relations === undefined) return { ...parsed, relations: [] }
208
+ return parsed
209
+ }
204
210
  if (!text) throw new Error('模型没有返回 JSON 对象。')
205
211
  throw new Error('模型返回了文本,但其中没有可解析的 JSON 对象。')
206
212
  }
@@ -504,9 +510,10 @@ function outputPrompt(kind, summaries, prompt, strict, options = {}) {
504
510
  ]
505
511
  const context = JSON.stringify(summaries)
506
512
  if (kind === 'mind-map') {
513
+ const minNodes = Math.max(2, Math.min(6, Number(options.mindMapMinNodes) || 4))
507
514
  return [
508
515
  '请根据以下多个对话摘要生成阶段性思维导图。节点不是关键词,narrative 必须是至少一段完整说明,包含背景、当前认识和下一步/未决点。',
509
- '最多生成 12 个节点;每个 narrative 控制在 300 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
516
+ `生成 ${minNodes}–12 个有效节点,至少包含根节点和覆盖不同对话/阶段的子节点;每个 narrative 控制在 300 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。`,
510
517
  'JSON 形状:{"rootId":"...","nodes":[{"id":"...","parentId":null,"type":"theme|stage|question|decision|solution|risk|conclusion","title":"...","narrative":"...","primarySourceSessionId":"...","sourceRefs":[{"sessionId":"...","eventSeqs":[1]}],"openQuestions":[]}]}。',
511
518
  ...rules,
512
519
  `额外要求:${shortText(prompt, 2000) || '没有额外要求。'}`,
@@ -903,7 +910,8 @@ export class KnowledgeGenerationOrchestrator {
903
910
  if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
904
911
  this.recordTimeline(task, 'view-start', '开始生成思维导图。')
905
912
  this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
906
- const baseMindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
913
+ const mindMapTargetNodes = Math.min(6, Math.max(2, summarizedSources.length + 1))
914
+ const baseMindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true, { mindMapMinNodes: mindMapTargetNodes })
907
915
  const generateMindMap = async (prompt) => this.runModel({
908
916
  kind: 'mind-map', prompt, cwd: request.cwd, strict: request.strict === true,
909
917
  selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
@@ -914,7 +922,7 @@ export class KnowledgeGenerationOrchestrator {
914
922
  const prompt = attempt === 0 ? baseMindMapPrompt : `${baseMindMapPrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(mindMapError), 500)}。请完全重置并从头生成,只输出完整、严格合法且更精简的 JSON。`
915
923
  const value = await generateMindMap(prompt)
916
924
  const sanitized = sanitizeMindMapSources(value, summarizedSources, request.strict === true)
917
- mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
925
+ mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true, minNodes: 2 })
918
926
  sourceWarnings.skippedRefs += sanitized.skippedRefs
919
927
  sourceWarnings.skippedItems += sanitized.skippedItems
920
928
  mindMapError = null
@@ -943,6 +951,7 @@ export class KnowledgeGenerationOrchestrator {
943
951
  this.recordTimeline(task, 'view-start', `开始分 ${graphBatches.length} 批生成知识图谱(最多 ${GRAPH_CONCURRENCY} 批并行)。`)
944
952
  this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: 0, total: graphBatches.length, label: '分批生成知识图谱' } })
945
953
  let completedGraphBatches = 0
954
+ let graphRetryCount = 0
946
955
  const graphBatchResults = await mapWithConcurrency(graphBatches, GRAPH_CONCURRENCY, async (batch, batchIndex) => {
947
956
  let graphError = null
948
957
  for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
@@ -967,7 +976,8 @@ export class KnowledgeGenerationOrchestrator {
967
976
  graphError = error
968
977
  logMessage(this.logger, 'warn', 'knowledge graph batch invalid id=%s batch=%d attempt=%d error=%s', logId(task.id), batchIndex + 1, attempt + 1, errorMessage(error))
969
978
  if (attempt < MAX_MODEL_RETRIES) {
970
- this.recordTimeline(task, 'retry', `知识图谱第 ${batchIndex + 1}/${graphBatches.length} 批失败(${shortText(errorMessage(graphError), 160)}),降低本批输出预算后重试 ${attempt + 1}/${MAX_MODEL_RETRIES}。`)
979
+ graphRetryCount += 1
980
+ this.recordTimeline(task, 'retry', `知识图谱第 ${batchIndex + 1}/${graphBatches.length} 批输出无效(${shortText(errorMessage(graphError), 160)}),正在降低本批输出预算并重试 ${attempt + 1}/${MAX_MODEL_RETRIES}。`)
971
981
  }
972
982
  }
973
983
  }
@@ -981,7 +991,8 @@ export class KnowledgeGenerationOrchestrator {
981
991
  knowledgeGraph = validateKnowledgeGraph(mergedGraph, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
982
992
  const rawEntities = successfulGraphBatches.reduce((total, result) => total + result.rawEntities, 0)
983
993
  const rawRelations = successfulGraphBatches.reduce((total, result) => total + result.rawRelations, 0)
984
- this.recordTimeline(task, 'graph-coverage', `知识图谱 ${successfulGraphBatches.length}/${graphBatches.length} 批成功,模型共返回 ${rawEntities} 个实体、${rawRelations} 条关系;合并去重后保留 ${knowledgeGraph.entities.length} 个实体、${knowledgeGraph.relations.length} 条关系。`, { entityLimit: graphBudget.entities, relationLimit: graphBudget.relations })
994
+ const retrySummary = graphRetryCount ? `;期间发生 ${graphRetryCount} 次批次重试${successfulGraphBatches.length === graphBatches.length ? ',已全部恢复' : ''}` : ''
995
+ this.recordTimeline(task, 'graph-coverage', `知识图谱 ${successfulGraphBatches.length}/${graphBatches.length} 批成功,模型共返回 ${rawEntities} 个实体、${rawRelations} 条关系;合并去重后保留 ${knowledgeGraph.entities.length} 个实体、${knowledgeGraph.relations.length} 条关系${retrySummary}。`, { entityLimit: graphBudget.entities, relationLimit: graphBudget.relations })
985
996
  this.recordTimeline(task, 'view-complete', '知识图谱分批生成与合并完成。')
986
997
  } else {
987
998
  const graphError = graphBatchResults.find((result) => result.error)?.error || new Error('所有知识图谱批次均生成失败。')
@@ -1019,7 +1030,7 @@ export class KnowledgeGenerationOrchestrator {
1019
1030
  knowledgeGraph
1020
1031
  })
1021
1032
  task.revision = saved.revision
1022
- task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
1033
+ task.result = { revision: saved.revision, manifest: saved.manifest, mindMap: saved.mindMap, knowledgeGraph: saved.knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
1023
1034
  this.recordTimeline(task, 'complete', '知识视图已保存,生成流程完成。')
1024
1035
  logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
1025
1036
  this.update(task, 'completed', {
@@ -45,10 +45,11 @@ function detectCycle(nodes) {
45
45
  for (const node of nodes) visit(node.id)
46
46
  }
47
47
 
48
- export function validateMindMap(input, { selectedSessionIds = [], strict = false } = {}) {
48
+ export function validateMindMap(input, { selectedSessionIds = [], strict = false, minNodes = 1 } = {}) {
49
49
  if (!input || typeof input !== 'object') throw new MindMapValidationError('思维导图结果必须是对象。')
50
50
  const rawNodes = Array.isArray(input.nodes) ? input.nodes : []
51
- if (!rawNodes.length) throw new MindMapValidationError('思维导图至少需要一个节点。')
51
+ const requiredNodes = Math.max(1, Math.floor(Number(minNodes) || 1))
52
+ if (rawNodes.length < requiredNodes) throw new MindMapValidationError(`思维导图至少需要 ${requiredNodes} 个有效节点,当前仅有 ${rawNodes.length} 个。`)
52
53
  if (rawNodes.length > MAX_MIND_MAP_NODES) throw new MindMapValidationError(`思维导图节点不能超过 ${MAX_MIND_MAP_NODES} 个。`)
53
54
  const rootId = asText(input.rootId || rawNodes[0]?.id, '思维导图 rootId', 256)
54
55
  const ids = new Set()
@@ -192,13 +192,17 @@ export class WorkspaceStorage {
192
192
  generatedAt: this.now()
193
193
  }
194
194
  const navigationHistory = current.navigationHistory || []
195
+ // A generation request may target only one view. Keep the other view intact
196
+ // instead of replacing its persisted JSON with null.
197
+ const nextMindMap = mindMap || current.mindMap || null
198
+ const nextKnowledgeGraph = knowledgeGraph || current.knowledgeGraph || null
195
199
  await replaceBundle(this.resolveDataDir(normalizedCwd), {
196
200
  manifest,
197
- mindMap: mindMap || null,
198
- knowledgeGraph: knowledgeGraph || null,
201
+ mindMap: nextMindMap,
202
+ knowledgeGraph: nextKnowledgeGraph,
199
203
  navigationHistory
200
204
  })
201
- return { revision, manifest, dataDir: this.resolveDataDir(normalizedCwd) }
205
+ return { revision, manifest, dataDir: this.resolveDataDir(normalizedCwd), mindMap: nextMindMap, knowledgeGraph: nextKnowledgeGraph }
202
206
  })
203
207
  this.locks.set(key, operation)
204
208
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/conversation-knowledge-map",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",