@p-dsh-market/conversation-knowledge-map 0.1.16 → 0.1.18
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 +2 -2
- package/lib/client.js +59 -22
- package/lib/generation-orchestrator.js +85 -7
- package/lib/workspace-storage.js +7 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,13 +13,13 @@ 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
|
-
|
|
16
|
+
生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。仅生成一种视图时会保留工作区中已有的另一种视图,不再用 `null` 覆盖其存储。思维导图使用递归树形布局并绘制父子连接线。知识图谱按关联度使用中心节点与自动扩展的多层环形布局,节点卡片内显示类型和最多两行名称,关系以带方向箭头的弱化曲线呈现,并通过描边区分推测或冲突内容;画布提供 50%–200% 缩放、独立的 80%–220% 节点间距以及选中实体的一跳局部视图,大图可通过横纵滚动查看。
|
|
17
17
|
|
|
18
18
|
节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
|
|
19
19
|
|
|
20
20
|
## 生成失败诊断
|
|
21
21
|
|
|
22
|
-
Host 和生成编排器会输出带 `[conversation-knowledge-map]` 前缀的诊断日志。日志包含路由、Provider / Model、Agent Session、实时事件类型、surface/session 读取次数、提取文本长度、输出形状和错误原因,不记录 Prompt
|
|
22
|
+
Host 和生成编排器会输出带 `[conversation-knowledge-map]` 前缀的诊断日志。日志包含路由、Provider / Model、Agent Session、实时事件类型、surface/session 读取次数、提取文本长度、输出形状和错误原因,不记录 Prompt 或原始对话正文。为诊断结构化输出失败,当前还会记录模型实际返回文本:不超过 32000 字符时完整记录,超过时保留首尾各 16000 字符;该内容可能包含模型整理出的对话信息,排障完成后应按运行环境的日志策略清理。解析失败日志会额外列出 Markdown JSON 围栏、首末大括号位置以及每个候选对象属于未闭合、JSON 语法错误、合法但结构不符或合法目标结构。优先查看 DSH Web Runtime 的终端日志;若 Runtime 提供 logger 服务,则同时写入该 logger。重点关注 `agent output content`、`agent output parse failed`、`agent event`、`agent idle`、`agent turn failure`、`agent surface read` 和 `agent session read`。Runtime 在 `turn/end.reason.kind = error` 时会优先显示其 `code/message`;只有未发现 turn 错误且确实没有助手输出时,才会报告“模型没有返回 JSON 对象”。
|
|
23
23
|
|
|
24
24
|
## 本地验证
|
|
25
25
|
|
package/lib/client.js
CHANGED
|
@@ -424,7 +424,7 @@ window.__ModuleLoader__.load({
|
|
|
424
424
|
]))
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
-
function graphLayout(entities, relations) {
|
|
427
|
+
function graphLayout(entities, relations, spacing) {
|
|
428
428
|
var degree = Object.create(null)
|
|
429
429
|
entities.forEach(function (entity) { degree[entity.id] = 0 })
|
|
430
430
|
relations.forEach(function (relation) {
|
|
@@ -434,20 +434,30 @@ window.__ModuleLoader__.load({
|
|
|
434
434
|
var ordered = entities.slice().sort(function (left, right) {
|
|
435
435
|
return (degree[right.id] || 0) - (degree[left.id] || 0) || String(left.name || '').localeCompare(String(right.name || ''))
|
|
436
436
|
})
|
|
437
|
-
if (!ordered.length) return []
|
|
438
|
-
var
|
|
437
|
+
if (!ordered.length) return { positions: [], width: 900, height: 620 }
|
|
438
|
+
var density = Math.max(0.8, Math.min(2.2, Number(spacing) || 1))
|
|
439
|
+
var nodeWidth = 150
|
|
440
|
+
var nodeHeight = 64
|
|
439
441
|
var remaining = ordered.slice(1)
|
|
440
|
-
var
|
|
441
|
-
var
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
442
|
+
var rings = []
|
|
443
|
+
var ringIndex = 1
|
|
444
|
+
while (remaining.length) {
|
|
445
|
+
var radius = ringIndex * 205 * density
|
|
446
|
+
var capacity = Math.max(8, Math.floor(Math.PI * 2 * radius / (nodeWidth + 34 * density)))
|
|
447
|
+
rings.push({ radius: radius, items: remaining.splice(0, capacity) })
|
|
448
|
+
ringIndex += 1
|
|
447
449
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
var maxRadius = rings.length ? rings[rings.length - 1].radius : 0
|
|
451
|
+
var size = Math.max(620, Math.ceil((maxRadius + nodeWidth / 2 + 110) * 2))
|
|
452
|
+
var center = size / 2
|
|
453
|
+
var positions = [{ entity: ordered[0], x: center, y: center, width: 174, height: 72, central: true }]
|
|
454
|
+
rings.forEach(function (ring, index) {
|
|
455
|
+
ring.items.forEach(function (entity, itemIndex) {
|
|
456
|
+
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
|
|
457
|
+
positions.push({ entity: entity, x: center + Math.cos(angle) * ring.radius, y: center + Math.sin(angle) * ring.radius, width: nodeWidth, height: nodeHeight, central: false })
|
|
458
|
+
})
|
|
459
|
+
})
|
|
460
|
+
return { positions: positions, width: size, height: size }
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
function graphLabelLines(value) {
|
|
@@ -493,32 +503,57 @@ window.__ModuleLoader__.load({
|
|
|
493
503
|
var zoomPair = React.useState(1)
|
|
494
504
|
var zoom = zoomPair[0]
|
|
495
505
|
var setZoom = zoomPair[1]
|
|
506
|
+
var spacingPair = React.useState(1)
|
|
507
|
+
var spacing = spacingPair[0]
|
|
508
|
+
var setSpacing = spacingPair[1]
|
|
509
|
+
var localPair = React.useState(false)
|
|
510
|
+
var localOnly = localPair[0]
|
|
511
|
+
var setLocalOnly = localPair[1]
|
|
496
512
|
if (!graph) return React.createElement(EmptyState, { title: '尚未生成知识图谱', copy: '从标题栏打开知识视图并选择生成知识图谱。' })
|
|
497
|
-
var
|
|
513
|
+
var filteredEntities = graph.entities.filter(function (entity) {
|
|
498
514
|
return (confidence === 'all' || entity.confidence === confidence) && (!query.trim() || (entity.name + ' ' + entity.summary).toLowerCase().indexOf(query.trim().toLowerCase()) >= 0)
|
|
499
515
|
})
|
|
500
516
|
var visible = Object.create(null)
|
|
501
|
-
|
|
502
|
-
var
|
|
503
|
-
var
|
|
517
|
+
filteredEntities.forEach(function (entity) { visible[entity.id] = true })
|
|
518
|
+
var filteredRelations = graph.relations.filter(function (relation) { return visible[relation.from] && visible[relation.to] })
|
|
519
|
+
var localIds = Object.create(null)
|
|
520
|
+
if (localOnly && selected && visible[selected.id]) {
|
|
521
|
+
localIds[selected.id] = true
|
|
522
|
+
filteredRelations.forEach(function (relation) {
|
|
523
|
+
if (relation.from === selected.id || relation.to === selected.id) {
|
|
524
|
+
localIds[relation.from] = true
|
|
525
|
+
localIds[relation.to] = true
|
|
526
|
+
}
|
|
527
|
+
})
|
|
528
|
+
}
|
|
529
|
+
var entities = localOnly && selected && visible[selected.id] ? filteredEntities.filter(function (entity) { return localIds[entity.id] }) : filteredEntities
|
|
530
|
+
var shown = Object.create(null)
|
|
531
|
+
entities.forEach(function (entity) { shown[entity.id] = true })
|
|
532
|
+
var relations = filteredRelations.filter(function (relation) { return shown[relation.from] && shown[relation.to] })
|
|
533
|
+
var layout = graphLayout(entities, relations, spacing)
|
|
534
|
+
var positions = layout.positions
|
|
504
535
|
var byId = Object.create(null)
|
|
505
536
|
positions.forEach(function (item) { byId[item.entity.id] = item })
|
|
506
|
-
var
|
|
507
|
-
var viewHeight = 620 / zoom
|
|
508
|
-
var viewBox = [(900 - viewWidth) / 2, (620 - viewHeight) / 2, viewWidth, viewHeight].join(' ')
|
|
537
|
+
var viewBox = ['0', '0', layout.width, layout.height].join(' ')
|
|
509
538
|
return React.createElement('div', { className: 'ckm-workspace ckm-graph-layout' }, [
|
|
510
539
|
React.createElement('section', { key: 'graph', className: 'ckm-graph-canvas' }, [
|
|
511
540
|
React.createElement('div', { key: 'toolbar', className: 'ckm-graph-toolbar' }, [
|
|
512
541
|
React.createElement('input', { key: 'search', value: query, placeholder: '搜索实体…', onChange: function (event) { setQuery(event.target.value) } }),
|
|
513
542
|
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' }, '有冲突')]),
|
|
543
|
+
Button({ key: 'local', className: 'ckm-secondary', disabled: !selected, title: selected ? '只显示选中实体及其直接相邻实体' : '请先选择一个实体', onClick: function () { setLocalOnly(!localOnly) } }, localOnly ? '全部视图' : '局部一跳'),
|
|
544
|
+
React.createElement('div', { key: 'spacing', className: 'ckm-graph-zoom', 'aria-label': '知识图谱节点间距控制' }, [
|
|
545
|
+
Button({ key: 'compact', title: '缩小节点间距', disabled: spacing <= 0.8, onClick: function () { setSpacing(Math.max(0.8, Number((spacing - 0.2).toFixed(1)))) } }, '−'),
|
|
546
|
+
Button({ key: 'value', title: '重置节点间距', className: 'ckm-zoom-value', onClick: function () { setSpacing(1) } }, '间距 ' + Math.round(spacing * 100) + '%'),
|
|
547
|
+
Button({ key: 'loose', title: '增大节点间距', disabled: spacing >= 2.2, onClick: function () { setSpacing(Math.min(2.2, Number((spacing + 0.2).toFixed(1)))) } }, '+')
|
|
548
|
+
]),
|
|
514
549
|
React.createElement('div', { key: 'zoom', className: 'ckm-graph-zoom', 'aria-label': '知识图谱缩放控制' }, [
|
|
515
550
|
Button({ key: 'out', title: '缩小', 'aria-label': '缩小知识图谱', disabled: zoom <= 0.5, onClick: function () { setZoom(Math.max(0.5, Number((zoom - 0.1).toFixed(1)))) } }, '−'),
|
|
516
551
|
Button({ key: 'reset', title: '重置缩放', className: 'ckm-zoom-value', onClick: function () { setZoom(1) } }, Math.round(zoom * 100) + '%'),
|
|
517
552
|
Button({ key: 'in', title: '放大', 'aria-label': '放大知识图谱', disabled: zoom >= 2, onClick: function () { setZoom(Math.min(2, Number((zoom + 0.1).toFixed(1)))) } }, '+')
|
|
518
553
|
])
|
|
519
554
|
]),
|
|
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': '知识图谱' }, [
|
|
555
|
+
React.createElement('p', { key: 'static', className: 'ckm-panel-hint' }, '知识图谱是静态结果;画布缩放与节点间距可分别调整,选择实体后可查看一跳局部子图。只能通过菜单栏完整重新生成,不支持直接编辑或节点发散。'),
|
|
556
|
+
React.createElement('svg', { key: 'svg', 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': '知识图谱' }, [
|
|
522
557
|
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
558
|
React.createElement('g', { key: 'edges', className: 'ckm-graph-edges' }, relations.map(function (relation) {
|
|
524
559
|
var from = byId[relation.from]
|
|
@@ -714,6 +749,8 @@ window.__ModuleLoader__.load({
|
|
|
714
749
|
'.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
750
|
'@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
751
|
|
|
752
|
+
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}'
|
|
753
|
+
|
|
717
754
|
function apply(ctx) {
|
|
718
755
|
var sessions = ctx.sessions || (ctx.get && ctx.get('sessions'))
|
|
719
756
|
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
|
|
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
|
}
|
|
@@ -128,12 +128,85 @@ function findJsonObject(text, kind = '') {
|
|
|
128
128
|
return null
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function modelOutputText(value) {
|
|
132
|
+
const text = textFromContent(value)
|
|
133
|
+
if (text) return text
|
|
134
|
+
if (value && typeof value === 'object') {
|
|
135
|
+
try { return JSON.stringify(value) } catch { /* fall through to a printable scalar */ }
|
|
136
|
+
}
|
|
137
|
+
return String(value ?? '')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loggableModelOutput(value, max = 32000) {
|
|
141
|
+
const text = modelOutputText(value)
|
|
142
|
+
if (text.length <= max) return { text, loggedText: JSON.stringify(text), truncated: false }
|
|
143
|
+
const half = Math.floor(max / 2)
|
|
144
|
+
const bounded = `${text.slice(0, half)}\n…[中间 ${text.length - max} 个字符因日志长度限制被省略]…\n${text.slice(-half)}`
|
|
145
|
+
return { text, loggedText: JSON.stringify(bounded), truncated: true }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function jsonParseDiagnostics(value, kind = '') {
|
|
149
|
+
const text = modelOutputText(value).replace(/^\uFEFF/, '').trim()
|
|
150
|
+
const starts = []
|
|
151
|
+
for (let index = 0; index < text.length && starts.length < 12; index += 1) if (text[index] === '{') starts.push(index)
|
|
152
|
+
const candidates = []
|
|
153
|
+
for (const start of starts) {
|
|
154
|
+
let depth = 0
|
|
155
|
+
let inString = false
|
|
156
|
+
let escaped = false
|
|
157
|
+
let end = -1
|
|
158
|
+
for (let index = start; index < text.length; index += 1) {
|
|
159
|
+
const char = text[index]
|
|
160
|
+
if (inString) {
|
|
161
|
+
if (escaped) escaped = false
|
|
162
|
+
else if (char === '\\') escaped = true
|
|
163
|
+
else if (char === '"') inString = false
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
if (char === '"') inString = true
|
|
167
|
+
else if (char === '{') depth += 1
|
|
168
|
+
else if (char === '}' && --depth === 0) { end = index; break }
|
|
169
|
+
}
|
|
170
|
+
if (end < 0) {
|
|
171
|
+
candidates.push({ start, status: 'unclosed-object' })
|
|
172
|
+
continue
|
|
173
|
+
}
|
|
174
|
+
const fragment = text.slice(start, end + 1)
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(fragment)
|
|
177
|
+
candidates.push({
|
|
178
|
+
start,
|
|
179
|
+
end,
|
|
180
|
+
status: expectedObject(parsed, kind) ? 'valid-expected-shape' : 'valid-json-wrong-shape',
|
|
181
|
+
keys: parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? Object.keys(parsed).slice(0, 20) : []
|
|
182
|
+
})
|
|
183
|
+
} catch (error) {
|
|
184
|
+
candidates.push({ start, end, status: 'invalid-json', error: shortText(errorMessage(error), 300) })
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
kind,
|
|
189
|
+
textLength: text.length,
|
|
190
|
+
fencedJson: /```(?:json)?\s*[\s\S]*?```/i.test(text),
|
|
191
|
+
firstBrace: text.indexOf('{'),
|
|
192
|
+
lastBrace: text.lastIndexOf('}'),
|
|
193
|
+
candidateCountInspected: candidates.length,
|
|
194
|
+
candidates
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
131
198
|
export function parseStructuredOutput(value, kind = '') {
|
|
132
199
|
const object = asObject(value)
|
|
133
|
-
if (object && expectedObject(object, kind))
|
|
200
|
+
if (object && expectedObject(object, kind)) {
|
|
201
|
+
if (kind === 'knowledge-graph' && object.relations === undefined) return { ...object, relations: [] }
|
|
202
|
+
return object
|
|
203
|
+
}
|
|
134
204
|
const text = textFromContent(value).replace(/^\uFEFF/, '').trim()
|
|
135
205
|
const parsed = findJsonObject(text, kind)
|
|
136
|
-
if (parsed)
|
|
206
|
+
if (parsed) {
|
|
207
|
+
if (kind === 'knowledge-graph' && parsed.relations === undefined) return { ...parsed, relations: [] }
|
|
208
|
+
return parsed
|
|
209
|
+
}
|
|
137
210
|
if (!text) throw new Error('模型没有返回 JSON 对象。')
|
|
138
211
|
throw new Error('模型返回了文本,但其中没有可解析的 JSON 对象。')
|
|
139
212
|
}
|
|
@@ -876,6 +949,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
876
949
|
this.recordTimeline(task, 'view-start', `开始分 ${graphBatches.length} 批生成知识图谱(最多 ${GRAPH_CONCURRENCY} 批并行)。`)
|
|
877
950
|
this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: 0, total: graphBatches.length, label: '分批生成知识图谱' } })
|
|
878
951
|
let completedGraphBatches = 0
|
|
952
|
+
let graphRetryCount = 0
|
|
879
953
|
const graphBatchResults = await mapWithConcurrency(graphBatches, GRAPH_CONCURRENCY, async (batch, batchIndex) => {
|
|
880
954
|
let graphError = null
|
|
881
955
|
for (let attempt = 0; attempt <= MAX_MODEL_RETRIES; attempt += 1) {
|
|
@@ -900,7 +974,8 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
900
974
|
graphError = error
|
|
901
975
|
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))
|
|
902
976
|
if (attempt < MAX_MODEL_RETRIES) {
|
|
903
|
-
|
|
977
|
+
graphRetryCount += 1
|
|
978
|
+
this.recordTimeline(task, 'retry', `知识图谱第 ${batchIndex + 1}/${graphBatches.length} 批输出无效(${shortText(errorMessage(graphError), 160)}),正在降低本批输出预算并重试 ${attempt + 1}/${MAX_MODEL_RETRIES}。`)
|
|
904
979
|
}
|
|
905
980
|
}
|
|
906
981
|
}
|
|
@@ -914,7 +989,8 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
914
989
|
knowledgeGraph = validateKnowledgeGraph(mergedGraph, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
915
990
|
const rawEntities = successfulGraphBatches.reduce((total, result) => total + result.rawEntities, 0)
|
|
916
991
|
const rawRelations = successfulGraphBatches.reduce((total, result) => total + result.rawRelations, 0)
|
|
917
|
-
|
|
992
|
+
const retrySummary = graphRetryCount ? `;期间发生 ${graphRetryCount} 次批次重试${successfulGraphBatches.length === graphBatches.length ? ',已全部恢复' : ''}` : ''
|
|
993
|
+
this.recordTimeline(task, 'graph-coverage', `知识图谱 ${successfulGraphBatches.length}/${graphBatches.length} 批成功,模型共返回 ${rawEntities} 个实体、${rawRelations} 条关系;合并去重后保留 ${knowledgeGraph.entities.length} 个实体、${knowledgeGraph.relations.length} 条关系${retrySummary}。`, { entityLimit: graphBudget.entities, relationLimit: graphBudget.relations })
|
|
918
994
|
this.recordTimeline(task, 'view-complete', '知识图谱分批生成与合并完成。')
|
|
919
995
|
} else {
|
|
920
996
|
const graphError = graphBatchResults.find((result) => result.error)?.error || new Error('所有知识图谱批次均生成失败。')
|
|
@@ -952,7 +1028,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
952
1028
|
knowledgeGraph
|
|
953
1029
|
})
|
|
954
1030
|
task.revision = saved.revision
|
|
955
|
-
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
|
|
1031
|
+
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap: saved.mindMap, knowledgeGraph: saved.knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
|
|
956
1032
|
this.recordTimeline(task, 'complete', '知识视图已保存,生成流程完成。')
|
|
957
1033
|
logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
|
|
958
1034
|
this.update(task, 'completed', {
|
|
@@ -976,13 +1052,15 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
976
1052
|
|
|
977
1053
|
async runModel(input) {
|
|
978
1054
|
const parseModelOutput = (value, source, diagnostics = {}) => {
|
|
1055
|
+
const output = loggableModelOutput(value)
|
|
1056
|
+
logMessage(this.logger, 'info', 'agent output content kind=%s source=%s length=%d truncated=%s content=%s', input.kind, source, output.text.length, output.truncated, output.loggedText)
|
|
979
1057
|
try {
|
|
980
1058
|
const result = parseStructuredOutput(value, input.kind)
|
|
981
1059
|
logMessage(this.logger, 'info', 'agent output parsed kind=%s source=%s value=%s result=%s', input.kind, source, diagnosticSummary(value), diagnosticSummary(result))
|
|
982
1060
|
return result
|
|
983
1061
|
} catch (error) {
|
|
984
1062
|
const surfacedError = diagnostics.agentLimit || error
|
|
985
|
-
logMessage(this.logger, 'error', 'agent output parse failed kind=%s source=%s value=%s
|
|
1063
|
+
logMessage(this.logger, 'error', 'agent output parse failed kind=%s source=%s value=%s parseDiagnostics=%s runtimeDiagnostics=%s error=%s', input.kind, source, diagnosticSummary(value), JSON.stringify(jsonParseDiagnostics(value, input.kind)), JSON.stringify(diagnostics), errorMessage(surfacedError))
|
|
986
1064
|
throw surfacedError
|
|
987
1065
|
}
|
|
988
1066
|
}
|
package/lib/workspace-storage.js
CHANGED
|
@@ -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:
|
|
198
|
-
knowledgeGraph:
|
|
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 {
|