@p-dsh-market/conversation-knowledge-map 0.1.10 → 0.1.12
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 +1 -1
- package/lib/client.js +43 -2
- package/lib/generation-orchestrator.js +237 -46
- package/lib/workspace-storage.js +20 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
|
|
|
11
11
|
|
|
12
12
|
配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
|
|
13
13
|
|
|
14
|
-
摘要阶段按对话并行处理,并发上限固定为 3
|
|
14
|
+
摘要阶段按对话并行处理,并发上限固定为 3;同一对话的超长分段保持顺序处理,随后先合并为一份对话摘要,再统一生成思维导图或知识图谱。结构化输出失败时会完全重置并最多重试 3 次;单个对话连续失败后只跳过该对话,不阻断其他对话和最终报告。“同时生成”模式下,一个最终视图失败也不会阻断另一个视图保存。进度区域和最终知识视图都会按时间线显示读取、摘要、重试、跳过、合并、生成及保存过程。结构化生成关闭推理输出以减少等待和 Token 消耗;知识图谱最多生成 20 个实体、30 条关系。最终模型若返回未选择的 Session 引用,会过滤该引用;严格模式下无有效来源的内容项会被跳过,最终页面列出实际总结的对话、失败对话和过滤数量。
|
|
15
15
|
|
|
16
16
|
节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
|
|
17
17
|
|
package/lib/client.js
CHANGED
|
@@ -260,12 +260,52 @@ 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,
|
|
264
|
+
state && state.manifest && state.manifest.generationTimeline ? React.createElement(GenerationTimeline, { key: 'timeline', items: state.manifest.generationTimeline }) : null,
|
|
263
265
|
!state || (!state.mindMap && !state.knowledgeGraph) ? React.createElement(EmptyState, { key: 'empty', title: '尚未生成知识视图', copy: '从标题栏打开“知识视图”,选择同工作路径下的对话并确认生成。' }) : (mode === 'mind-map'
|
|
264
266
|
? React.createElement(MindMapPanel, { key: 'mind', store: store, selectedNodeId: selectedNodeId, setSelectedNodeId: setSelectedNodeId })
|
|
265
267
|
: React.createElement(KnowledgeGraphPanel, { key: 'graph', store: store }))
|
|
266
268
|
])
|
|
267
269
|
}
|
|
268
270
|
|
|
271
|
+
function SourceSummary(props) {
|
|
272
|
+
var manifest = props.manifest || {}
|
|
273
|
+
var sources = Array.isArray(manifest.sourceSessions) && manifest.sourceSessions.length
|
|
274
|
+
? manifest.sourceSessions
|
|
275
|
+
: (manifest.sourceSessionIds || []).map(function (sessionId) { return { sessionId: sessionId, title: '' } })
|
|
276
|
+
var warnings = manifest.sourceWarnings || {}
|
|
277
|
+
var failedSources = manifest.failedSources || []
|
|
278
|
+
if (!sources.length) return null
|
|
279
|
+
return React.createElement('section', { className: 'ckm-source-summary', 'aria-label': '本次总结来源' }, [
|
|
280
|
+
React.createElement('div', { key: 'head', className: 'ckm-source-summary-head' }, [
|
|
281
|
+
React.createElement('strong', { key: 'title' }, '已总结 ' + sources.length + ' 个对话'),
|
|
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
|
|
283
|
+
]),
|
|
284
|
+
React.createElement('div', { key: 'list', className: 'ckm-source-list' }, sources.map(function (source) {
|
|
285
|
+
var sessionId = String(source.sessionId || '')
|
|
286
|
+
var label = source.title ? source.title + ' · ' + sessionId.slice(0, 16) + '…' : sessionId
|
|
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
|
+
if (!items.length) return null
|
|
296
|
+
return React.createElement('section', { className: 'ckm-timeline', 'aria-label': '生成过程时间线' }, [
|
|
297
|
+
React.createElement('strong', { key: 'title', className: 'ckm-timeline-title' }, '生成过程时间线'),
|
|
298
|
+
React.createElement('ol', { key: 'items' }, items.map(function (item, index) {
|
|
299
|
+
var time = ''
|
|
300
|
+
try { time = new Date(Number(item.at) || 0).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }) } catch (_) {}
|
|
301
|
+
return React.createElement('li', { key: String(item.id || index), 'data-type': item.type || 'info' }, [
|
|
302
|
+
React.createElement('time', { key: 'time' }, time),
|
|
303
|
+
React.createElement('span', { key: 'message' }, item.message)
|
|
304
|
+
])
|
|
305
|
+
}))
|
|
306
|
+
])
|
|
307
|
+
}
|
|
308
|
+
|
|
269
309
|
function GenerationStrip(props) {
|
|
270
310
|
var generation = props.store.generation
|
|
271
311
|
var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
|
|
@@ -276,7 +316,8 @@ window.__ModuleLoader__.load({
|
|
|
276
316
|
React.createElement('strong', { key: 'status' }, generation.message || generation.status),
|
|
277
317
|
React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
|
|
278
318
|
]),
|
|
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 + '%' } }))
|
|
319
|
+
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 + '%' } })),
|
|
320
|
+
React.createElement(GenerationTimeline, { key: 'activity', items: generation.timeline || [] })
|
|
280
321
|
]),
|
|
281
322
|
generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
|
|
282
323
|
['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 +612,7 @@ window.__ModuleLoader__.load({
|
|
|
571
612
|
'.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
613
|
'.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
614
|
'.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:
|
|
615
|
+
'.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-title{color:var(--dsw-alias-label-primary)}.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}' +
|
|
575
616
|
'.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
617
|
'.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
618
|
'.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)}' +
|
|
@@ -9,6 +9,7 @@ const MAX_SUMMARY_CHARS = 2400
|
|
|
9
9
|
const SUMMARY_MAX_TOKENS = 8000
|
|
10
10
|
const VIEW_MAX_TOKENS = 12000
|
|
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()
|
|
@@ -198,6 +199,94 @@ async function mapWithConcurrency(items, limit, mapper) {
|
|
|
198
199
|
return results
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
function filterSourceRefs(refs, sources) {
|
|
203
|
+
const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
|
|
204
|
+
let skipped = 0
|
|
205
|
+
const filtered = []
|
|
206
|
+
for (const ref of Array.isArray(refs) ? refs : []) {
|
|
207
|
+
const sessionId = String(ref?.sessionId || '')
|
|
208
|
+
const allowedSeqs = allowed.get(sessionId)
|
|
209
|
+
if (!allowedSeqs) {
|
|
210
|
+
skipped += 1
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
213
|
+
const eventSeqs = [...new Set((Array.isArray(ref?.eventSeqs) ? ref.eventSeqs : []).filter((seq) => Number.isInteger(seq) && allowedSeqs.has(seq)))]
|
|
214
|
+
if (!eventSeqs.length) {
|
|
215
|
+
skipped += 1
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
filtered.push({ sessionId, eventSeqs })
|
|
219
|
+
}
|
|
220
|
+
return { refs: filtered.slice(0, 12), skipped }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function sanitizeMindMapSources(value, sources, strict) {
|
|
224
|
+
const rawNodes = Array.isArray(value?.nodes) ? value.nodes : []
|
|
225
|
+
const candidates = rawNodes.map((raw, index) => ({ ...raw, id: String(raw?.id || `mind-node-${index + 1}`) }))
|
|
226
|
+
const originalById = new Map(candidates.map((node) => [node.id, node]))
|
|
227
|
+
let skippedRefs = 0
|
|
228
|
+
let skippedItems = 0
|
|
229
|
+
let nodes = candidates.map((node) => {
|
|
230
|
+
const filtered = filterSourceRefs(node.sourceRefs, sources)
|
|
231
|
+
skippedRefs += filtered.skipped
|
|
232
|
+
const primary = filtered.refs.some((ref) => ref.sessionId === node.primarySourceSessionId)
|
|
233
|
+
? node.primarySourceSessionId
|
|
234
|
+
: (filtered.refs[0]?.sessionId || '')
|
|
235
|
+
return { ...node, sourceRefs: filtered.refs, primarySourceSessionId: primary }
|
|
236
|
+
})
|
|
237
|
+
if (strict) {
|
|
238
|
+
const before = nodes.length
|
|
239
|
+
nodes = nodes.filter((node) => node.sourceRefs.length)
|
|
240
|
+
skippedItems += before - nodes.length
|
|
241
|
+
}
|
|
242
|
+
if (!nodes.length) return { value: { ...value, nodes }, skippedRefs, skippedItems }
|
|
243
|
+
const surviving = new Set(nodes.map((node) => node.id))
|
|
244
|
+
let rootId = surviving.has(String(value?.rootId || '')) ? String(value.rootId) : nodes[0].id
|
|
245
|
+
const nearestParent = (node) => {
|
|
246
|
+
let parentId = node.parentId === null || node.parentId === undefined ? '' : String(node.parentId)
|
|
247
|
+
const visited = new Set([node.id])
|
|
248
|
+
while (parentId && !surviving.has(parentId) && !visited.has(parentId)) {
|
|
249
|
+
visited.add(parentId)
|
|
250
|
+
const parent = originalById.get(parentId)
|
|
251
|
+
parentId = parent?.parentId === null || parent?.parentId === undefined ? '' : String(parent.parentId)
|
|
252
|
+
}
|
|
253
|
+
return surviving.has(parentId) ? parentId : null
|
|
254
|
+
}
|
|
255
|
+
nodes = nodes.map((node) => {
|
|
256
|
+
if (node.id === rootId) return { ...node, parentId: null }
|
|
257
|
+
const parentId = nearestParent(node)
|
|
258
|
+
return { ...node, parentId: parentId && parentId !== node.id ? parentId : rootId }
|
|
259
|
+
})
|
|
260
|
+
return { value: { ...value, rootId, nodes }, skippedRefs, skippedItems }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function sanitizeKnowledgeGraphSources(value, sources, strict) {
|
|
264
|
+
let skippedRefs = 0
|
|
265
|
+
let skippedItems = 0
|
|
266
|
+
let entities = (Array.isArray(value?.entities) ? value.entities : []).map((entity) => {
|
|
267
|
+
const filtered = filterSourceRefs(entity?.sourceRefs, sources)
|
|
268
|
+
skippedRefs += filtered.skipped
|
|
269
|
+
return { ...entity, sourceRefs: filtered.refs }
|
|
270
|
+
})
|
|
271
|
+
if (strict) {
|
|
272
|
+
const before = entities.length
|
|
273
|
+
entities = entities.filter((entity) => entity.sourceRefs.length)
|
|
274
|
+
skippedItems += before - entities.length
|
|
275
|
+
}
|
|
276
|
+
const entityIds = new Set(entities.map((entity) => String(entity?.id || '')))
|
|
277
|
+
let relations = []
|
|
278
|
+
for (const relation of Array.isArray(value?.relations) ? value.relations : []) {
|
|
279
|
+
const filtered = filterSourceRefs(relation?.evidence, sources)
|
|
280
|
+
skippedRefs += filtered.skipped
|
|
281
|
+
if (!entityIds.has(String(relation?.from || '')) || !entityIds.has(String(relation?.to || '')) || (strict && !filtered.refs.length)) {
|
|
282
|
+
skippedItems += 1
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
relations.push({ ...relation, evidence: filtered.refs })
|
|
286
|
+
}
|
|
287
|
+
return { value: { ...value, entities, relations }, skippedRefs, skippedItems }
|
|
288
|
+
}
|
|
289
|
+
|
|
201
290
|
function assertOutputSourceRefs(value, sources) {
|
|
202
291
|
const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
|
|
203
292
|
const check = (ref, label) => {
|
|
@@ -445,6 +534,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
445
534
|
revision: task.revision || 0,
|
|
446
535
|
progress: task.progress ? clone(task.progress) : null,
|
|
447
536
|
result: task.result ? clone(task.result) : null,
|
|
537
|
+
timeline: Array.isArray(task.timeline) ? clone(task.timeline) : [],
|
|
448
538
|
events: task.events.slice(-40)
|
|
449
539
|
}
|
|
450
540
|
}
|
|
@@ -475,6 +565,11 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
475
565
|
}
|
|
476
566
|
}
|
|
477
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
|
+
|
|
478
573
|
start(request) {
|
|
479
574
|
const key = String(request.cwd || '').toLowerCase()
|
|
480
575
|
if (this.busyByWorkspace.has(key)) throw new Error('当前工作路径已有生成任务在运行,请先取消或等待完成。')
|
|
@@ -489,7 +584,9 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
489
584
|
createdAt: this.now(),
|
|
490
585
|
updatedAt: this.now(),
|
|
491
586
|
nextEventId: 1,
|
|
587
|
+
nextTimelineId: 1,
|
|
492
588
|
events: [],
|
|
589
|
+
timeline: [],
|
|
493
590
|
listeners: new Set(),
|
|
494
591
|
controller: new AbortController(),
|
|
495
592
|
promise: null
|
|
@@ -518,6 +615,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
518
615
|
|
|
519
616
|
async run(task, request) {
|
|
520
617
|
try {
|
|
618
|
+
this.recordTimeline(task, 'start', `开始处理 ${request.selectedSessionIds.length} 个已选对话。`)
|
|
521
619
|
this.update(task, 'reading-sources', { progress: { percent: 5, current: 0, total: request.selectedSessionIds.length, label: '读取对话记录' } })
|
|
522
620
|
const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
|
|
523
621
|
cwd: request.cwd,
|
|
@@ -525,94 +623,179 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
525
623
|
fallbackSessionId: request.anchorSessionId,
|
|
526
624
|
includeSubagents: request.includeSubagents === true
|
|
527
625
|
})
|
|
528
|
-
const
|
|
626
|
+
const loadedSessionIds = [...new Set(sources.map((source) => source.sessionId))]
|
|
529
627
|
this.assertNotCancelled(task)
|
|
530
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 个并行)。`)
|
|
531
630
|
this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
|
|
532
631
|
let completedSources = 0
|
|
533
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))
|
|
534
|
-
const
|
|
633
|
+
const summaryResults = await mapWithConcurrency(sources, SUMMARY_CONCURRENCY, async (source, sourceIndex) => {
|
|
634
|
+
this.recordTimeline(task, 'summary-start', `开始摘要:${source.title}`, { sessionId: source.sessionId })
|
|
535
635
|
this.update(task, 'summarizing', {
|
|
536
636
|
sourceCount: sources.length,
|
|
537
637
|
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `并行整理:${source.title}` }
|
|
538
638
|
})
|
|
539
639
|
const chunkSummaries = []
|
|
540
|
-
|
|
640
|
+
const chunks = chunkSourceText(source)
|
|
641
|
+
let finalError = null
|
|
642
|
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex += 1) {
|
|
643
|
+
const chunk = chunks[chunkIndex]
|
|
541
644
|
this.assertNotCancelled(task)
|
|
542
|
-
const
|
|
645
|
+
const basePrompt = summaryPrompt(source, chunk, request.strict === true)
|
|
543
646
|
const generateSummary = async (summaryInput) => this.runModel({
|
|
544
647
|
kind: 'summary', prompt: summaryInput, cwd: request.cwd, strict: request.strict === true,
|
|
545
|
-
selectedSessionIds:
|
|
648
|
+
selectedSessionIds: loadedSessionIds, model: request.model, signal: task.controller.signal
|
|
546
649
|
})
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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', `摘要失败,重置重试 ${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
|
+
}
|
|
560
669
|
}
|
|
670
|
+
if (finalError) break
|
|
561
671
|
}
|
|
562
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 })
|
|
563
683
|
this.update(task, 'summarizing', {
|
|
564
684
|
sourceCount: sources.length,
|
|
565
|
-
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: source.title }
|
|
685
|
+
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `已完成摘要:${source.title}` }
|
|
566
686
|
})
|
|
567
|
-
return mergeSourceSummaries(source, chunkSummaries)
|
|
687
|
+
return { source, error: '', summary: mergeSourceSummaries(source, chunkSummaries) }
|
|
568
688
|
})
|
|
569
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} 个失败对话` : ''},开始生成最终视图。`)
|
|
570
699
|
let mindMap = null
|
|
571
700
|
let knowledgeGraph = null
|
|
701
|
+
const sourceWarnings = { skippedRefs: 0, skippedItems: 0 }
|
|
572
702
|
if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
|
|
703
|
+
this.recordTimeline(task, 'view-start', '开始生成思维导图。')
|
|
573
704
|
this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
|
|
574
|
-
const
|
|
705
|
+
const baseMindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
|
|
575
706
|
const generateMindMap = async (prompt) => this.runModel({
|
|
576
707
|
kind: 'mind-map', prompt, cwd: request.cwd, strict: request.strict === true,
|
|
577
708
|
selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
|
|
578
709
|
})
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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', `思维导图结构无效,重置重试 ${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
|
|
592
738
|
}
|
|
593
739
|
}
|
|
594
740
|
if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
|
|
741
|
+
this.recordTimeline(task, 'view-start', '开始生成知识图谱。')
|
|
595
742
|
this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: sources.length, total: sources.length, label: '生成知识图谱' } })
|
|
596
|
-
const
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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', `知识图谱结构无效,重置重试 ${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
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (!mindMap && !knowledgeGraph) throw new Error('思维导图和知识图谱均未能生成有效结果。')
|
|
778
|
+
if (sourceWarnings.skippedRefs || sourceWarnings.skippedItems) {
|
|
779
|
+
logMessage(this.logger, 'warn', 'generation filtered invalid sources id=%s skippedRefs=%d skippedItems=%d', logId(task.id), sourceWarnings.skippedRefs, sourceWarnings.skippedItems)
|
|
780
|
+
this.update(task, 'validating', {
|
|
781
|
+
message: `已跳过 ${sourceWarnings.skippedRefs} 个无效来源引用、${sourceWarnings.skippedItems} 个无有效来源的内容项。`,
|
|
782
|
+
progress: { percent: 90, current: sources.length, total: sources.length, label: '过滤无效来源' }
|
|
604
783
|
})
|
|
605
|
-
knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
606
784
|
}
|
|
607
|
-
assertOutputSourceRefs({ mindMap, knowledgeGraph },
|
|
785
|
+
assertOutputSourceRefs({ mindMap, knowledgeGraph }, summarizedSources)
|
|
608
786
|
this.assertNotCancelled(task)
|
|
609
787
|
this.update(task, 'validating', { progress: { percent: 92, current: sources.length, total: sources.length, label: '校验结构和来源' } })
|
|
788
|
+
this.recordTimeline(task, 'save', '最终结果已通过校验,正在保存知识视图。')
|
|
610
789
|
this.update(task, 'saving', { progress: { percent: 97, current: sources.length, total: sources.length, label: '保存知识视图' } })
|
|
611
790
|
const saved = await this.storage.saveBundle({
|
|
612
791
|
cwd: request.cwd,
|
|
613
792
|
expectedRevision: request.expectedRevision,
|
|
614
793
|
generationId: task.id,
|
|
615
794
|
sourceSessionIds,
|
|
795
|
+
sourceSessions: summarizedSources.map((source) => ({ sessionId: source.sessionId, title: source.title })),
|
|
796
|
+
failedSources,
|
|
797
|
+
sourceWarnings,
|
|
798
|
+
generationTimeline: task.timeline,
|
|
616
799
|
prompt: request.prompt,
|
|
617
800
|
strict: request.strict,
|
|
618
801
|
outputMode: request.outputMode,
|
|
@@ -621,14 +804,22 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
621
804
|
knowledgeGraph
|
|
622
805
|
})
|
|
623
806
|
task.revision = saved.revision
|
|
624
|
-
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph }
|
|
807
|
+
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph, sourceSessions: saved.manifest.sourceSessions, failedSources, sourceWarnings }
|
|
808
|
+
this.recordTimeline(task, 'complete', '知识视图已保存,生成流程完成。')
|
|
625
809
|
logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
|
|
626
|
-
this.update(task, 'completed', {
|
|
810
|
+
this.update(task, 'completed', {
|
|
811
|
+
message: `知识视图生成完成,已总结 ${summarizedSources.length} 个对话${failedSources.length ? `,跳过 ${failedSources.length} 个失败对话` : ''}${sourceWarnings.skippedItems ? `,过滤 ${sourceWarnings.skippedItems} 个无有效来源的内容项` : ''}。`,
|
|
812
|
+
revision: saved.revision,
|
|
813
|
+
result: task.result,
|
|
814
|
+
progress: { percent: 100, current: sources.length, total: sources.length, label: '合并并生成最终报告完成' }
|
|
815
|
+
})
|
|
627
816
|
} catch (error) {
|
|
628
817
|
if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
|
|
818
|
+
this.recordTimeline(task, 'cancelled', '用户取消了知识视图生成。')
|
|
629
819
|
this.update(task, 'cancelled', { error: '' })
|
|
630
820
|
} else {
|
|
631
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)}`)
|
|
632
823
|
this.update(task, 'failed', { error: errorMessage(error) })
|
|
633
824
|
}
|
|
634
825
|
}
|
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, 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()
|
|
@@ -166,6 +166,25 @@ 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
|
+
},
|
|
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
|
+
})) : [],
|
|
169
188
|
promptSummary: shortText(prompt, 500),
|
|
170
189
|
strict: strict === true,
|
|
171
190
|
outputMode: String(outputMode || 'both'),
|