@p-dsh-market/conversation-knowledge-map 0.1.8 → 0.1.10
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/generation-orchestrator.js +79 -22
- package/lib/session-source.js +1 -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 条关系。
|
|
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 后验证。
|
|
@@ -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()
|
|
@@ -68,7 +69,16 @@ function textFromContent(value) {
|
|
|
68
69
|
return ''
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
function
|
|
72
|
+
function expectedObject(value, kind = '') {
|
|
73
|
+
if (!kind) return true
|
|
74
|
+
if (kind === 'summary') return typeof value?.summary === 'string' || typeof value?.narrative === 'string' || typeof value?.text === 'string'
|
|
75
|
+
if (kind === 'mind-map') return Array.isArray(value?.nodes)
|
|
76
|
+
if (kind === 'knowledge-graph') return Array.isArray(value?.entities) && Array.isArray(value?.relations)
|
|
77
|
+
if (kind === 'follow-up') return typeof value?.question === 'string'
|
|
78
|
+
return true
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function findJsonObject(text, kind = '') {
|
|
72
82
|
const candidates = []
|
|
73
83
|
const fenced = String(text || '').match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
74
84
|
if (fenced) candidates.push(fenced[1])
|
|
@@ -98,7 +108,7 @@ function findJsonObject(text) {
|
|
|
98
108
|
const fragment = candidate.slice(start, index + 1)
|
|
99
109
|
try {
|
|
100
110
|
const parsed = JSON.parse(fragment)
|
|
101
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
|
|
111
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && expectedObject(parsed, kind)) return parsed
|
|
102
112
|
} catch {
|
|
103
113
|
// Try the next opening brace in case prose contained an example.
|
|
104
114
|
}
|
|
@@ -111,11 +121,11 @@ function findJsonObject(text) {
|
|
|
111
121
|
return null
|
|
112
122
|
}
|
|
113
123
|
|
|
114
|
-
export function parseStructuredOutput(value) {
|
|
124
|
+
export function parseStructuredOutput(value, kind = '') {
|
|
115
125
|
const object = asObject(value)
|
|
116
|
-
if (object) return object
|
|
126
|
+
if (object && expectedObject(object, kind)) return object
|
|
117
127
|
const text = textFromContent(value).replace(/^\uFEFF/, '').trim()
|
|
118
|
-
const parsed = findJsonObject(text)
|
|
128
|
+
const parsed = findJsonObject(text, kind)
|
|
119
129
|
if (parsed) return parsed
|
|
120
130
|
if (!text) throw new Error('模型没有返回 JSON 对象。')
|
|
121
131
|
throw new Error('模型返回了文本,但其中没有可解析的 JSON 对象。')
|
|
@@ -155,6 +165,39 @@ function normalizeSummary(value, source, chunk) {
|
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
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
|
+
|
|
158
201
|
function assertOutputSourceRefs(value, sources) {
|
|
159
202
|
const allowed = new Map(sources.map((source) => [source.sessionId, new Set((source.events || []).flatMap((event) => [event.seq, ...(event.sourceEventSeqs || [])]))]))
|
|
160
203
|
const check = (ref, label) => {
|
|
@@ -187,6 +230,7 @@ function outputPrompt(kind, summaries, prompt, strict) {
|
|
|
187
230
|
}
|
|
188
231
|
return [
|
|
189
232
|
'请根据以下多个对话摘要生成静态知识图谱。抽取实体、概念、模块、接口、决策、风险和外部系统,并只建立有依据的关系。',
|
|
233
|
+
'最多生成 20 个实体、30 条关系;实体 summary 控制在 220 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
|
|
190
234
|
'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]}]}]}。',
|
|
191
235
|
...rules,
|
|
192
236
|
`额外要求:${shortText(prompt, 2000) || '没有额外要求。'}`,
|
|
@@ -198,6 +242,7 @@ function summaryPrompt(source, chunk, strict) {
|
|
|
198
242
|
return [
|
|
199
243
|
'请把一段 DSH 对话整理成带来源的结构化摘要。不要复述完整聊天,不要添加对话中没有的事实。',
|
|
200
244
|
'只输出 JSON:{"summary":"完整阶段性说明","keyPoints":["..."],"sourceRefs":[{"sessionId":"...","eventSeqs":[1]}]}。',
|
|
245
|
+
'summary 控制在 1200 字以内;keyPoints 最多 8 条,每条不超过 160 字。不要增加其他字段。字符串中的双引号必须正确转义,确保整个输出可以被 JSON.parse 直接解析。',
|
|
201
246
|
strict ? '严格模式:每个关键点都要能回指给定事件。' : '允许标记尚未确认的冲突,但不能编造事件序号。',
|
|
202
247
|
`Session:${source.sessionId}`,
|
|
203
248
|
`标题:${source.title}`,
|
|
@@ -484,31 +529,43 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
484
529
|
this.assertNotCancelled(task)
|
|
485
530
|
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))
|
|
486
531
|
this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
532
|
+
let completedSources = 0
|
|
533
|
+
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 summaries = await mapWithConcurrency(sources, SUMMARY_CONCURRENCY, async (source, sourceIndex) => {
|
|
490
535
|
this.update(task, 'summarizing', {
|
|
491
536
|
sourceCount: sources.length,
|
|
492
|
-
progress: { percent: 10 + Math.floor(60 *
|
|
537
|
+
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: `并行整理:${source.title}` }
|
|
493
538
|
})
|
|
539
|
+
const chunkSummaries = []
|
|
494
540
|
for (const chunk of chunkSourceText(source)) {
|
|
495
541
|
this.assertNotCancelled(task)
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
prompt:
|
|
499
|
-
|
|
500
|
-
strict: request.strict === true,
|
|
501
|
-
selectedSessionIds: sourceSessionIds,
|
|
502
|
-
model: request.model,
|
|
503
|
-
signal: task.controller.signal
|
|
542
|
+
const prompt = summaryPrompt(source, chunk, request.strict === true)
|
|
543
|
+
const generateSummary = async (summaryInput) => this.runModel({
|
|
544
|
+
kind: 'summary', prompt: summaryInput, cwd: request.cwd, strict: request.strict === true,
|
|
545
|
+
selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
|
|
504
546
|
})
|
|
505
|
-
|
|
547
|
+
try {
|
|
548
|
+
const value = await generateSummary(prompt)
|
|
549
|
+
chunkSummaries.push(normalizeSummary(value, source, chunk))
|
|
550
|
+
} catch (error) {
|
|
551
|
+
this.assertNotCancelled(task)
|
|
552
|
+
logMessage(this.logger, 'warn', 'summary first attempt invalid id=%s session=%s error=%s', logId(task.id), logId(source.sessionId), errorMessage(error))
|
|
553
|
+
this.update(task, 'summarizing', {
|
|
554
|
+
message: `对话 ${sourceIndex + 1}/${sources.length} 摘要结构无效,正在自动修复…`,
|
|
555
|
+
progress: { percent: 10 + Math.floor(60 * sourceIndex / sources.length), current: sourceIndex, total: sources.length, label: `自动修复:${source.title}` }
|
|
556
|
+
})
|
|
557
|
+
const retryPrompt = `${prompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新整理,缩短内容,只输出严格合法的顶层摘要 JSON。`
|
|
558
|
+
const retryValue = await generateSummary(retryPrompt)
|
|
559
|
+
chunkSummaries.push(normalizeSummary(retryValue, source, chunk))
|
|
560
|
+
}
|
|
506
561
|
}
|
|
562
|
+
completedSources += 1
|
|
507
563
|
this.update(task, 'summarizing', {
|
|
508
564
|
sourceCount: sources.length,
|
|
509
|
-
progress: { percent: 10 + Math.floor(60 *
|
|
565
|
+
progress: { percent: 10 + Math.floor(60 * completedSources / sources.length), current: completedSources, total: sources.length, label: source.title }
|
|
510
566
|
})
|
|
511
|
-
|
|
567
|
+
return mergeSourceSummaries(source, chunkSummaries)
|
|
568
|
+
})
|
|
512
569
|
this.assertNotCancelled(task)
|
|
513
570
|
let mindMap = null
|
|
514
571
|
let knowledgeGraph = null
|
|
@@ -581,7 +638,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
581
638
|
async runModel(input) {
|
|
582
639
|
const parseModelOutput = (value, source, diagnostics = {}) => {
|
|
583
640
|
try {
|
|
584
|
-
const result = parseStructuredOutput(value)
|
|
641
|
+
const result = parseStructuredOutput(value, input.kind)
|
|
585
642
|
logMessage(this.logger, 'info', 'agent output parsed kind=%s source=%s value=%s result=%s', input.kind, source, diagnosticSummary(value), diagnosticSummary(result))
|
|
586
643
|
return result
|
|
587
644
|
} catch (error) {
|
|
@@ -666,7 +723,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
666
723
|
handle = await this.agents.create({
|
|
667
724
|
sessionId,
|
|
668
725
|
meta: { cwd: input.cwd, origin: 'subagent' },
|
|
669
|
-
agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? SUMMARY_MAX_TOKENS : VIEW_MAX_TOKENS },
|
|
726
|
+
agentOptions: { provider, model, reasoningEffort: 'off', maxTokens: input.kind === 'summary' ? SUMMARY_MAX_TOKENS : VIEW_MAX_TOKENS },
|
|
670
727
|
signal: input.signal,
|
|
671
728
|
setup: async (agentCtx) => {
|
|
672
729
|
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 = []
|