@p-dsh-market/conversation-knowledge-map 0.1.2 → 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 ''
@@ -259,6 +283,7 @@ export class KnowledgeGenerationOrchestrator {
259
283
  storage,
260
284
  modelRunner,
261
285
  sessionEventSource,
286
+ logger,
262
287
  sourceReader = readSelectedSurfaces,
263
288
  now = () => Date.now(),
264
289
  idFactory = () => randomUUID()
@@ -270,6 +295,7 @@ export class KnowledgeGenerationOrchestrator {
270
295
  this.storage = storage
271
296
  this.modelRunner = modelRunner
272
297
  this.sessionEventSource = sessionEventSource
298
+ this.logger = logger
273
299
  this.sourceReader = sourceReader
274
300
  this.now = now
275
301
  this.idFactory = idFactory
@@ -338,6 +364,7 @@ export class KnowledgeGenerationOrchestrator {
338
364
  }
339
365
  this.tasks.set(task.id, task)
340
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))
341
368
  this.update(task, 'created', { request: { ...request, prompt: shortText(request.prompt, 500) } })
342
369
  task.promise = this.run(task, request).finally(() => {
343
370
  if (this.busyByWorkspace.get(key) === task.id) this.busyByWorkspace.delete(key)
@@ -366,6 +393,7 @@ export class KnowledgeGenerationOrchestrator {
366
393
  includeSubagents: request.includeSubagents === true
367
394
  })
368
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))
369
397
  this.update(task, 'summarizing', { sourceCount: sources.length })
370
398
  const summaries = []
371
399
  for (const source of sources) {
@@ -430,11 +458,13 @@ export class KnowledgeGenerationOrchestrator {
430
458
  })
431
459
  task.revision = saved.revision
432
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)
433
462
  this.update(task, 'completed', { revision: saved.revision, result: task.result })
434
463
  } catch (error) {
435
464
  if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
436
465
  this.update(task, 'cancelled', { error: '' })
437
466
  } else {
467
+ logMessage(this.logger, 'error', 'generation failed id=%s status=%s error=%s', logId(task.id), task.status, errorMessage(error))
438
468
  this.update(task, 'failed', { error: errorMessage(error) })
439
469
  }
440
470
  }
@@ -442,25 +472,67 @@ export class KnowledgeGenerationOrchestrator {
442
472
  }
443
473
 
444
474
  async runModel(input) {
445
- 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
+ }
446
490
  if (!this.agents?.create) throw new Error('当前 DSH Runtime 未提供 agents.create,无法生成知识视图。')
447
491
  let selection = normalizeModelSelection(input.model)
448
492
  if (!selection) {
449
- 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
+ }
450
499
  }
451
500
  const provider = String(selection?.provider || '').trim()
452
501
  const model = String(selection?.model || '').trim()
453
- 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
+ }
454
506
  const sessionId = `knowledge-map-${this.idFactory()}`
455
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)
456
519
  let unsubscribe
457
520
  if (typeof this.sessionEventSource === 'function') {
458
521
  try {
459
522
  unsubscribe = this.sessionEventSource((session, event) => {
460
523
  const eventSessionId = String(session?.id || session?.header?.id || '')
461
- if (eventSessionId === sessionId && event) liveEvents.push(event)
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))
462
531
  })
463
- } catch { /* event subscription is an optimization; surface reads remain the fallback */ }
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))
535
+ }
464
536
  }
465
537
  let handle
466
538
  try {
@@ -483,11 +555,18 @@ export class KnowledgeGenerationOrchestrator {
483
555
  handle.agent.followup(makeUserMessage(input.prompt, `${sessionId}-${input.kind}`))
484
556
  await handle.agent.whenIdle()
485
557
  const liveText = extractAgentText({ events: liveEvents })
486
- if (liveText.trim()) return parseStructuredOutput(liveText)
487
- return parseStructuredOutput(await readAgentText(this.sessionQuery, sessionId, input.signal))
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
488
566
  } finally {
489
- await unsubscribe?.()
567
+ if (typeof unsubscribe === 'function') await unsubscribe()
490
568
  await handle?.dispose?.()
569
+ logMessage(this.logger, 'info', 'agent call disposed kind=%s session=%s', input.kind, logId(sessionId))
491
570
  }
492
571
  }
493
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,6 +110,7 @@ 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)
114
115
  const sessionEventSource = options.sessionEventSource || (typeof ctx?.on === 'function' ? (listener) => ctx.on('session/event', listener) : null)
115
116
  const orchestrator = options.orchestrator || new KnowledgeGenerationOrchestrator({
@@ -119,10 +120,12 @@ export function createHost(options = {}) {
119
120
  agentDefaultModel,
120
121
  storage,
121
122
  modelRunner: options.modelRunner,
122
- sessionEventSource
123
+ sessionEventSource,
124
+ logger
123
125
  })
124
126
  const confirmations = new Map()
125
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))
126
129
 
127
130
  async function contextFor(sessionId) {
128
131
  const anchor = await resolveAnchorSession({ sessionQuery, sessions }, sessionId)
@@ -159,6 +162,7 @@ export function createHost(options = {}) {
159
162
  if (!group) groups.unshift({ id: defaultModel.provider, name: defaultModel.provider, models: [{ id: defaultModel.model, name: defaultModel.model }] })
160
163
  else if (!group.models.some((item) => item.id === defaultModel.model)) group.models.unshift({ id: defaultModel.model, name: defaultModel.model })
161
164
  }
165
+ logMessage(logger, 'info', 'model catalog default=%s providers=%d groups=%d', defaultModel ? `${defaultModel.provider}/${defaultModel.model}` : 'none', providers.length, groups.length)
162
166
  return { default: defaultModel, groups }
163
167
  }
164
168
 
@@ -180,6 +184,7 @@ export function createHost(options = {}) {
180
184
  const payload = { ...input, model, anchorSessionId: context.sessionId, cwd: context.cwd, expectedRevision: state.revision }
181
185
  const token = randomUUID()
182
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)
183
188
  return {
184
189
  token,
185
190
  expiresAt: Date.now() + CONFIRMATION_TTL,
@@ -209,6 +214,8 @@ export function createHost(options = {}) {
209
214
  const path = parsePath(req)
210
215
  const method = methodOf(req)
211
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)
212
219
  try {
213
220
  if (path.length === 0 && method === 'GET') {
214
221
  return jsonResponse(res, 200, { ok: true, plugin: PLUGIN_ID })
@@ -242,6 +249,7 @@ export function createHost(options = {}) {
242
249
  const body = await readJson(req)
243
250
  const request = consumeConfirmation(body.token, body)
244
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}`)
245
253
  return jsonResponse(res, 202, { ok: true, generation: task })
246
254
  }
247
255
  if (path[0] === 'generations' && path[1] && path[2] === 'events' && method === 'GET') {
@@ -284,7 +292,9 @@ export function createHost(options = {}) {
284
292
  }
285
293
  return jsonResponse(res, 404, { ok: false, error: 'Not found' })
286
294
  } catch (error) {
287
- 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)
288
298
  }
289
299
  }
290
300
 
@@ -297,6 +307,7 @@ export function createHost(options = {}) {
297
307
  content: readFileSync(SKILL_PATH, 'utf8').replace(/^---\s*\r?\n[\s\S]*?\r?\n---\s*\r?\n?/, ''),
298
308
  resourceBase: { kind: 'directory', path: SKILL_DIR_PATH }
299
309
  })
310
+ logMessage(logger, 'info', 'host registrations complete plugin=%s', PLUGIN_ID)
300
311
  if (typeof ctx?.effect === 'function') {
301
312
  ctx.effect(() => () => {
302
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.2",
3
+ "version": "0.1.3",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",