@p-dsh-market/conversation-knowledge-map 0.1.1 → 0.1.3

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 CHANGED
@@ -13,6 +13,10 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
13
13
 
14
14
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
15
15
 
16
+ ## 生成失败诊断
17
+
18
+ Host 和生成编排器会输出带 `[conversation-knowledge-map]` 前缀的诊断日志。日志包含路由、Provider / Model、Agent Session、实时事件类型、surface/session 读取次数、提取文本长度、输出形状和错误原因,不记录 Prompt、对话正文或模型返回正文。优先查看 DSH Web Runtime 的终端日志;若 Runtime 提供 logger 服务,则同时写入该 logger。重点关注 `agent event`、`agent idle`、`agent surface read`、`agent session read` 和 `agent output parse failed`。
19
+
16
20
  ## 本地验证
17
21
 
18
22
  ```powershell
@@ -2,11 +2,22 @@ import { randomUUID } from 'node:crypto'
2
2
 
3
3
  import { validateKnowledgeGraph } from './knowledge-graph-schema.js'
4
4
  import { validateMindMap } from './mind-map-schema.js'
5
- import { clone, errorMessage, makeUserMessage, shortText } from './protocol.js'
5
+ import { clone, diagnosticSummary, errorMessage, logMessage, makeUserMessage, shortText } from './protocol.js'
6
6
  import { chunkSourceText, readSelectedSurfaces } from './session-source.js'
7
7
 
8
8
  const MAX_SUMMARY_CHARS = 2400
9
9
 
10
+ function modelLabel(selection) {
11
+ const provider = String(selection?.provider || '').trim()
12
+ const model = String(selection?.model || '').trim()
13
+ return provider && model ? `${provider}/${model}` : 'default'
14
+ }
15
+
16
+ function logId(value) {
17
+ const text = String(value || '')
18
+ return text.length > 96 ? `${text.slice(0, 40)}…${text.slice(-40)}` : text
19
+ }
20
+
10
21
  function phaseMessage(status) {
11
22
  return {
12
23
  confirming: '等待用户确认生成范围…',
@@ -224,17 +235,30 @@ function extractAgentText(surface) {
224
235
  return chunks.join('')
225
236
  }
226
237
 
227
- async function readAgentText(sessionQuery, sessionId, signal) {
238
+ async function readAgentText(sessionQuery, sessionId, signal, { logger, diagnostics } = {}) {
239
+ const stats = diagnostics || { surfaceReads: 0, sessionReads: 0 }
228
240
  for (let attempt = 0; attempt < 6; attempt += 1) {
229
241
  if (signal?.aborted) throw signal.reason || new Error('生成已取消。')
242
+ stats.surfaceReads += 1
230
243
  const surface = await sessionQuery?.readSurface?.(sessionId)
231
244
  const text = extractAgentText(surface)
245
+ const eventCount = Array.isArray(surface?.events) ? surface.events.length : 0
246
+ stats.lastSurfaceEventCount = eventCount
247
+ stats.lastSurfaceShape = diagnosticSummary(surface)
248
+ stats.lastSurfaceTextLength = text.length
249
+ logMessage(logger, 'info', 'agent surface read session=%s attempt=%d events=%d extractedTextLength=%d shape=%s', logId(sessionId), attempt + 1, eventCount, text.length, stats.lastSurfaceShape)
232
250
  if (text.trim()) return text
233
251
  if (attempt < 5) await new Promise((resolve) => setTimeout(resolve, 40 * (attempt + 1)))
234
252
  }
235
253
  if (typeof sessionQuery?.readSession === 'function') {
254
+ stats.sessionReads += 1
236
255
  const log = await sessionQuery.readSession(sessionId)
237
256
  const text = extractAgentText(log)
257
+ const eventCount = Array.isArray(log?.events) ? log.events.length : 0
258
+ stats.lastSessionEventCount = eventCount
259
+ stats.lastSessionShape = diagnosticSummary(log)
260
+ stats.lastSessionTextLength = text.length
261
+ logMessage(logger, 'info', 'agent session read session=%s events=%d extractedTextLength=%d shape=%s', logId(sessionId), eventCount, text.length, stats.lastSessionShape)
238
262
  if (text.trim()) return text
239
263
  }
240
264
  return ''
@@ -258,6 +282,8 @@ export class KnowledgeGenerationOrchestrator {
258
282
  agentDefaultModel,
259
283
  storage,
260
284
  modelRunner,
285
+ sessionEventSource,
286
+ logger,
261
287
  sourceReader = readSelectedSurfaces,
262
288
  now = () => Date.now(),
263
289
  idFactory = () => randomUUID()
@@ -268,6 +294,8 @@ export class KnowledgeGenerationOrchestrator {
268
294
  this.agentDefaultModel = agentDefaultModel
269
295
  this.storage = storage
270
296
  this.modelRunner = modelRunner
297
+ this.sessionEventSource = sessionEventSource
298
+ this.logger = logger
271
299
  this.sourceReader = sourceReader
272
300
  this.now = now
273
301
  this.idFactory = idFactory
@@ -336,6 +364,7 @@ export class KnowledgeGenerationOrchestrator {
336
364
  }
337
365
  this.tasks.set(task.id, task)
338
366
  this.busyByWorkspace.set(key, task.id)
367
+ logMessage(this.logger, 'info', 'generation start id=%s cwd=%s outputMode=%s selectedSessions=%d strict=%s model=%s', logId(task.id), shortText(request.cwd, 180), request.outputMode, Array.isArray(request.selectedSessionIds) ? request.selectedSessionIds.length : 0, request.strict === true, modelLabel(request.model))
339
368
  this.update(task, 'created', { request: { ...request, prompt: shortText(request.prompt, 500) } })
340
369
  task.promise = this.run(task, request).finally(() => {
341
370
  if (this.busyByWorkspace.get(key) === task.id) this.busyByWorkspace.delete(key)
@@ -364,6 +393,7 @@ export class KnowledgeGenerationOrchestrator {
364
393
  includeSubagents: request.includeSubagents === true
365
394
  })
366
395
  this.assertNotCancelled(task)
396
+ 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))
367
397
  this.update(task, 'summarizing', { sourceCount: sources.length })
368
398
  const summaries = []
369
399
  for (const source of sources) {
@@ -428,11 +458,13 @@ export class KnowledgeGenerationOrchestrator {
428
458
  })
429
459
  task.revision = saved.revision
430
460
  task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph }
461
+ logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
431
462
  this.update(task, 'completed', { revision: saved.revision, result: task.result })
432
463
  } catch (error) {
433
464
  if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
434
465
  this.update(task, 'cancelled', { error: '' })
435
466
  } else {
467
+ logMessage(this.logger, 'error', 'generation failed id=%s status=%s error=%s', logId(task.id), task.status, errorMessage(error))
436
468
  this.update(task, 'failed', { error: errorMessage(error) })
437
469
  }
438
470
  }
@@ -440,38 +472,101 @@ export class KnowledgeGenerationOrchestrator {
440
472
  }
441
473
 
442
474
  async runModel(input) {
443
- if (typeof this.modelRunner === 'function') return parseStructuredOutput(await this.modelRunner(input))
475
+ const parseModelOutput = (value, source, diagnostics = {}) => {
476
+ try {
477
+ const result = parseStructuredOutput(value)
478
+ logMessage(this.logger, 'info', 'agent output parsed kind=%s source=%s value=%s result=%s', input.kind, source, diagnosticSummary(value), diagnosticSummary(result))
479
+ return result
480
+ } catch (error) {
481
+ logMessage(this.logger, 'error', 'agent output parse failed kind=%s source=%s value=%s diagnostics=%s error=%s', input.kind, source, diagnosticSummary(value), JSON.stringify(diagnostics), errorMessage(error))
482
+ throw error
483
+ }
484
+ }
485
+ if (typeof this.modelRunner === 'function') {
486
+ logMessage(this.logger, 'info', 'model runner start kind=%s model=%s promptLength=%d', input.kind, modelLabel(input.model), String(input.prompt || '').length)
487
+ const value = await this.modelRunner(input)
488
+ return parseModelOutput(value, 'model-runner')
489
+ }
444
490
  if (!this.agents?.create) throw new Error('当前 DSH Runtime 未提供 agents.create,无法生成知识视图。')
445
491
  let selection = normalizeModelSelection(input.model)
446
492
  if (!selection) {
447
- try { selection = normalizeModelSelection(this.agentDefaultModel?.currentSelection?.()) } catch { selection = null }
493
+ try {
494
+ selection = normalizeModelSelection(this.agentDefaultModel?.currentSelection?.())
495
+ } catch (error) {
496
+ logMessage(this.logger, 'warn', 'default model selection failed kind=%s error=%s', input.kind, shortText(errorMessage(error), 500))
497
+ selection = null
498
+ }
448
499
  }
449
500
  const provider = String(selection?.provider || '').trim()
450
501
  const model = String(selection?.model || '').trim()
451
- if (!provider || !model) throw new Error('当前没有可用的默认 Provider/Model。')
502
+ if (!provider || !model) {
503
+ logMessage(this.logger, 'error', 'agent call has no usable model kind=%s requestedModel=%s defaultModel=%s', input.kind, modelLabel(input.model), modelLabel(selection))
504
+ throw new Error('当前没有可用的默认 Provider/Model。')
505
+ }
452
506
  const sessionId = `knowledge-map-${this.idFactory()}`
453
- const handle = await this.agents.create({
454
- sessionId,
455
- meta: { cwd: input.cwd, origin: 'subagent' },
456
- agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? 2500 : 6000 },
457
- signal: input.signal,
458
- setup: async (agentCtx) => {
459
- agentCtx?.systemPrompt?.section?.({
460
- name: 'knowledge-map:protocol',
461
- order: 0,
462
- text: '你是 DSH 知识视图生成器。只输出调用方要求的 JSON;不要调用外部网络、文件写入或其他 Agent 工具。'
507
+ const liveEvents = []
508
+ const diagnostics = {
509
+ liveEvents: 0,
510
+ liveAssistantMessages: 0,
511
+ liveAssistantChunks: 0,
512
+ liveOtherEvents: 0,
513
+ surfaceReads: 0,
514
+ sessionReads: 0,
515
+ lastSurfaceEventCount: 0,
516
+ lastSessionEventCount: 0
517
+ }
518
+ logMessage(this.logger, 'info', 'agent call start kind=%s session=%s provider=%s model=%s promptLength=%d', input.kind, logId(sessionId), provider, model, String(input.prompt || '').length)
519
+ let unsubscribe
520
+ if (typeof this.sessionEventSource === 'function') {
521
+ try {
522
+ unsubscribe = this.sessionEventSource((session, event) => {
523
+ const eventSessionId = String(session?.id || session?.header?.id || '')
524
+ if (eventSessionId !== sessionId || !event) return
525
+ liveEvents.push(event)
526
+ diagnostics.liveEvents += 1
527
+ if (event.type === 'assistant/message') diagnostics.liveAssistantMessages += 1
528
+ else if (event.type === 'assistant/chunk') diagnostics.liveAssistantChunks += 1
529
+ else diagnostics.liveOtherEvents += 1
530
+ logMessage(this.logger, 'info', 'agent event session=%s type=%s data=%s', logId(sessionId), String(event.type || 'unknown'), diagnosticSummary(event.data))
463
531
  })
464
- try {
465
- agentCtx?.tools?.restrict?.({ deny: ['multi_agent_discuss', 'shell', 'filesystem', 'web_search', 'browser'] })
466
- } catch { /* older runtimes may not expose tool restriction */ }
532
+ logMessage(this.logger, 'info', 'agent event subscription ready session=%s', logId(sessionId))
533
+ } catch (error) {
534
+ logMessage(this.logger, 'warn', 'agent event subscription failed session=%s error=%s', logId(sessionId), errorMessage(error))
467
535
  }
468
- })
536
+ }
537
+ let handle
469
538
  try {
539
+ handle = await this.agents.create({
540
+ sessionId,
541
+ meta: { cwd: input.cwd, origin: 'subagent' },
542
+ agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? 2500 : 6000 },
543
+ signal: input.signal,
544
+ setup: async (agentCtx) => {
545
+ agentCtx?.systemPrompt?.section?.({
546
+ name: 'knowledge-map:protocol',
547
+ order: 0,
548
+ text: '你是 DSH 知识视图生成器。只输出调用方要求的 JSON;不要调用外部网络、文件写入或其他 Agent 工具。'
549
+ })
550
+ try {
551
+ agentCtx?.tools?.restrict?.({ deny: ['multi_agent_discuss', 'shell', 'filesystem', 'web_search', 'browser'] })
552
+ } catch { /* older runtimes may not expose tool restriction */ }
553
+ }
554
+ })
470
555
  handle.agent.followup(makeUserMessage(input.prompt, `${sessionId}-${input.kind}`))
471
556
  await handle.agent.whenIdle()
472
- return parseStructuredOutput(await readAgentText(this.sessionQuery, sessionId, input.signal))
557
+ const liveText = extractAgentText({ events: liveEvents })
558
+ logMessage(this.logger, 'info', 'agent idle session=%s liveEvents=%d assistantMessages=%d assistantChunks=%d otherEvents=%d liveTextLength=%d', logId(sessionId), diagnostics.liveEvents, diagnostics.liveAssistantMessages, diagnostics.liveAssistantChunks, diagnostics.liveOtherEvents, liveText.length)
559
+ if (liveText.trim()) return parseModelOutput(liveText, 'live-events', diagnostics)
560
+ const persistedText = await readAgentText(this.sessionQuery, sessionId, input.signal, { logger: this.logger, diagnostics })
561
+ logMessage(this.logger, 'info', 'agent persisted output session=%s textLength=%d surfaceReads=%d sessionReads=%d', logId(sessionId), persistedText.length, diagnostics.surfaceReads, diagnostics.sessionReads)
562
+ return parseModelOutput(persistedText, 'persisted-session', diagnostics)
563
+ } catch (error) {
564
+ logMessage(this.logger, 'error', 'agent call failed kind=%s session=%s liveEvents=%d surfaceReads=%d sessionReads=%d error=%s', input.kind, logId(sessionId), diagnostics.liveEvents, diagnostics.surfaceReads, diagnostics.sessionReads, errorMessage(error))
565
+ throw error
473
566
  } finally {
474
- await handle.dispose?.()
567
+ if (typeof unsubscribe === 'function') await unsubscribe()
568
+ await handle?.dispose?.()
569
+ logMessage(this.logger, 'info', 'agent call disposed kind=%s session=%s', input.kind, logId(sessionId))
475
570
  }
476
571
  }
477
572
 
package/lib/index.js CHANGED
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url'
3
3
  import { randomUUID } from 'node:crypto'
4
4
 
5
5
  import { KnowledgeGenerationOrchestrator } from './generation-orchestrator.js'
6
- import { errorMessage, jsonResponse, methodOf, parseUrl, readJson, safeId, sseWrite, shortText } from './protocol.js'
6
+ import { errorMessage, jsonResponse, loggerFacade, logMessage, methodOf, parseUrl, readJson, safeId, sseWrite, shortText } from './protocol.js'
7
7
  import { listWorkspaceSessions, normalizeWorkspacePath, resolveAnchorSession } from './session-source.js'
8
8
  import { WorkspaceRevisionError, WorkspaceStorage } from './workspace-storage.js'
9
9
 
@@ -110,17 +110,22 @@ export function createHost(options = {}) {
110
110
  const llm = options.llm || getService(ctx, 'llm')
111
111
  const skills = options.skills || getService(ctx, 'skills')
112
112
  const webServer = options.webServer || getService(ctx, 'webServer')
113
+ const logger = loggerFacade(options.logger || getService(ctx, 'logger') || ctx?.logger || console)
113
114
  const storage = options.storage || new WorkspaceStorage(options.storageOptions)
115
+ const sessionEventSource = options.sessionEventSource || (typeof ctx?.on === 'function' ? (listener) => ctx.on('session/event', listener) : null)
114
116
  const orchestrator = options.orchestrator || new KnowledgeGenerationOrchestrator({
115
117
  sessionQuery,
116
118
  sessions,
117
119
  agents,
118
120
  agentDefaultModel,
119
121
  storage,
120
- modelRunner: options.modelRunner
122
+ modelRunner: options.modelRunner,
123
+ sessionEventSource,
124
+ logger
121
125
  })
122
126
  const confirmations = new Map()
123
127
  const sseClients = new Set()
128
+ logMessage(logger, 'info', 'host apply plugin=%s services sessionQuery=%s sessions=%s agents=%s llm=%s webServer=%s sessionEvents=%s', PLUGIN_ID, Boolean(sessionQuery), Boolean(sessions), Boolean(agents?.create), Boolean(llm), Boolean(webServer?.register), Boolean(sessionEventSource))
124
129
 
125
130
  async function contextFor(sessionId) {
126
131
  const anchor = await resolveAnchorSession({ sessionQuery, sessions }, sessionId)
@@ -157,6 +162,7 @@ export function createHost(options = {}) {
157
162
  if (!group) groups.unshift({ id: defaultModel.provider, name: defaultModel.provider, models: [{ id: defaultModel.model, name: defaultModel.model }] })
158
163
  else if (!group.models.some((item) => item.id === defaultModel.model)) group.models.unshift({ id: defaultModel.model, name: defaultModel.model })
159
164
  }
165
+ logMessage(logger, 'info', 'model catalog default=%s providers=%d groups=%d', defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : 'none', providers.length, groups.length)
160
166
  return { default: defaultModel, groups }
161
167
  }
162
168
 
@@ -178,6 +184,7 @@ export function createHost(options = {}) {
178
184
  const payload = { ...input, model, anchorSessionId: context.sessionId, cwd: context.cwd, expectedRevision: state.revision }
179
185
  const token = randomUUID()
180
186
  confirmations.set(token, { payload, expiresAt: Date.now() + CONFIRMATION_TTL, used: false })
187
+ logMessage(logger, 'info', 'generation confirmed cwd=%s anchorSession=%s selectedSessions=%d outputMode=%s strict=%s model=%s expectedRevision=%d', shortText(context.cwd, 180), shortText(context.sessionId, 96), input.selectedSessionIds.length, input.outputMode, input.strict, `${model.provider}/${model.model}`, state.revision)
181
188
  return {
182
189
  token,
183
190
  expiresAt: Date.now() + CONFIRMATION_TTL,
@@ -207,6 +214,8 @@ export function createHost(options = {}) {
207
214
  const path = parsePath(req)
208
215
  const method = methodOf(req)
209
216
  if (!path) return jsonResponse(res, 404, { ok: false, error: 'Not found' })
217
+ const routeLabel = path.join('/') || '(root)'
218
+ logMessage(logger, 'info', 'route start method=%s path=%s', method, routeLabel)
210
219
  try {
211
220
  if (path.length === 0 && method === 'GET') {
212
221
  return jsonResponse(res, 200, { ok: true, plugin: PLUGIN_ID })
@@ -240,6 +249,7 @@ export function createHost(options = {}) {
240
249
  const body = await readJson(req)
241
250
  const request = consumeConfirmation(body.token, body)
242
251
  const task = orchestrator.start(request)
252
+ logMessage(logger, 'info', 'generation accepted id=%s outputMode=%s model=%s', shortText(task.id, 96), request.outputMode, `${request.model.provider}/${request.model.model}`)
243
253
  return jsonResponse(res, 202, { ok: true, generation: task })
244
254
  }
245
255
  if (path[0] === 'generations' && path[1] && path[2] === 'events' && method === 'GET') {
@@ -282,7 +292,9 @@ export function createHost(options = {}) {
282
292
  }
283
293
  return jsonResponse(res, 404, { ok: false, error: 'Not found' })
284
294
  } catch (error) {
285
- return writeError(res, error, statusForError(error))
295
+ const status = statusForError(error)
296
+ logMessage(logger, 'error', 'route failed method=%s path=%s status=%d error=%s', method, routeLabel, status, errorMessage(error))
297
+ return writeError(res, error, status)
286
298
  }
287
299
  }
288
300
 
@@ -295,6 +307,7 @@ export function createHost(options = {}) {
295
307
  content: readFileSync(SKILL_PATH, 'utf8').replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/, ''),
296
308
  resourceBase: { kind: 'directory', path: SKILL_DIR_PATH }
297
309
  })
310
+ logMessage(logger, 'info', 'host registrations complete plugin=%s', PLUGIN_ID)
298
311
  if (typeof ctx?.effect === 'function') {
299
312
  ctx.effect(() => () => {
300
313
  for (const client of sseClients.values()) client.close?.()
package/lib/protocol.js CHANGED
@@ -23,6 +23,36 @@ export function shortText(value, max = 8000) {
23
23
  return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text
24
24
  }
25
25
 
26
+ export function loggerFacade(service, name = 'conversation-knowledge-map') {
27
+ if (!service) return null
28
+ try {
29
+ const logger = typeof service === 'function' ? service(name) : service
30
+ return logger && typeof logger === 'object' ? logger : null
31
+ } catch {
32
+ return null
33
+ }
34
+ }
35
+
36
+ export function logMessage(logger, level, format, ...params) {
37
+ try { logger?.[level]?.(`[conversation-knowledge-map] ${format}`, ...params) } catch { /* logging must not affect the plugin */ }
38
+ }
39
+
40
+ function diagnosticShape(value, depth = 0) {
41
+ if (value === null) return 'null'
42
+ if (value === undefined) return 'undefined'
43
+ if (typeof value === 'string') return `string(${value.length})`
44
+ if (typeof value !== 'object') return typeof value
45
+ if (depth >= 2) return Array.isArray(value) ? `array(${value.length})` : 'object'
46
+ if (Array.isArray(value)) return `array(${value.length})[${value.slice(0, 6).map((item) => diagnosticShape(item, depth + 1)).join(',')}]`
47
+ const keys = Object.keys(value).sort()
48
+ const suffix = keys.length > 12 ? ',…' : ''
49
+ return `object{${keys.slice(0, 12).join(',')}${suffix}}`
50
+ }
51
+
52
+ export function diagnosticSummary(value) {
53
+ return diagnosticShape(value)
54
+ }
55
+
26
56
  export function clone(value) {
27
57
  return value === undefined ? undefined : JSON.parse(JSON.stringify(value))
28
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/conversation-knowledge-map",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",