@p-dsh-market/conversation-knowledge-map 0.1.9 → 0.1.11
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 +39 -2
- package/lib/generation-orchestrator.js +164 -14
- package/lib/session-source.js +1 -1
- package/lib/workspace-storage.js +9 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,8 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
|
|
|
11
11
|
|
|
12
12
|
配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
|
|
13
13
|
|
|
14
|
+
摘要阶段按对话并行处理,并发上限固定为 3;同一对话的超长分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。进度区域同时显示最近处理动态。结构化生成关闭推理输出以减少等待和 Token 消耗;知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话和过滤数量。
|
|
15
|
+
|
|
14
16
|
节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
|
|
15
17
|
|
|
16
18
|
## 生成失败诊断
|
|
@@ -25,4 +27,4 @@ node --check market/conversation-knowledge-map/lib/index.js
|
|
|
25
27
|
node --check market/conversation-knowledge-map/lib/client.js
|
|
26
28
|
```
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
仓库测试通过后可同步到 DSH Web Profile;真实 DSH Web 视觉回放仍需重启 Runtime 后验证。
|
package/lib/client.js
CHANGED
|
@@ -260,23 +260,60 @@ window.__ModuleLoader__.load({
|
|
|
260
260
|
]),
|
|
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
|
+
state && state.manifest ? React.createElement(SourceSummary, { key: 'sources', manifest: state.manifest }) : null,
|
|
263
264
|
!state || (!state.mindMap && !state.knowledgeGraph) ? React.createElement(EmptyState, { key: 'empty', title: '尚未生成知识视图', copy: '从标题栏打开“知识视图”,选择同工作路径下的对话并确认生成。' }) : (mode === 'mind-map'
|
|
264
265
|
? React.createElement(MindMapPanel, { key: 'mind', store: store, selectedNodeId: selectedNodeId, setSelectedNodeId: setSelectedNodeId })
|
|
265
266
|
: React.createElement(KnowledgeGraphPanel, { key: 'graph', store: store }))
|
|
266
267
|
])
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
function SourceSummary(props) {
|
|
271
|
+
var manifest = props.manifest || {}
|
|
272
|
+
var sources = Array.isArray(manifest.sourceSessions) && manifest.sourceSessions.length
|
|
273
|
+
? manifest.sourceSessions
|
|
274
|
+
: (manifest.sourceSessionIds || []).map(function (sessionId) { return { sessionId: sessionId, title: '' } })
|
|
275
|
+
var warnings = manifest.sourceWarnings || {}
|
|
276
|
+
if (!sources.length) return null
|
|
277
|
+
return React.createElement('section', { className: 'ckm-source-summary', 'aria-label': '本次总结来源' }, [
|
|
278
|
+
React.createElement('div', { key: 'head', className: 'ckm-source-summary-head' }, [
|
|
279
|
+
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
|
|
281
|
+
]),
|
|
282
|
+
React.createElement('div', { key: 'list', className: 'ckm-source-list' }, sources.map(function (source) {
|
|
283
|
+
var sessionId = String(source.sessionId || '')
|
|
284
|
+
var label = source.title ? source.title + ' · ' + sessionId.slice(0, 16) + '…' : sessionId
|
|
285
|
+
return React.createElement('span', { key: sessionId, className: 'ckm-source-chip', title: sessionId }, label)
|
|
286
|
+
}))
|
|
287
|
+
])
|
|
288
|
+
}
|
|
289
|
+
|
|
269
290
|
function GenerationStrip(props) {
|
|
270
291
|
var generation = props.store.generation
|
|
271
292
|
var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
|
|
272
293
|
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()
|
|
273
304
|
return React.createElement('div', { className: 'ckm-generation-strip', 'data-status': generation.status }, [
|
|
274
305
|
React.createElement('div', { key: 'body', className: 'ckm-generation-body' }, [
|
|
275
306
|
React.createElement('div', { key: 'status-row', className: 'ckm-generation-status-row' }, [
|
|
276
307
|
React.createElement('strong', { key: 'status' }, generation.message || generation.status),
|
|
277
308
|
React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
|
|
278
309
|
]),
|
|
279
|
-
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 + '%' } }))
|
|
310
|
+
React.createElement('div', { key: 'progress', className: 'ckm-progress-track', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': percent, 'aria-label': '知识视图生成进度' }, React.createElement('span', { className: 'ckm-progress-value', style: { width: percent + '%' } })),
|
|
311
|
+
activities.length ? React.createElement('div', { key: 'activity', className: 'ckm-progress-activity' }, [
|
|
312
|
+
React.createElement('strong', { key: 'title' }, '最近处理动态'),
|
|
313
|
+
React.createElement('ul', { key: 'list' }, activities.map(function (activity, index) {
|
|
314
|
+
return React.createElement('li', { key: activity.status + '-' + activity.label + '-' + index, 'data-status': activity.status }, activity.label)
|
|
315
|
+
}))
|
|
316
|
+
]) : null
|
|
280
317
|
]),
|
|
281
318
|
generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
|
|
282
319
|
['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) }) } }, '取消')
|
|
@@ -571,7 +608,7 @@ window.__ModuleLoader__.load({
|
|
|
571
608
|
'.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}' +
|
|
572
609
|
'.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}' +
|
|
573
610
|
'.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}' +
|
|
574
|
-
'.ckm-generation-strip{display:flex;align-items:
|
|
611
|
+
'.ckm-generation-strip{display:flex;align-items:flex-start;gap:12px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-state-business-tertiary);font-size:12px}.ckm-generation-body{display:grid;gap:7px;flex:1;min-width:0}.ckm-generation-status-row{display:flex;justify-content:space-between;gap:12px}.ckm-generation-status-row span{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ckm-progress-track{height:6px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.ckm-progress-value{display:block;height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.ckm-progress-activity{display:grid;grid-template-columns:auto 1fr;gap:6px 12px;color:var(--dsw-alias-label-secondary)}.ckm-progress-activity ul{display:flex;flex-wrap:wrap;gap:5px 12px;margin:0;padding:0;list-style:none}.ckm-progress-activity li:before{content:"·";margin-right:5px;color:var(--dsw-alias-state-business-primary)}.ckm-generation-error{color:#ff9898;max-width:38%;overflow-wrap:anywhere}.ckm-generation-strip .ckm-danger{margin-left:auto}.ckm-source-summary{display:grid;gap:8px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}.ckm-source-summary-head{display:flex;align-items:center;justify-content:space-between;gap:12px;font-size:12px}.ckm-source-warning{color:#d69a42}.ckm-source-list{display:flex;flex-wrap:wrap;gap:6px}.ckm-source-chip{max-width:360px;padding:4px 8px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;color:var(--dsw-alias-label-secondary);font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
|
|
575
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{display:flex;flex-direction:column;gap:8px;max-width:900px;margin:0 auto}.ckm-tree-node{display:flex;flex-direction:column;align-items:flex-start;gap:5px;width:calc(100% - 0px);padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);text-align:left;cursor:pointer}.ckm-tree-node[data-active=true]{border-color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}.ckm-tree-node:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-tree-node strong{font-size:13px}.ckm-tree-node span:last-child{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.65}.ckm-node-type{display:inline-block;color:var(--dsw-alias-state-business-primary);font-size:10px;letter-spacing:.04em;text-transform:uppercase}.ckm-detail-head{display:flex;flex-direction:column;gap:5px}.ckm-detail h3{margin:0;font-size:16px;line-height:1.45}.ckm-narrative{font-size:13px;line-height:1.8}.ckm-source-box{margin:16px 0;padding:12px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-base);font-size:11px;line-height:1.6}.ckm-source-box strong{display:block;margin-bottom:5px}.ckm-source-box ul{margin:0;padding-left:18px;color:var(--dsw-alias-label-secondary)}.ckm-detail-empty{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.7}' +
|
|
576
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}' +
|
|
577
614
|
'.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)}' +
|
|
@@ -8,6 +8,7 @@ import { chunkSourceText, readSelectedSurfaces } from './session-source.js'
|
|
|
8
8
|
const MAX_SUMMARY_CHARS = 2400
|
|
9
9
|
const SUMMARY_MAX_TOKENS = 8000
|
|
10
10
|
const VIEW_MAX_TOKENS = 12000
|
|
11
|
+
const SUMMARY_CONCURRENCY = 3
|
|
11
12
|
|
|
12
13
|
function modelLabel(selection) {
|
|
13
14
|
const provider = String(selection?.provider || '').trim()
|
|
@@ -164,6 +165,127 @@ function normalizeSummary(value, source, chunk) {
|
|
|
164
165
|
}
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
function mergeSourceSummaries(source, chunkSummaries) {
|
|
169
|
+
if (chunkSummaries.length === 1) return chunkSummaries[0]
|
|
170
|
+
const summary = shortText(chunkSummaries.map((item) => item.summary).filter(Boolean).join('\n'), MAX_SUMMARY_CHARS)
|
|
171
|
+
const keyPoints = [...new Set(chunkSummaries.flatMap((item) => item.keyPoints || []).map((item) => shortText(item, 500)).filter(Boolean))].slice(0, 12)
|
|
172
|
+
const eventSeqs = [...new Set(chunkSummaries.flatMap((item) => item.sourceRefs || [])
|
|
173
|
+
.filter((ref) => ref.sessionId === source.sessionId)
|
|
174
|
+
.flatMap((ref) => ref.eventSeqs || []))].slice(0, 24)
|
|
175
|
+
const sourceRefs = eventSeqs.length ? [{ sessionId: source.sessionId, eventSeqs }] : []
|
|
176
|
+
return { sessionId: source.sessionId, title: source.title, summary, keyPoints, sourceRefs }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
180
|
+
const results = new Array(items.length)
|
|
181
|
+
let nextIndex = 0
|
|
182
|
+
let failure = null
|
|
183
|
+
const worker = async () => {
|
|
184
|
+
while (!failure) {
|
|
185
|
+
const index = nextIndex
|
|
186
|
+
nextIndex += 1
|
|
187
|
+
if (index >= items.length) return
|
|
188
|
+
try {
|
|
189
|
+
results[index] = await mapper(items[index], index)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
failure ||= error
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const workerCount = Math.min(Math.max(1, limit), items.length)
|
|
196
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()))
|
|
197
|
+
if (failure) throw failure
|
|
198
|
+
return results
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function filterSourceRefs(refs, sources) {
|
|
202
|
+
const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
|
|
203
|
+
let skipped = 0
|
|
204
|
+
const filtered = []
|
|
205
|
+
for (const ref of Array.isArray(refs) ? refs : []) {
|
|
206
|
+
const sessionId = String(ref?.sessionId || '')
|
|
207
|
+
const allowedSeqs = allowed.get(sessionId)
|
|
208
|
+
if (!allowedSeqs) {
|
|
209
|
+
skipped += 1
|
|
210
|
+
continue
|
|
211
|
+
}
|
|
212
|
+
const eventSeqs = [...new Set((Array.isArray(ref?.eventSeqs) ? ref.eventSeqs : []).filter((seq) => Number.isInteger(seq) && allowedSeqs.has(seq)))]
|
|
213
|
+
if (!eventSeqs.length) {
|
|
214
|
+
skipped += 1
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
filtered.push({ sessionId, eventSeqs })
|
|
218
|
+
}
|
|
219
|
+
return { refs: filtered.slice(0, 12), skipped }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sanitizeMindMapSources(value, sources, strict) {
|
|
223
|
+
const rawNodes = Array.isArray(value?.nodes) ? value.nodes : []
|
|
224
|
+
const candidates = rawNodes.map((raw, index) => ({ ...raw, id: String(raw?.id || `mind-node-${index + 1}`) }))
|
|
225
|
+
const originalById = new Map(candidates.map((node) => [node.id, node]))
|
|
226
|
+
let skippedRefs = 0
|
|
227
|
+
let skippedItems = 0
|
|
228
|
+
let nodes = candidates.map((node) => {
|
|
229
|
+
const filtered = filterSourceRefs(node.sourceRefs, sources)
|
|
230
|
+
skippedRefs += filtered.skipped
|
|
231
|
+
const primary = filtered.refs.some((ref) => ref.sessionId === node.primarySourceSessionId)
|
|
232
|
+
? node.primarySourceSessionId
|
|
233
|
+
: (filtered.refs[0]?.sessionId || '')
|
|
234
|
+
return { ...node, sourceRefs: filtered.refs, primarySourceSessionId: primary }
|
|
235
|
+
})
|
|
236
|
+
if (strict) {
|
|
237
|
+
const before = nodes.length
|
|
238
|
+
nodes = nodes.filter((node) => node.sourceRefs.length)
|
|
239
|
+
skippedItems += before - nodes.length
|
|
240
|
+
}
|
|
241
|
+
if (!nodes.length) return { value: { ...value, nodes }, skippedRefs, skippedItems }
|
|
242
|
+
const surviving = new Set(nodes.map((node) => node.id))
|
|
243
|
+
let rootId = surviving.has(String(value?.rootId || '')) ? String(value.rootId) : nodes[0].id
|
|
244
|
+
const nearestParent = (node) => {
|
|
245
|
+
let parentId = node.parentId === null || node.parentId === undefined ? '' : String(node.parentId)
|
|
246
|
+
const visited = new Set([node.id])
|
|
247
|
+
while (parentId && !surviving.has(parentId) && !visited.has(parentId)) {
|
|
248
|
+
visited.add(parentId)
|
|
249
|
+
const parent = originalById.get(parentId)
|
|
250
|
+
parentId = parent?.parentId === null || parent?.parentId === undefined ? '' : String(parent.parentId)
|
|
251
|
+
}
|
|
252
|
+
return surviving.has(parentId) ? parentId : null
|
|
253
|
+
}
|
|
254
|
+
nodes = nodes.map((node) => {
|
|
255
|
+
if (node.id === rootId) return { ...node, parentId: null }
|
|
256
|
+
const parentId = nearestParent(node)
|
|
257
|
+
return { ...node, parentId: parentId && parentId !== node.id ? parentId : rootId }
|
|
258
|
+
})
|
|
259
|
+
return { value: { ...value, rootId, nodes }, skippedRefs, skippedItems }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function sanitizeKnowledgeGraphSources(value, sources, strict) {
|
|
263
|
+
let skippedRefs = 0
|
|
264
|
+
let skippedItems = 0
|
|
265
|
+
let entities = (Array.isArray(value?.entities) ? value.entities : []).map((entity) => {
|
|
266
|
+
const filtered = filterSourceRefs(entity?.sourceRefs, sources)
|
|
267
|
+
skippedRefs += filtered.skipped
|
|
268
|
+
return { ...entity, sourceRefs: filtered.refs }
|
|
269
|
+
})
|
|
270
|
+
if (strict) {
|
|
271
|
+
const before = entities.length
|
|
272
|
+
entities = entities.filter((entity) => entity.sourceRefs.length)
|
|
273
|
+
skippedItems += before - entities.length
|
|
274
|
+
}
|
|
275
|
+
const entityIds = new Set(entities.map((entity) => String(entity?.id || '')))
|
|
276
|
+
let relations = []
|
|
277
|
+
for (const relation of Array.isArray(value?.relations) ? value.relations : []) {
|
|
278
|
+
const filtered = filterSourceRefs(relation?.evidence, sources)
|
|
279
|
+
skippedRefs += filtered.skipped
|
|
280
|
+
if (!entityIds.has(String(relation?.from || '')) || !entityIds.has(String(relation?.to || '')) || (strict && !filtered.refs.length)) {
|
|
281
|
+
skippedItems += 1
|
|
282
|
+
continue
|
|
283
|
+
}
|
|
284
|
+
relations.push({ ...relation, evidence: filtered.refs })
|
|
285
|
+
}
|
|
286
|
+
return { value: { ...value, entities, relations }, skippedRefs, skippedItems }
|
|
287
|
+
}
|
|
288
|
+
|
|
167
289
|
function assertOutputSourceRefs(value, sources) {
|
|
168
290
|
const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
|
|
169
291
|
const check = (ref, label) => {
|
|
@@ -196,6 +318,7 @@ function outputPrompt(kind, summaries, prompt, strict) {
|
|
|
196
318
|
}
|
|
197
319
|
return [
|
|
198
320
|
'请根据以下多个对话摘要生成静态知识图谱。抽取实体、概念、模块、接口、决策、风险和外部系统,并只建立有依据的关系。',
|
|
321
|
+
'最多生成 20 个实体、30 条关系;实体 summary 控制在 220 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
|
|
199
322
|
'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]}]}]}。',
|
|
200
323
|
...rules,
|
|
201
324
|
`额外要求:${shortText(prompt, 2000) || '没有额外要求。'}`,
|
|
@@ -494,13 +617,14 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
494
617
|
this.assertNotCancelled(task)
|
|
495
618
|
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))
|
|
496
619
|
this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
620
|
+
let completedSources = 0
|
|
621
|
+
logMessage(this.logger, 'info', 'summary batch start id=%s sources=%d concurrency=%d', logId(task.id), sources.length, Math.min(SUMMARY_CONCURRENCY, sources.length))
|
|
622
|
+
const summaries = await mapWithConcurrency(sources, SUMMARY_CONCURRENCY, async (source, sourceIndex) => {
|
|
500
623
|
this.update(task, 'summarizing', {
|
|
501
624
|
sourceCount: sources.length,
|
|
502
|
-
progress: { percent: 10 + Math.floor(60 *
|
|
625
|
+
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `并行整理:${source.title}` }
|
|
503
626
|
})
|
|
627
|
+
const chunkSummaries = []
|
|
504
628
|
for (const chunk of chunkSourceText(source)) {
|
|
505
629
|
this.assertNotCancelled(task)
|
|
506
630
|
const prompt = summaryPrompt(source, chunk, request.strict === true)
|
|
@@ -510,7 +634,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
510
634
|
})
|
|
511
635
|
try {
|
|
512
636
|
const value = await generateSummary(prompt)
|
|
513
|
-
|
|
637
|
+
chunkSummaries.push(normalizeSummary(value, source, chunk))
|
|
514
638
|
} catch (error) {
|
|
515
639
|
this.assertNotCancelled(task)
|
|
516
640
|
logMessage(this.logger, 'warn', 'summary first attempt invalid id=%s session=%s error=%s', logId(task.id), logId(source.sessionId), errorMessage(error))
|
|
@@ -520,17 +644,20 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
520
644
|
})
|
|
521
645
|
const retryPrompt = `${prompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新整理,缩短内容,只输出严格合法的顶层摘要 JSON。`
|
|
522
646
|
const retryValue = await generateSummary(retryPrompt)
|
|
523
|
-
|
|
647
|
+
chunkSummaries.push(normalizeSummary(retryValue, source, chunk))
|
|
524
648
|
}
|
|
525
649
|
}
|
|
650
|
+
completedSources += 1
|
|
526
651
|
this.update(task, 'summarizing', {
|
|
527
652
|
sourceCount: sources.length,
|
|
528
|
-
progress: { percent: 10 + Math.floor(60 *
|
|
653
|
+
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已完成摘要:${source.title}` }
|
|
529
654
|
})
|
|
530
|
-
|
|
655
|
+
return mergeSourceSummaries(source, chunkSummaries)
|
|
656
|
+
})
|
|
531
657
|
this.assertNotCancelled(task)
|
|
532
658
|
let mindMap = null
|
|
533
659
|
let knowledgeGraph = null
|
|
660
|
+
const sourceWarnings = { skippedRefs: 0, skippedItems: 0 }
|
|
534
661
|
if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
|
|
535
662
|
this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
|
|
536
663
|
const mindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
|
|
@@ -540,7 +667,10 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
540
667
|
})
|
|
541
668
|
try {
|
|
542
669
|
const value = await generateMindMap(mindMapPrompt)
|
|
543
|
-
|
|
670
|
+
const sanitized = sanitizeMindMapSources(value, sources, request.strict === true)
|
|
671
|
+
mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
672
|
+
sourceWarnings.skippedRefs += sanitized.skippedRefs
|
|
673
|
+
sourceWarnings.skippedItems += sanitized.skippedItems
|
|
544
674
|
} catch (error) {
|
|
545
675
|
this.assertNotCancelled(task)
|
|
546
676
|
logMessage(this.logger, 'warn', 'mind map first attempt invalid id=%s error=%s', logId(task.id), errorMessage(error))
|
|
@@ -550,7 +680,10 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
550
680
|
})
|
|
551
681
|
const retryPrompt = `${mindMapPrompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新生成,不要复用上一次文本;只输出完整、严格合法的 JSON。`
|
|
552
682
|
const retryValue = await generateMindMap(retryPrompt)
|
|
553
|
-
|
|
683
|
+
const sanitized = sanitizeMindMapSources(retryValue, sources, request.strict === true)
|
|
684
|
+
mindMap = validateMindMap(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
685
|
+
sourceWarnings.skippedRefs += sanitized.skippedRefs
|
|
686
|
+
sourceWarnings.skippedItems += sanitized.skippedItems
|
|
554
687
|
}
|
|
555
688
|
}
|
|
556
689
|
if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
|
|
@@ -564,7 +697,17 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
564
697
|
model: request.model,
|
|
565
698
|
signal: task.controller.signal
|
|
566
699
|
})
|
|
567
|
-
|
|
700
|
+
const sanitized = sanitizeKnowledgeGraphSources(value, sources, request.strict === true)
|
|
701
|
+
sourceWarnings.skippedRefs += sanitized.skippedRefs
|
|
702
|
+
sourceWarnings.skippedItems += sanitized.skippedItems
|
|
703
|
+
knowledgeGraph = validateKnowledgeGraph(sanitized.value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
704
|
+
}
|
|
705
|
+
if (sourceWarnings.skippedRefs || sourceWarnings.skippedItems) {
|
|
706
|
+
logMessage(this.logger, 'warn', 'generation filtered invalid sources id=%s skippedRefs=%d skippedItems=%d', logId(task.id), sourceWarnings.skippedRefs, sourceWarnings.skippedItems)
|
|
707
|
+
this.update(task, 'validating', {
|
|
708
|
+
message: `已跳过 ${sourceWarnings.skippedRefs} 个无效来源引用、${sourceWarnings.skippedItems} 个无有效来源的内容项。`,
|
|
709
|
+
progress: { percent: 90, current: sources.length, total: sources.length, label: '过滤无效来源' }
|
|
710
|
+
})
|
|
568
711
|
}
|
|
569
712
|
assertOutputSourceRefs({ mindMap, knowledgeGraph }, sources)
|
|
570
713
|
this.assertNotCancelled(task)
|
|
@@ -575,6 +718,8 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
575
718
|
expectedRevision: request.expectedRevision,
|
|
576
719
|
generationId: task.id,
|
|
577
720
|
sourceSessionIds,
|
|
721
|
+
sourceSessions: sources.map((source) => ({ sessionId: source.sessionId, title: source.title })),
|
|
722
|
+
sourceWarnings,
|
|
578
723
|
prompt: request.prompt,
|
|
579
724
|
strict: request.strict,
|
|
580
725
|
outputMode: request.outputMode,
|
|
@@ -583,9 +728,14 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
583
728
|
knowledgeGraph
|
|
584
729
|
})
|
|
585
730
|
task.revision = saved.revision
|
|
586
|
-
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph }
|
|
731
|
+
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, sourceWarnings }
|
|
587
732
|
logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
|
|
588
|
-
this.update(task, 'completed', {
|
|
733
|
+
this.update(task, 'completed', {
|
|
734
|
+
message: `知识视图生成完成,已总结 ${sources.length} 个对话${sourceWarnings.skippedItems ? `,跳过 ${sourceWarnings.skippedItems} 个无有效来源的内容项` : ''}。`,
|
|
735
|
+
revision: saved.revision,
|
|
736
|
+
result: task.result,
|
|
737
|
+
progress: { percent: 100, current: sources.length, total: sources.length, label: '合并并生成最终报告完成' }
|
|
738
|
+
})
|
|
589
739
|
} catch (error) {
|
|
590
740
|
if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
|
|
591
741
|
this.update(task, 'cancelled', { error: '' })
|
|
@@ -685,7 +835,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
685
835
|
handle = await this.agents.create({
|
|
686
836
|
sessionId,
|
|
687
837
|
meta: { cwd: input.cwd, origin: 'subagent' },
|
|
688
|
-
agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? SUMMARY_MAX_TOKENS : VIEW_MAX_TOKENS },
|
|
838
|
+
agentOptions: { provider, model, reasoningEffort: 'off', maxTokens: input.kind === 'summary' ? SUMMARY_MAX_TOKENS : VIEW_MAX_TOKENS },
|
|
689
839
|
signal: input.signal,
|
|
690
840
|
setup: async (agentCtx) => {
|
|
691
841
|
agentCtx?.systemPrompt?.section?.({
|
package/lib/session-source.js
CHANGED
|
@@ -206,7 +206,7 @@ export async function readSelectedSurfaces({ sessionQuery, sessions }, { cwd, se
|
|
|
206
206
|
return sources
|
|
207
207
|
}
|
|
208
208
|
|
|
209
|
-
export function chunkSourceText(source, maxChars =
|
|
209
|
+
export function chunkSourceText(source, maxChars = 16000) {
|
|
210
210
|
const text = String(source?.text || '')
|
|
211
211
|
if (!text) return [{ text: '', sourceRefs: [] }]
|
|
212
212
|
const chunks = []
|
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, prompt, strict, outputMode, model, mindMap, knowledgeGraph }) {
|
|
154
|
+
async saveBundle({ cwd, expectedRevision = 0, generationId, sourceSessionIds, sourceSessions, sourceWarnings, 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()
|
|
@@ -166,6 +166,14 @@ export class WorkspaceStorage {
|
|
|
166
166
|
revision,
|
|
167
167
|
cwd: normalizedCwd,
|
|
168
168
|
sourceSessionIds: [...new Set((sourceSessionIds || []).map(String))],
|
|
169
|
+
sourceSessions: Array.isArray(sourceSessions) ? sourceSessions.map((source) => ({
|
|
170
|
+
sessionId: String(source?.sessionId || ''),
|
|
171
|
+
title: shortText(source?.title, 200)
|
|
172
|
+
})).filter((source) => source.sessionId) : [],
|
|
173
|
+
sourceWarnings: {
|
|
174
|
+
skippedRefs: Math.max(0, Number(sourceWarnings?.skippedRefs) || 0),
|
|
175
|
+
skippedItems: Math.max(0, Number(sourceWarnings?.skippedItems) || 0)
|
|
176
|
+
},
|
|
169
177
|
promptSummary: shortText(prompt, 500),
|
|
170
178
|
strict: strict === true,
|
|
171
179
|
outputMode: String(outputMode || 'both'),
|