@p-dsh-market/conversation-knowledge-map 0.1.13 → 0.1.15

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,9 +11,9 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
11
11
 
12
12
  配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
13
13
 
14
- 摘要阶段按对话并行处理,并发上限固定为 3;每个对话按约 5000 字、优先在完整段落边界分段,只有单段本身超长时才按句末或硬边界拆分。同一对话的分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、具体失败原因、重试、跳过、合并、生成及保存过程。模型可以保留 thinking,但结果提取只读取最终文本并过滤 reasoning 内容;摘要和最终视图分别预留 12000 与 24000 个输出 Token。知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
14
+ 摘要阶段按对话并行处理,并发上限固定为 3;每个对话按约 5000 字、优先在完整段落边界分段,只有单段本身超长时才按句末或硬边界拆分。同一对话的分段保持顺序处理。思维导图使用合并后的会话摘要;知识图谱直接使用保留来源的分段摘要,避免长对话在二次压缩后丢失实体。知识图谱根据已读取文本量和有效分段数动态计算上限,范围为 30–100 个实体、最多 200 条关系,并要求逐分段检查覆盖范围。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、具体失败原因、重试、跳过、合并、生成及保存过程;知识图谱时间线还会显示模型返回数量和来源校验后的保留数量。模型可以保留 thinking,但结果提取只读取最终文本并过滤 reasoning 内容;摘要和最终视图分别预留 12000 与 24000 个输出 Token。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
15
15
 
16
- 生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。思维导图使用递归树形布局并绘制父子连接线;知识图谱提供 50%–200% 缩放和一键恢复 100%,缩放不会影响实体筛选、点击和详情查看。
16
+ 生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。思维导图使用递归树形布局并绘制父子连接线。知识图谱按关联度使用中心节点与内外双环布局,节点卡片内显示类型和最多两行名称,关系以带方向箭头的弱化曲线呈现,并通过描边区分推测或冲突内容;画布提供 50%–200% 缩放和一键恢复 100%,缩放不会影响实体筛选、点击和详情查看,也不会产生横向滚动条。
17
17
 
18
18
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
19
19
 
package/lib/client.js CHANGED
@@ -424,6 +424,61 @@ window.__ModuleLoader__.load({
424
424
  ]))
425
425
  }
426
426
 
427
+ function graphLayout(entities, relations) {
428
+ var degree = Object.create(null)
429
+ entities.forEach(function (entity) { degree[entity.id] = 0 })
430
+ relations.forEach(function (relation) {
431
+ if (degree[relation.from] !== undefined) degree[relation.from] += 1
432
+ if (degree[relation.to] !== undefined) degree[relation.to] += 1
433
+ })
434
+ var ordered = entities.slice().sort(function (left, right) {
435
+ return (degree[right.id] || 0) - (degree[left.id] || 0) || String(left.name || '').localeCompare(String(right.name || ''))
436
+ })
437
+ if (!ordered.length) return []
438
+ var positions = [{ entity: ordered[0], x: 450, y: 310, width: 164, height: 68, central: true }]
439
+ 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
+ })
447
+ }
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
+ }
452
+
453
+ function graphLabelLines(value) {
454
+ var text = String(value || '').replace(/\s+/g, ' ').trim()
455
+ if (text.length <= 16) return [text]
456
+ var probe = text.slice(0, 17)
457
+ var split = Math.max(probe.lastIndexOf(' '), probe.lastIndexOf('_'), probe.lastIndexOf('/'), probe.lastIndexOf('-'))
458
+ if (split < 7) split = 15
459
+ var first = text.slice(0, split).trim()
460
+ var rest = text.slice(split).replace(/^[_/\-\s]+/, '').trim()
461
+ var second = rest.length > 16 ? rest.slice(0, 15) + '…' : rest
462
+ return [first, second].filter(Boolean)
463
+ }
464
+
465
+ function graphEdgePath(from, to, relationId) {
466
+ var dx = to.x - from.x
467
+ var dy = to.y - from.y
468
+ var distance = Math.max(1, Math.sqrt(dx * dx + dy * dy))
469
+ var fromScale = Math.min((from.width / 2) / Math.max(1, Math.abs(dx)), (from.height / 2) / Math.max(1, Math.abs(dy)))
470
+ var toScale = Math.min((to.width / 2) / Math.max(1, Math.abs(dx)), (to.height / 2) / Math.max(1, Math.abs(dy)))
471
+ var startX = from.x + dx * fromScale
472
+ var startY = from.y + dy * fromScale
473
+ var endX = to.x - dx * toScale
474
+ var endY = to.y - dy * toScale
475
+ var hash = String(relationId || '').split('').reduce(function (sum, char) { return sum + char.charCodeAt(0) }, 0)
476
+ var bend = (hash % 3 - 1) * 12
477
+ var controlX = (startX + endX) / 2 - dy / distance * bend
478
+ var controlY = (startY + endY) / 2 + dx / distance * bend
479
+ return 'M ' + startX + ' ' + startY + ' Q ' + controlX + ' ' + controlY + ' ' + endX + ' ' + endY
480
+ }
481
+
427
482
  function KnowledgeGraphPanel(props) {
428
483
  var graph = props.store.state && props.store.state.knowledgeGraph
429
484
  var queryPair = React.useState('')
@@ -444,16 +499,13 @@ window.__ModuleLoader__.load({
444
499
  })
445
500
  var visible = Object.create(null)
446
501
  entities.forEach(function (entity) { visible[entity.id] = true })
447
- var positions = entities.map(function (entity, index) {
448
- var angle = index / Math.max(1, entities.length) * Math.PI * 2
449
- return { entity: entity, x: 400 + Math.cos(angle) * 260, y: 230 + Math.sin(angle) * 160 }
450
- })
502
+ var relations = graph.relations.filter(function (relation) { return visible[relation.from] && visible[relation.to] })
503
+ var positions = graphLayout(entities, relations)
451
504
  var byId = Object.create(null)
452
505
  positions.forEach(function (item) { byId[item.entity.id] = item })
453
- var relations = graph.relations.filter(function (relation) { return visible[relation.from] && visible[relation.to] })
454
- var viewWidth = 800 / zoom
455
- var viewHeight = 460 / zoom
456
- var viewBox = [(800 - viewWidth) / 2, (460 - viewHeight) / 2, viewWidth, viewHeight].join(' ')
506
+ var viewWidth = 900 / zoom
507
+ var viewHeight = 620 / zoom
508
+ var viewBox = [(900 - viewWidth) / 2, (620 - viewHeight) / 2, viewWidth, viewHeight].join(' ')
457
509
  return React.createElement('div', { className: 'ckm-workspace ckm-graph-layout' }, [
458
510
  React.createElement('section', { key: 'graph', className: 'ckm-graph-canvas' }, [
459
511
  React.createElement('div', { key: 'toolbar', className: 'ckm-graph-toolbar' }, [
@@ -467,8 +519,23 @@ window.__ModuleLoader__.load({
467
519
  ]),
468
520
  React.createElement('p', { key: 'static', className: 'ckm-panel-hint' }, '知识图谱是静态结果,可缩放查看;只能通过菜单栏完整重新生成,不支持直接编辑或节点发散。'),
469
521
  React.createElement('svg', { key: 'svg', className: 'ckm-graph-svg', viewBox: viewBox, role: 'img', 'aria-label': '知识图谱' }, [
470
- relations.map(function (relation) { var from = byId[relation.from]; var to = byId[relation.to]; return React.createElement('line', { key: relation.id, x1: from.x, y1: from.y, x2: to.x, y2: to.y, className: 'ckm-edge' }) }),
471
- positions.map(function (item) { return React.createElement('g', { key: item.entity.id, className: 'ckm-graph-node', 'data-active': selected && selected.id === item.entity.id, onClick: function () { setSelected(item.entity) } }, [React.createElement('circle', { key: 'circle', cx: item.x, cy: item.y, r: 26 }), React.createElement('text', { key: 'text', x: item.x, y: item.y + 44, textAnchor: 'middle' }, item.entity.name.slice(0, 18))]) })
522
+ 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
+ React.createElement('g', { key: 'edges', className: 'ckm-graph-edges' }, relations.map(function (relation) {
524
+ var from = byId[relation.from]
525
+ var to = byId[relation.to]
526
+ return React.createElement('path', { key: relation.id, d: graphEdgePath(from, to, relation.id), className: 'ckm-edge', markerEnd: 'url(#ckm-arrow)' }, React.createElement('title', null, relation.type))
527
+ })),
528
+ positions.map(function (item) {
529
+ var lines = graphLabelLines(item.entity.name)
530
+ return React.createElement('g', { key: item.entity.id, className: 'ckm-graph-node', 'data-active': selected && selected.id === item.entity.id, 'data-confidence': item.entity.confidence, 'data-central': item.central, onClick: function () { setSelected(item.entity) } }, [
531
+ React.createElement('rect', { key: 'shape', x: item.x - item.width / 2, y: item.y - item.height / 2, width: item.width, height: item.height, rx: 13 }),
532
+ React.createElement('text', { key: 'type', className: 'ckm-graph-node-type', x: item.x, y: item.y - 12, textAnchor: 'middle' }, String(item.entity.type || 'entity').slice(0, 18)),
533
+ React.createElement('text', { key: 'name', className: 'ckm-graph-node-name', x: item.x, y: lines.length === 1 ? item.y + 10 : item.y + 4, textAnchor: 'middle' }, lines.map(function (line, index) {
534
+ return React.createElement('tspan', { key: index, x: item.x, dy: index === 0 ? 0 : 16 }, line)
535
+ })),
536
+ React.createElement('title', { key: 'title' }, item.entity.name + ' · ' + item.entity.confidence)
537
+ ])
538
+ })
472
539
  ])
473
540
  ]),
474
541
  React.createElement('aside', { key: 'detail', className: 'ckm-detail' }, [
@@ -642,8 +709,8 @@ window.__ModuleLoader__.load({
642
709
  '.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}' +
643
710
  '.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}' +
644
711
  '.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-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.ckm-timeline-title{color:var(--dsw-alias-label-primary)}.ckm-timeline-toggle{border:0;padding:3px 7px;border-radius:6px;background:transparent;color:var(--dsw-alias-state-business-primary);cursor:pointer;font:inherit;font-size:11px}.ckm-timeline-toggle:hover{background:var(--dsw-alias-interactive-bg-hover)}.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}' +
645
- '.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{max-width:900px;margin:0 auto;padding:0;list-style:none}.ckm-tree-branch{position:relative;padding:6px 0 6px 30px;list-style:none}.ckm-tree-root>.ckm-tree-branch{padding-left:0}.ckm-tree-children{position:relative;margin:8px 0 0 20px;padding:0 0 0 22px;list-style:none}.ckm-tree-children>.ckm-tree-branch:before{content:"";position:absolute;left:-22px;top:-8px;bottom:50%;border-left:1px solid var(--dsw-alias-border-l2)}.ckm-tree-children>.ckm-tree-branch:not(:last-child):before{bottom:-8px}.ckm-tree-children>.ckm-tree-branch:after{content:"";position:absolute;left:-22px;top:50%;width:22px;border-top:1px solid var(--dsw-alias-border-l2)}.ckm-tree-node{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:min(720px,100%);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}' +
646
- '.ckm-graph-layout{grid-template-columns:minmax(0,1fr) 330px}.ckm-graph-toolbar{display:flex;align-items:center;flex-wrap:wrap;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;min-width:180px}.ckm-graph-zoom{display:flex;align-items:center;margin-left:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}.ckm-graph-zoom button{min-width:32px;border:0;border-right:1px solid var(--dsw-alias-border-l2);padding:7px 9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit}.ckm-graph-zoom button:last-child{border-right:0}.ckm-graph-zoom button:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-graph-zoom button:disabled{opacity:.4;cursor:not-allowed}.ckm-graph-zoom .ckm-zoom-value{min-width:58px;color:var(--dsw-alias-state-business-primary);font-size:11px}.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}' +
712
+ '.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;padding:20px;background:var(--dsw-alias-bg-base)}.ckm-mind-canvas{overflow:auto}.ckm-graph-canvas{overflow-x:hidden;overflow-y:auto}.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{max-width:900px;margin:0 auto;padding:0;list-style:none}.ckm-tree-branch{position:relative;padding:6px 0 6px 30px;list-style:none}.ckm-tree-root>.ckm-tree-branch{padding-left:0}.ckm-tree-children{position:relative;margin:8px 0 0 20px;padding:0 0 0 22px;list-style:none}.ckm-tree-children>.ckm-tree-branch:before{content:"";position:absolute;left:-22px;top:-8px;bottom:50%;border-left:1px solid var(--dsw-alias-border-l2)}.ckm-tree-children>.ckm-tree-branch:not(:last-child):before{bottom:-8px}.ckm-tree-children>.ckm-tree-branch:after{content:"";position:absolute;left:-22px;top:50%;width:22px;border-top:1px solid var(--dsw-alias-border-l2)}.ckm-tree-node{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:min(720px,100%);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}' +
713
+ '.ckm-graph-layout{grid-template-columns:minmax(0,1fr) 330px}.ckm-graph-toolbar{display:flex;align-items:center;flex-wrap:wrap;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;min-width:180px}.ckm-graph-zoom{display:flex;align-items:center;margin-left:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;overflow:hidden}.ckm-graph-zoom button{min-width:32px;border:0;border-right:1px solid var(--dsw-alias-border-l2);padding:7px 9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit}.ckm-graph-zoom button:last-child{border-right:0}.ckm-graph-zoom button:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-graph-zoom button:disabled{opacity:.4;cursor:not-allowed}.ckm-graph-zoom .ckm-zoom-value{min-width:58px;color:var(--dsw-alias-state-business-primary);font-size:11px}.ckm-graph-svg{display:block;width:100%;max-width:100%;height:auto;min-height:500px;margin-top:8px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:radial-gradient(circle at 50% 48%,var(--dsw-alias-state-business-tertiary),var(--dsw-alias-bg-layer-1) 58%);box-sizing:border-box}.ckm-graph-edges{opacity:.62}.ckm-edge{fill:none;stroke:var(--dsw-alias-border-l2);stroke-width:1.4}.ckm-arrow-head{fill:var(--dsw-alias-label-tertiary)}.ckm-graph-node{cursor:pointer}.ckm-graph-node rect{fill:var(--dsw-alias-bg-layer-1);stroke:var(--dsw-alias-state-business-primary);stroke-width:1.5;filter:drop-shadow(0 4px 8px rgba(0,0,0,.14))}.ckm-graph-node[data-central=true] rect{fill:var(--dsw-alias-button-info-fill);stroke:var(--dsw-alias-label-primary);stroke-width:2}.ckm-graph-node[data-confidence="inferred"] rect{stroke:#d69a42}.ckm-graph-node[data-confidence="conflicted"] rect{stroke:#e06c75}.ckm-graph-node[data-active=true] rect{fill:var(--dsw-alias-state-business-tertiary);stroke:var(--dsw-alias-label-primary);stroke-width:2.5}.ckm-graph-node text{pointer-events:none}.ckm-graph-node-type{fill:var(--dsw-alias-label-tertiary);font-size:9px;letter-spacing:.04em;text-transform:uppercase}.ckm-graph-node-name{fill:var(--dsw-alias-label-primary);font-size:11px;font-weight:600}.ckm-graph-node[data-central=true] .ckm-graph-node-type,.ckm-graph-node[data-central=true] .ckm-graph-node-name{fill:var(--dsw-alias-label-primary-foreground)}' +
647
714
  '.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)}' +
648
715
  '@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}}'
649
716
 
@@ -10,6 +10,9 @@ const SUMMARY_MAX_TOKENS = 12000
10
10
  const VIEW_MAX_TOKENS = 24000
11
11
  const SUMMARY_CONCURRENCY = 3
12
12
  const MAX_MODEL_RETRIES = 3
13
+ const MIN_GRAPH_ENTITIES = 30
14
+ const MAX_GRAPH_ENTITIES = 100
15
+ const MAX_GRAPH_RELATIONS = 200
13
16
 
14
17
  function modelLabel(selection) {
15
18
  const provider = String(selection?.provider || '').trim()
@@ -154,7 +157,7 @@ function normalizeSummary(value, source, chunk) {
154
157
  const summary = shortText(result.summary || result.narrative || result.text, MAX_SUMMARY_CHARS)
155
158
  if (!summary) throw new Error(`对话 ${source.sessionId} 的摘要为空。`)
156
159
  const keyPoints = Array.isArray(result.keyPoints)
157
- ? result.keyPoints.map((item) => shortText(item, 500)).filter(Boolean).slice(0, 12)
160
+ ? result.keyPoints.map((item) => shortText(item, 500)).filter(Boolean).slice(0, 24)
158
161
  : []
159
162
  const sourceRefs = sanitizeSourceRefs(result.sourceRefs, source, sourceRefsFromChunk(source, chunk))
160
163
  return {
@@ -177,6 +180,31 @@ function mergeSourceSummaries(source, chunkSummaries) {
177
180
  return { sessionId: source.sessionId, title: source.title, summary, keyPoints, sourceRefs }
178
181
  }
179
182
 
183
+ function graphGenerationContext(successfulResults) {
184
+ return successfulResults.flatMap((result) => (result.chunkSummaries || []).map((summary, index) => ({
185
+ sessionId: summary.sessionId,
186
+ title: summary.title,
187
+ chunk: index + 1,
188
+ summary: summary.summary,
189
+ keyPoints: summary.keyPoints,
190
+ sourceRefs: summary.sourceRefs
191
+ })))
192
+ }
193
+
194
+ function graphGenerationBudget(sources, context) {
195
+ const sourceChars = sources.reduce((total, source) => total + String(source?.text || '').length, 0)
196
+ const evidenceUnits = Math.max(sources.length, context.length)
197
+ const entities = Math.min(MAX_GRAPH_ENTITIES, Math.max(
198
+ MIN_GRAPH_ENTITIES,
199
+ Math.ceil(sourceChars / 1200),
200
+ evidenceUnits * 6
201
+ ))
202
+ return {
203
+ entities,
204
+ relations: Math.min(MAX_GRAPH_RELATIONS, Math.max(entities, Math.ceil(entities * 1.8)))
205
+ }
206
+ }
207
+
180
208
  async function mapWithConcurrency(items, limit, mapper) {
181
209
  const results = new Array(items.length)
182
210
  let nextIndex = 0
@@ -300,7 +328,7 @@ function assertOutputSourceRefs(value, sources) {
300
328
  for (const relation of value?.knowledgeGraph?.relations || []) for (const ref of relation.evidence || []) check(ref, `知识图谱关系 ${relation.id}`)
301
329
  }
302
330
 
303
- function outputPrompt(kind, summaries, prompt, strict) {
331
+ function outputPrompt(kind, summaries, prompt, strict, options = {}) {
304
332
  const rules = [
305
333
  '只输出一个 JSON 对象,不要 Markdown 代码围栏,不要额外解释。',
306
334
  '所有 sourceRefs/evidence 必须使用给定的 sessionId 和 eventSeqs,不能虚构来源。',
@@ -317,13 +345,16 @@ function outputPrompt(kind, summaries, prompt, strict) {
317
345
  `对话摘要:${context}`
318
346
  ].join('\n\n')
319
347
  }
348
+ const graphBudget = options.graphBudget || { entities: MIN_GRAPH_ENTITIES, relations: Math.ceil(MIN_GRAPH_ENTITIES * 1.8) }
320
349
  return [
321
- '请根据以下多个对话摘要生成静态知识图谱。抽取实体、概念、模块、接口、决策、风险和外部系统,并只建立有依据的关系。',
322
- '最多生成 20 个实体、30 条关系;实体 summary 控制在 220 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
350
+ '请根据以下多个对话分段证据生成高召回的静态知识图谱。系统性抽取人物/组织、实体、概念、模块、接口、数据对象、工具、外部系统、需求、约束、决策、结论、风险、问题和行动项,并只建立有依据的关系。',
351
+ `本次内容规模对应的上限为 ${graphBudget.entities} 个实体、${graphBudget.relations} 条关系。这是防止输出失控的上限,不是要求凑数;在有依据的前提下应尽量完整覆盖每个对话和分段,不要只保留少数总括性主题。实体 summary 控制在 220 字以内。`,
352
+ '同名同义实体应合并,但不同模块、接口、决策、风险或行动项不能因为属于同一主题就合并。优先保留能承载具体关系的细粒度实体;每个分段都要检查是否存在尚未覆盖的新实体和关系。',
353
+ '不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
323
354
  'JSON 形状:{"entities":[{"id":"...","type":"...","name":"...","summary":"...","confidence":"confirmed|inferred|conflicted","sourceRefs":[{"sessionId":"...","eventSeqs":[1]}]}],"relations":[{"id":"...","from":"...","to":"...","type":"depends_on|calls|supports|constrains|belongs_to|derived_from","confidence":"confirmed|inferred|conflicted","evidence":[{"sessionId":"...","eventSeqs":[1]}]}]}。',
324
355
  ...rules,
325
356
  `额外要求:${shortText(prompt, 2000) || '没有额外要求。'}`,
326
- `对话摘要:${context}`
357
+ `对话分段证据:${context}`
327
358
  ].join('\n\n')
328
359
  }
329
360
 
@@ -331,7 +362,7 @@ function summaryPrompt(source, chunk, strict) {
331
362
  return [
332
363
  '请把一段 DSH 对话整理成带来源的结构化摘要。不要复述完整聊天,不要添加对话中没有的事实。',
333
364
  '只输出 JSON:{"summary":"完整阶段性说明","keyPoints":["..."],"sourceRefs":[{"sessionId":"...","eventSeqs":[1]}]}。',
334
- 'summary 控制在 1200 字以内;keyPoints 最多 8 条,每条不超过 160 字。不要增加其他字段。字符串中的双引号必须正确转义,确保整个输出可以被 JSON.parse 直接解析。',
365
+ 'summary 控制在 1200 字以内;keyPoints 最多 16 条,每条不超过 160 字。keyPoints 要优先保留具名的人物/组织、模块、接口、数据对象、工具、外部系统、需求、约束、决策、风险、问题、行动项以及它们之间的关系,不要只写总括性主题。不要增加其他字段。字符串中的双引号必须正确转义,确保整个输出可以被 JSON.parse 直接解析。',
335
366
  strict ? '严格模式:每个关键点都要能回指给定事件。' : '允许标记尚未确认的冲突,但不能编造事件序号。',
336
367
  `Session:${source.sessionId}`,
337
368
  `标题:${source.title}`,
@@ -684,7 +715,7 @@ export class KnowledgeGenerationOrchestrator {
684
715
  sourceCount: sources.length,
685
716
  progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已完成摘要:${source.title}` }
686
717
  })
687
- return { source, error: '', summary: mergeSourceSummaries(source, chunkSummaries) }
718
+ return { source, error: '', summary: mergeSourceSummaries(source, chunkSummaries), chunkSummaries }
688
719
  })
689
720
  this.assertNotCancelled(task)
690
721
  const successfulResults = summaryResults.filter((result) => result?.summary)
@@ -694,6 +725,8 @@ export class KnowledgeGenerationOrchestrator {
694
725
  if (!successfulResults.length) throw new Error(`所选 ${sources.length} 个对话均未能生成合法摘要,已分别重试 ${MAX_MODEL_RETRIES} 次。`)
695
726
  const summarizedSources = successfulResults.map((result) => result.source)
696
727
  const summaries = successfulResults.map((result) => result.summary)
728
+ const graphContext = graphGenerationContext(successfulResults)
729
+ const graphBudget = graphGenerationBudget(summarizedSources, graphContext)
697
730
  const sourceSessionIds = summarizedSources.map((source) => source.sessionId)
698
731
  this.recordTimeline(task, 'merge', `已合并 ${summaries.length} 个对话摘要${failedSources.length ? `,跳过 ${failedSources.length} 个失败对话` : ''},开始生成最终视图。`)
699
732
  let mindMap = null
@@ -740,17 +773,18 @@ export class KnowledgeGenerationOrchestrator {
740
773
  if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
741
774
  this.recordTimeline(task, 'view-start', '开始生成知识图谱。')
742
775
  this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: sources.length, total: sources.length, label: '生成知识图谱' } })
743
- const baseGraphPrompt = outputPrompt('knowledge-graph', summaries, request.prompt, request.strict === true)
776
+ const baseGraphPrompt = outputPrompt('knowledge-graph', graphContext, request.prompt, request.strict === true, { graphBudget })
744
777
  let graphError = null
745
778
  for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
746
779
  try {
747
- const prompt = attempt === 0 ? baseGraphPrompt : `${baseGraphPrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(graphError), 500)}。请完全重置并从头生成,只输出完整、严格合法且更精简的 JSON。`
780
+ const prompt = attempt === 0 ? baseGraphPrompt : `${baseGraphPrompt}\n\n第 ${attempt} 次输出未通过校验:${shortText(errorMessage(graphError), 500)}。请完全重置并从头生成,修正结构或来源错误,同时保持对各分段的完整覆盖;只有确实超过本次上限时才精简。只输出完整、严格合法的 JSON。`
748
781
  const value = await this.runModel({
749
782
  kind: 'knowledge-graph', prompt, cwd: request.cwd, strict: request.strict === true,
750
783
  selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
751
784
  })
752
785
  const sanitized = sanitizeKnowledgeGraphSources(value, summarizedSources, request.strict === true)
753
786
  knowledgeGraph = validateKnowledgeGraph(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
787
+ this.recordTimeline(task, 'graph-coverage', `知识图谱模型返回 ${Array.isArray(value?.entities) ? value.entities.length : 0} 个实体、${Array.isArray(value?.relations) ? value.relations.length : 0} 条关系;来源校验后保留 ${knowledgeGraph.entities.length} 个实体、${knowledgeGraph.relations.length} 条关系。`, { entityLimit: graphBudget.entities, relationLimit: graphBudget.relations })
754
788
  sourceWarnings.skippedRefs += sanitized.skippedRefs
755
789
  sourceWarnings.skippedItems += sanitized.skippedItems
756
790
  graphError = null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/conversation-knowledge-map",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",