@p-dsh-market/conversation-knowledge-map 0.1.11 → 0.1.13
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 +3 -1
- package/lib/client.js +71 -38
- package/lib/generation-orchestrator.js +138 -59
- package/lib/session-source.js +74 -7
- package/lib/workspace-storage.js +12 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,9 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
|
|
|
11
11
|
|
|
12
12
|
配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
|
|
13
13
|
|
|
14
|
-
摘要阶段按对话并行处理,并发上限固定为 3
|
|
14
|
+
摘要阶段按对话并行处理,并发上限固定为 3;每个对话按约 5000 字、优先在完整段落边界分段,只有单段本身超长时才按句末或硬边界拆分。同一对话的分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、具体失败原因、重试、跳过、合并、生成及保存过程。模型可以保留 thinking,但结果提取只读取最终文本并过滤 reasoning 内容;摘要和最终视图分别预留 12000 与 24000 个输出 Token。知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
|
|
15
|
+
|
|
16
|
+
生成过程时间线支持折叠和展开:进行中的任务默认展开,已完成结果中的历史时间线默认折叠。思维导图使用递归树形布局并绘制父子连接线;知识图谱提供 50%–200% 缩放和一键恢复 100%,缩放不会影响实体筛选、点击和详情查看。
|
|
15
17
|
|
|
16
18
|
节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
|
|
17
19
|
|
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, defaultCollapsed: true }) : 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,40 @@ 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
|
+
var collapsedPair = React.useState(props.defaultCollapsed === true)
|
|
296
|
+
var collapsed = collapsedPair[0]
|
|
297
|
+
var setCollapsed = collapsedPair[1]
|
|
298
|
+
if (!items.length) return null
|
|
299
|
+
return React.createElement('section', { className: 'ckm-timeline', 'aria-label': '生成过程时间线' }, [
|
|
300
|
+
React.createElement('div', { key: 'head', className: 'ckm-timeline-head' }, [
|
|
301
|
+
React.createElement('strong', { key: 'title', className: 'ckm-timeline-title' }, '生成过程时间线 · ' + items.length + ' 条'),
|
|
302
|
+
Button({ key: 'toggle', className: 'ckm-timeline-toggle', 'aria-expanded': !collapsed, onClick: function () { setCollapsed(!collapsed) } }, collapsed ? '展开' : '折叠')
|
|
303
|
+
]),
|
|
304
|
+
collapsed ? null : React.createElement('ol', { key: 'items' }, items.map(function (item, index) {
|
|
305
|
+
var time = ''
|
|
306
|
+
try { time = new Date(Number(item.at) || 0).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } catch (_) {}
|
|
307
|
+
return React.createElement('li', { key: String(item.id || index), 'data-type': item.type || 'info' }, [
|
|
308
|
+
React.createElement('time', { key: 'time' }, time),
|
|
309
|
+
React.createElement('span', { key: 'message' }, item.message)
|
|
310
|
+
])
|
|
286
311
|
}))
|
|
287
312
|
])
|
|
288
313
|
}
|
|
@@ -291,16 +316,6 @@ window.__ModuleLoader__.load({
|
|
|
291
316
|
var generation = props.store.generation
|
|
292
317
|
var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
|
|
293
318
|
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
319
|
return React.createElement('div', { className: 'ckm-generation-strip', 'data-status': generation.status }, [
|
|
305
320
|
React.createElement('div', { key: 'body', className: 'ckm-generation-body' }, [
|
|
306
321
|
React.createElement('div', { key: 'status-row', className: 'ckm-generation-status-row' }, [
|
|
@@ -308,36 +323,46 @@ window.__ModuleLoader__.load({
|
|
|
308
323
|
React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
|
|
309
324
|
]),
|
|
310
325
|
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
|
-
|
|
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
|
|
326
|
+
React.createElement(GenerationTimeline, { key: 'activity', items: generation.timeline || [] })
|
|
317
327
|
]),
|
|
318
328
|
generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
|
|
319
329
|
['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) }) } }, '取消')
|
|
320
330
|
])
|
|
321
331
|
}
|
|
322
332
|
|
|
323
|
-
function
|
|
333
|
+
function mindTree(map) {
|
|
324
334
|
var nodes = (map && map.nodes) || []
|
|
325
335
|
var byParent = Object.create(null)
|
|
326
336
|
nodes.forEach(function (node) { var key = node.parentId || ''; (byParent[key] || (byParent[key] = [])).push(node) })
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
337
|
+
function branch(node, ancestors) {
|
|
338
|
+
var seen = Object.assign({}, ancestors || {})
|
|
339
|
+
if (seen[node.id]) return { node: node, children: [] }
|
|
340
|
+
seen[node.id] = true
|
|
341
|
+
return { node: node, children: (byParent[node.id] || []).map(function (child) { return branch(child, seen) }) }
|
|
331
342
|
}
|
|
332
343
|
var root = nodes.filter(function (node) { return node.id === map.rootId })[0]
|
|
333
|
-
|
|
334
|
-
|
|
344
|
+
return root ? branch(root, {}) : null
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function MindTreeBranch(props) {
|
|
348
|
+
var branch = props.branch
|
|
349
|
+
var node = branch.node
|
|
350
|
+
return React.createElement('li', { className: 'ckm-tree-branch' }, [
|
|
351
|
+
Button({ key: 'node', className: 'ckm-tree-node', 'data-active': node.id === props.selectedNodeId, onClick: function () { props.setSelectedNodeId(node.id) } }, [
|
|
352
|
+
React.createElement('span', { key: 'type', className: 'ckm-node-type' }, node.type),
|
|
353
|
+
React.createElement('strong', { key: 'title' }, node.title),
|
|
354
|
+
React.createElement('span', { key: 'narrative' }, node.narrative)
|
|
355
|
+
]),
|
|
356
|
+
branch.children.length ? React.createElement('ul', { key: 'children', className: 'ckm-tree-children' }, branch.children.map(function (child) {
|
|
357
|
+
return React.createElement(MindTreeBranch, { key: child.node.id, branch: child, selectedNodeId: props.selectedNodeId, setSelectedNodeId: props.setSelectedNodeId })
|
|
358
|
+
})) : null
|
|
359
|
+
])
|
|
335
360
|
}
|
|
336
361
|
|
|
337
362
|
function MindMapPanel(props) {
|
|
338
363
|
var map = props.store.state && props.store.state.mindMap
|
|
339
364
|
var selected = map && map.nodes.filter(function (node) { return node.id === props.selectedNodeId })[0]
|
|
340
|
-
var
|
|
365
|
+
var tree = map ? mindTree(map) : null
|
|
341
366
|
var followPair = React.useState(null)
|
|
342
367
|
var followUp = followPair[0]
|
|
343
368
|
var setFollowUp = followPair[1]
|
|
@@ -355,13 +380,7 @@ window.__ModuleLoader__.load({
|
|
|
355
380
|
return React.createElement('div', { className: 'ckm-workspace' }, [
|
|
356
381
|
React.createElement('section', { key: 'canvas', className: 'ckm-mind-canvas' }, [
|
|
357
382
|
React.createElement('div', { key: 'hint', className: 'ckm-panel-hint' }, '节点代表阶段性认知;点击后可形成问题,但不会直接修改脑图。'),
|
|
358
|
-
React.createElement('
|
|
359
|
-
return Button({ key: row.node.id, className: 'ckm-tree-node', 'data-active': row.node.id === props.selectedNodeId, style: { marginLeft: (row.depth * 24) + 'px' }, onClick: function () { props.setSelectedNodeId(row.node.id) } }, [
|
|
360
|
-
React.createElement('span', { key: 'type', className: 'ckm-node-type' }, row.node.type),
|
|
361
|
-
React.createElement('strong', { key: 'title' }, row.node.title),
|
|
362
|
-
React.createElement('span', { key: 'narrative' }, row.node.narrative)
|
|
363
|
-
])
|
|
364
|
-
}))
|
|
383
|
+
tree ? React.createElement('ul', { key: 'tree', className: 'ckm-tree ckm-tree-root' }, React.createElement(MindTreeBranch, { branch: tree, selectedNodeId: props.selectedNodeId, setSelectedNodeId: props.setSelectedNodeId })) : null
|
|
365
384
|
]),
|
|
366
385
|
React.createElement('aside', { key: 'detail', className: 'ckm-detail' }, [
|
|
367
386
|
selected ? [
|
|
@@ -416,6 +435,9 @@ window.__ModuleLoader__.load({
|
|
|
416
435
|
var selectedPair = React.useState(null)
|
|
417
436
|
var selected = selectedPair[0]
|
|
418
437
|
var setSelected = selectedPair[1]
|
|
438
|
+
var zoomPair = React.useState(1)
|
|
439
|
+
var zoom = zoomPair[0]
|
|
440
|
+
var setZoom = zoomPair[1]
|
|
419
441
|
if (!graph) return React.createElement(EmptyState, { title: '尚未生成知识图谱', copy: '从标题栏打开知识视图并选择生成知识图谱。' })
|
|
420
442
|
var entities = graph.entities.filter(function (entity) {
|
|
421
443
|
return (confidence === 'all' || entity.confidence === confidence) && (!query.trim() || (entity.name + ' ' + entity.summary).toLowerCase().indexOf(query.trim().toLowerCase()) >= 0)
|
|
@@ -429,11 +451,22 @@ window.__ModuleLoader__.load({
|
|
|
429
451
|
var byId = Object.create(null)
|
|
430
452
|
positions.forEach(function (item) { byId[item.entity.id] = item })
|
|
431
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(' ')
|
|
432
457
|
return React.createElement('div', { className: 'ckm-workspace ckm-graph-layout' }, [
|
|
433
458
|
React.createElement('section', { key: 'graph', className: 'ckm-graph-canvas' }, [
|
|
434
|
-
React.createElement('div', { key: 'toolbar', className: 'ckm-graph-toolbar' }, [
|
|
435
|
-
|
|
436
|
-
|
|
459
|
+
React.createElement('div', { key: 'toolbar', className: 'ckm-graph-toolbar' }, [
|
|
460
|
+
React.createElement('input', { key: 'search', value: query, placeholder: '搜索实体…', onChange: function (event) { setQuery(event.target.value) } }),
|
|
461
|
+
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' }, '有冲突')]),
|
|
462
|
+
React.createElement('div', { key: 'zoom', className: 'ckm-graph-zoom', 'aria-label': '知识图谱缩放控制' }, [
|
|
463
|
+
Button({ key: 'out', title: '缩小', 'aria-label': '缩小知识图谱', disabled: zoom <= 0.5, onClick: function () { setZoom(Math.max(0.5, Number((zoom - 0.1).toFixed(1)))) } }, '−'),
|
|
464
|
+
Button({ key: 'reset', title: '重置缩放', className: 'ckm-zoom-value', onClick: function () { setZoom(1) } }, Math.round(zoom * 100) + '%'),
|
|
465
|
+
Button({ key: 'in', title: '放大', 'aria-label': '放大知识图谱', disabled: zoom >= 2, onClick: function () { setZoom(Math.min(2, Number((zoom + 0.1).toFixed(1)))) } }, '+')
|
|
466
|
+
])
|
|
467
|
+
]),
|
|
468
|
+
React.createElement('p', { key: 'static', className: 'ckm-panel-hint' }, '知识图谱是静态结果,可缩放查看;只能通过菜单栏完整重新生成,不支持直接编辑或节点发散。'),
|
|
469
|
+
React.createElement('svg', { key: 'svg', className: 'ckm-graph-svg', viewBox: viewBox, role: 'img', 'aria-label': '知识图谱' }, [
|
|
437
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' }) }),
|
|
438
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))]) })
|
|
439
472
|
])
|
|
@@ -608,9 +641,9 @@ window.__ModuleLoader__.load({
|
|
|
608
641
|
'.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
642
|
'.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
643
|
'.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-
|
|
612
|
-
'.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{
|
|
613
|
-
'.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}' +
|
|
644
|
+
'.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}' +
|
|
614
647
|
'.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)}' +
|
|
615
648
|
'@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}}'
|
|
616
649
|
|
|
@@ -6,9 +6,10 @@ import { clone, diagnosticSummary, errorMessage, logMessage, makeUserMessage, sh
|
|
|
6
6
|
import { chunkSourceText, readSelectedSurfaces } from './session-source.js'
|
|
7
7
|
|
|
8
8
|
const MAX_SUMMARY_CHARS = 2400
|
|
9
|
-
const SUMMARY_MAX_TOKENS =
|
|
10
|
-
const VIEW_MAX_TOKENS =
|
|
9
|
+
const SUMMARY_MAX_TOKENS = 12000
|
|
10
|
+
const VIEW_MAX_TOKENS = 24000
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
648
|
+
selectedSessionIds: loadedSessionIds, model: request.model, signal: task.controller.signal
|
|
634
649
|
})
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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', `摘要失败(${shortText(errorMessage(finalError), 160)}),重置重试 ${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
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
})
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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', `思维导图生成失败(${shortText(errorMessage(mindMapError), 160)}),重置重试 ${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
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
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', `知识图谱生成失败(${shortText(errorMessage(graphError), 160)}),重置重试 ${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 },
|
|
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:
|
|
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: `知识视图生成完成,已总结 ${
|
|
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
|
}
|
|
@@ -835,7 +914,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
835
914
|
handle = await this.agents.create({
|
|
836
915
|
sessionId,
|
|
837
916
|
meta: { cwd: input.cwd, origin: 'subagent' },
|
|
838
|
-
agentOptions: { provider, model,
|
|
917
|
+
agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? SUMMARY_MAX_TOKENS : VIEW_MAX_TOKENS },
|
|
839
918
|
signal: input.signal,
|
|
840
919
|
setup: async (agentCtx) => {
|
|
841
920
|
agentCtx?.systemPrompt?.section?.({
|
package/lib/session-source.js
CHANGED
|
@@ -206,19 +206,86 @@ export async function readSelectedSurfaces({ sessionQuery, sessions }, { cwd, se
|
|
|
206
206
|
return sources
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
export
|
|
209
|
+
export const DEFAULT_SOURCE_CHUNK_CHARS = 5000
|
|
210
|
+
|
|
211
|
+
function paragraphRanges(text) {
|
|
212
|
+
const ranges = []
|
|
213
|
+
const separator = /\r?\n[\t ]*\r?\n/g
|
|
214
|
+
let start = 0
|
|
215
|
+
let match
|
|
216
|
+
while ((match = separator.exec(text)) !== null) {
|
|
217
|
+
const end = match.index + match[0].length
|
|
218
|
+
ranges.push({ start, end })
|
|
219
|
+
start = end
|
|
220
|
+
}
|
|
221
|
+
if (start < text.length) ranges.push({ start, end: text.length })
|
|
222
|
+
return ranges
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function splitOversizedRange(text, range, maxChars) {
|
|
226
|
+
const ranges = []
|
|
227
|
+
let start = range.start
|
|
228
|
+
while (start < range.end) {
|
|
229
|
+
let end = Math.min(range.end, start + maxChars)
|
|
230
|
+
if (end < range.end) {
|
|
231
|
+
const candidate = text.slice(start, end)
|
|
232
|
+
const minimumBoundary = Math.floor(maxChars * 0.6)
|
|
233
|
+
let boundary = Math.max(candidate.lastIndexOf('\n'), candidate.lastIndexOf('。'), candidate.lastIndexOf('!'), candidate.lastIndexOf('?'), candidate.lastIndexOf(';'))
|
|
234
|
+
if (boundary + 1 >= minimumBoundary) end = start + boundary + 1
|
|
235
|
+
}
|
|
236
|
+
ranges.push({ start, end })
|
|
237
|
+
start = end
|
|
238
|
+
}
|
|
239
|
+
return ranges
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function sourceEventRanges(source, text) {
|
|
243
|
+
const ranges = []
|
|
244
|
+
let cursor = 0
|
|
245
|
+
for (const event of source.events || []) {
|
|
246
|
+
const eventText = String(event?.text || '')
|
|
247
|
+
if (!eventText) continue
|
|
248
|
+
let start = text.indexOf(eventText, cursor)
|
|
249
|
+
if (start < 0) start = text.indexOf(eventText)
|
|
250
|
+
if (start < 0) continue
|
|
251
|
+
const end = start + eventText.length
|
|
252
|
+
ranges.push({ start, end, seq: event.seq })
|
|
253
|
+
cursor = end
|
|
254
|
+
}
|
|
255
|
+
return ranges
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function chunkSourceText(source, maxChars = DEFAULT_SOURCE_CHUNK_CHARS) {
|
|
210
259
|
const text = String(source?.text || '')
|
|
211
260
|
if (!text) return [{ text: '', sourceRefs: [] }]
|
|
261
|
+
const limit = Math.max(1, Number(maxChars) || DEFAULT_SOURCE_CHUNK_CHARS)
|
|
262
|
+
const units = paragraphRanges(text).flatMap((range) => (
|
|
263
|
+
range.end - range.start > limit ? splitOversizedRange(text, range, limit) : [range]
|
|
264
|
+
))
|
|
265
|
+
const ranges = []
|
|
266
|
+
let current = null
|
|
267
|
+
for (const unit of units) {
|
|
268
|
+
if (!current) {
|
|
269
|
+
current = { ...unit }
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
if (unit.end - current.start <= limit) {
|
|
273
|
+
current.end = unit.end
|
|
274
|
+
} else {
|
|
275
|
+
ranges.push(current)
|
|
276
|
+
current = { ...unit }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (current) ranges.push(current)
|
|
280
|
+
|
|
281
|
+
const eventRanges = sourceEventRanges(source, text)
|
|
212
282
|
const chunks = []
|
|
213
|
-
|
|
214
|
-
while (start < text.length) {
|
|
215
|
-
const end = Math.min(text.length, start + maxChars)
|
|
283
|
+
for (const { start, end } of ranges) {
|
|
216
284
|
const chunkText = text.slice(start, end)
|
|
217
|
-
const refs =
|
|
218
|
-
.filter((event) => event.
|
|
285
|
+
const refs = eventRanges
|
|
286
|
+
.filter((event) => event.start < end && event.end > start)
|
|
219
287
|
.map((event) => ({ sessionId: source.sessionId, eventSeqs: [event.seq] }))
|
|
220
288
|
chunks.push({ text: chunkText, sourceRefs: refs })
|
|
221
|
-
start = end
|
|
222
289
|
}
|
|
223
290
|
return chunks
|
|
224
291
|
}
|
package/lib/workspace-storage.js
CHANGED
|
@@ -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'),
|