@p-dsh-market/conversation-knowledge-map 0.1.0 → 0.1.2

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
@@ -9,6 +9,8 @@ DSH 的“知识视图”插件,把同一工作路径下用户明确选择的
9
9
 
10
10
  生成流程必须经过菜单栏配置和应用内确认。Host 会再次校验锚点 Session、`cwd`、选中 Session、revision 和固定保存目录;模型输出先通过图数据 Schema 校验,失败或取消不会替换旧结果。
11
11
 
12
+ 配置面板会列出运行时可用的 Provider / Model,并默认带入 DSH 默认模型;本次选择会绑定到确认令牌、Agent 调用和 `manifest.json`,不会因为默认模型变化而被静默替换。提交任务后配置对话框立即关闭,失败原因会显示在知识视图页的任务条中。
13
+
12
14
  节点“继续对话”只形成一个可编辑的后续问题。确认导航后,插件使用公开的 Session 导航入口;如果当前 Runtime 没有向该槽位暴露草稿镜像,则提供“打开并复制问题”的安全降级,不自动发送消息。
13
15
 
14
16
  ## 本地验证
package/cordis.patch.yml CHANGED
@@ -1,4 +1,4 @@
1
1
  - insert:
2
2
  - id: p-dsh-market-conversation-knowledge-map
3
3
  name: '@p-dsh-market/conversation-knowledge-map'
4
- inject: [agentDefaultModel, agents, sessionQuery, sessions, skills, webServer]
4
+ inject: [agentDefaultModel, agents, llm, sessionQuery, sessions, skills, webServer]
package/lib/client.js CHANGED
@@ -92,6 +92,7 @@ window.__ModuleLoader__.load({
92
92
  context: null,
93
93
  state: null,
94
94
  sessions: [],
95
+ modelCatalog: null,
95
96
  generation: null,
96
97
  loading: false,
97
98
  loaded: false,
@@ -450,6 +451,15 @@ window.__ModuleLoader__.load({
450
451
  var includePair = React.useState(false)
451
452
  var includeSubagents = includePair[0]
452
453
  var setIncludeSubagents = includePair[1]
454
+ var modelCatalogPair = React.useState(null)
455
+ var modelCatalog = modelCatalogPair[0]
456
+ var setModelCatalog = modelCatalogPair[1]
457
+ var providerPair = React.useState('')
458
+ var modelProvider = providerPair[0]
459
+ var setModelProvider = providerPair[1]
460
+ var modelIdPair = React.useState('')
461
+ var modelId = modelIdPair[0]
462
+ var setModelId = modelIdPair[1]
453
463
  var confirmationPair = React.useState(null)
454
464
  var confirmation = confirmationPair[0]
455
465
  var setConfirmation = confirmationPair[1]
@@ -466,17 +476,30 @@ window.__ModuleLoader__.load({
466
476
  setSelected(function (old) { return old.length ? old.filter(function (id) { return (body.sessions || []).some(function (item) { return item.id === id }) }) : [store.sessionId] })
467
477
  }).catch(function (error) { setMessage(error.message || String(error)) })
468
478
  }, [store.context && store.context.cwd, includeSubagents])
479
+ React.useEffect(function () {
480
+ if (!store.context || !store.context.ready) return
481
+ request('/models').then(function (body) {
482
+ var catalog = body.catalog || { default: null, groups: [] }
483
+ setModelCatalog(catalog)
484
+ var defaultModel = catalog.default || {}
485
+ var firstGroup = catalog.groups && catalog.groups[0] ? catalog.groups[0] : {}
486
+ var firstModel = firstGroup.models && firstGroup.models[0] ? firstGroup.models[0].id : ''
487
+ setModelProvider(function (old) { return old || defaultModel.provider || firstGroup.id || '' })
488
+ setModelId(function (old) { return old || defaultModel.model || firstModel || '' })
489
+ }).catch(function (error) { setMessage(error.message || String(error)) })
490
+ }, [store.context && store.context.cwd])
469
491
  function toggle(id) { setSelected(function (old) { return old.indexOf(id) >= 0 ? old.filter(function (item) { return item !== id }) : old.concat([id]) }) }
470
492
  function confirm() {
471
493
  if (!selected.length) { setMessage('至少选择一个对话。'); return }
472
494
  setBusy(true)
473
- request('/confirm', { method: 'POST', body: { anchorSessionId: store.sessionId, selectedSessionIds: selected, outputMode: mode, prompt: prompt, strict: strict, includeSubagents: includeSubagents, expectedRevision: store.state ? store.state.revision : 0 } }).then(function (body) { setConfirmation(body.confirmation); setMessage(''); setBusy(false) }).catch(function (error) { setMessage(error.message || String(error)); setBusy(false) })
495
+ request('/confirm', { method: 'POST', body: { anchorSessionId: store.sessionId, selectedSessionIds: selected, outputMode: mode, prompt: prompt, strict: strict, includeSubagents: includeSubagents, model: { provider: modelProvider, model: modelId }, expectedRevision: store.state ? store.state.revision : 0 } }).then(function (body) { setConfirmation(body.confirmation); setMessage(''); setBusy(false) }).catch(function (error) { setMessage(error.message || String(error)); setBusy(false) })
474
496
  }
475
497
  function start() {
476
498
  setBusy(true)
477
- request('/generations', { method: 'POST', body: { token: confirmation.token, anchorSessionId: store.sessionId, selectedSessionIds: selected, outputMode: mode, prompt: prompt, strict: strict, includeSubagents: includeSubagents, expectedRevision: confirmation.revision } }).then(function (body) {
499
+ request('/generations', { method: 'POST', body: { token: confirmation.token, anchorSessionId: store.sessionId, selectedSessionIds: selected, outputMode: mode, prompt: prompt, strict: strict, includeSubagents: includeSubagents, model: confirmation.model, expectedRevision: confirmation.revision } }).then(function (body) {
478
500
  setConfirmation(null)
479
501
  setBusy(false)
502
+ setOverlayOpen(false)
480
503
  applyGeneration(store, body.generation)
481
504
  openGenerationStream(store, body.generation.id)
482
505
  }).catch(function (error) { setMessage(error.message || String(error)); setBusy(false) })
@@ -484,7 +507,7 @@ window.__ModuleLoader__.load({
484
507
  if (!store.context || !store.context.ready) return React.createElement('div', { className: 'ckm-modal-backdrop' }, React.createElement('div', { className: 'ckm-modal' }, [React.createElement('h3', { key: 'title' }, '知识视图'), React.createElement('p', { key: 'copy' }, '请先打开一个有明确工作路径的已有对话。'), Button({ key: 'close', className: 'ckm-primary', onClick: function () { setOverlayOpen(false) } }, '关闭')]))
485
508
  return React.createElement('div', { className: 'ckm-modal-backdrop' }, React.createElement('div', { className: 'ckm-modal ckm-config-modal', role: 'dialog', 'aria-modal': 'true' }, confirmation ? [
486
509
  React.createElement('h3', { key: 'title' }, '确认生成知识视图?'),
487
- React.createElement('dl', { key: 'summary', className: 'ckm-confirm-summary' }, [React.createElement('dt', { key: 'cwd-label' }, '工作路径'), React.createElement('dd', { key: 'cwd' }, store.context.cwd), React.createElement('dt', { key: 'source-label' }, '来源'), React.createElement('dd', { key: 'sources' }, confirmation.selectedSessions.length + ' 个已选择对话'), React.createElement('dt', { key: 'output-label' }, '生成'), React.createElement('dd', { key: 'output' }, mode === 'both' ? '思维导图 + 知识图谱' : mode), React.createElement('dt', { key: 'strict-label' }, '约束'), React.createElement('dd', { key: 'strict' }, strict ? '严格约束已开启' : '普通约束'), React.createElement('dt', { key: 'save-label' }, '保存'), React.createElement('dd', { key: 'save' }, '.g-dsh-market-knowledge' + (confirmation.overwrite ? '(将替换已有结果)' : ''))]),
510
+ React.createElement('dl', { key: 'summary', className: 'ckm-confirm-summary' }, [React.createElement('dt', { key: 'cwd-label' }, '工作路径'), React.createElement('dd', { key: 'cwd' }, store.context.cwd), React.createElement('dt', { key: 'source-label' }, '来源'), React.createElement('dd', { key: 'sources' }, confirmation.selectedSessions.length + ' 个已选择对话'), React.createElement('dt', { key: 'output-label' }, '生成'), React.createElement('dd', { key: 'output' }, mode === 'both' ? '思维导图 + 知识图谱' : mode), React.createElement('dt', { key: 'model-label' }, '模型'), React.createElement('dd', { key: 'model' }, confirmation.model.provider + ' / ' + confirmation.model.model), React.createElement('dt', { key: 'strict-label' }, '约束'), React.createElement('dd', { key: 'strict' }, strict ? '严格约束已开启' : '普通约束'), React.createElement('dt', { key: 'save-label' }, '保存'), React.createElement('dd', { key: 'save' }, '.g-dsh-market-knowledge' + (confirmation.overwrite ? '(将替换已有结果)' : ''))]),
488
511
  React.createElement('p', { key: 'note', className: 'ckm-warning' }, '确认后才会读取所选对话正文、调用模型并写入工作区;不会自动发送消息。'),
489
512
  React.createElement('div', { key: 'actions', className: 'ckm-modal-actions' }, [Button({ key: 'back', className: 'ckm-secondary', onClick: function () { setConfirmation(null) }, disabled: busy }, '返回修改'), Button({ key: 'ok', className: 'ckm-primary', onClick: start, disabled: busy }, busy ? '生成中…' : '确认并生成')])
490
513
  ] : [
@@ -507,6 +530,17 @@ window.__ModuleLoader__.load({
507
530
  ])
508
531
  ]),
509
532
  React.createElement('label', { key: 'mode', className: 'ckm-field' }, ['生成内容', React.createElement('select', { key: 'select', value: mode, onChange: function (event) { setMode(event.target.value) } }, [React.createElement('option', { key: 'both', value: 'both' }, '思维导图 + 知识图谱'), React.createElement('option', { key: 'mind', value: 'mind-map' }, '仅思维导图'), React.createElement('option', { key: 'graph', value: 'knowledge-graph' }, '仅知识图谱')])]),
533
+ React.createElement('label', { key: 'model', className: 'ckm-field' }, [
534
+ '生成模型',
535
+ React.createElement('span', { key: 'hint', className: 'ckm-panel-hint' }, '默认带入 DSH 默认模型;可以为本次知识视图单独选择 Provider / Model。'),
536
+ modelCatalog && modelCatalog.groups && modelCatalog.groups.length ? React.createElement('div', { key: 'selectors', className: 'ckm-model-selectors' }, [
537
+ React.createElement('select', { key: 'provider', value: modelProvider, onChange: function (event) { var next = event.target.value; var group = modelCatalog.groups.filter(function (item) { return item.id === next })[0]; setModelProvider(next); setModelId(group && group.models && group.models[0] ? group.models[0].id : '') } }, modelCatalog.groups.map(function (group) { return React.createElement('option', { key: group.id, value: group.id }, group.name + '(' + group.id + ')') })),
538
+ React.createElement('select', { key: 'model', value: modelId, onChange: function (event) { setModelId(event.target.value) } }, ((modelCatalog.groups.filter(function (item) { return item.id === modelProvider })[0] || {}).models || []).map(function (item) { return React.createElement('option', { key: item.id, value: item.id }, item.name + '(' + item.id + ')') }))
539
+ ]) : React.createElement('div', { key: 'inputs', className: 'ckm-model-selectors' }, [
540
+ React.createElement('input', { key: 'provider', value: modelProvider, placeholder: 'Provider', onChange: function (event) { setModelProvider(event.target.value) } }),
541
+ React.createElement('input', { key: 'model', value: modelId, placeholder: 'Model', onChange: function (event) { setModelId(event.target.value) } })
542
+ ])
543
+ ]),
510
544
  React.createElement('label', { key: 'prompt', className: 'ckm-field' }, ['额外要求', React.createElement('textarea', { key: 'textarea', rows: 4, maxLength: 4000, value: prompt, placeholder: '请输入形成思维导图或知识图谱时需要遵守的 Prompt…', onChange: function (event) { setPrompt(event.target.value) } })]),
511
545
  React.createElement('label', { key: 'strict', className: 'ckm-inline-field' }, [React.createElement('input', { key: 'check', type: 'checkbox', checked: strict, onChange: function (event) { setStrict(event.target.checked) } }), '严格约束模式(来源、路径、工具和写入均由 Host 校验)']),
512
546
  message ? React.createElement('p', { key: 'message', className: 'ckm-error' }, message) : null,
@@ -531,7 +565,7 @@ window.__ModuleLoader__.load({
531
565
  '.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}' +
532
566
  '.ckm-generation-strip{display:flex;align-items:center;gap:12px;padding:9px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-state-business-tertiary);font-size:12px}.ckm-generation-strip span{color:var(--dsw-alias-label-secondary)}.ckm-generation-strip .ckm-danger{margin-left:auto}' +
533
567
  '.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}' +
534
- '.ckm-graph-layout{grid-template-columns:minmax(0,1fr) 330px}.ckm-graph-toolbar{display:flex;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}' +
568
+ '.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}' +
535
569
  '.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)}' +
536
570
  '@media(max-width:800px){.ckm-workspace,.ckm-graph-layout{display:flex;flex-direction:column}.ckm-detail{border-top:1px solid var(--dsw-alias-border-l2);border-left:0}.ckm-page-header{align-items:flex-start;flex-direction:column}.ckm-page-actions{width:100%;flex-wrap:wrap}}'
537
571
 
@@ -23,26 +23,89 @@ function phaseMessage(status) {
23
23
  }
24
24
 
25
25
  function asObject(value) {
26
- if (value && typeof value === 'object') {
27
- if (value.result && typeof value.result === 'object') return value.result
28
- if (value.value && typeof value.value === 'object') return value.value
26
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
27
+ const wrapperKeys = ['result', 'value', 'output', 'data', 'message', 'content', 'text']
28
+ for (const key of wrapperKeys) {
29
+ if (value[key] && typeof value[key] === 'object' && !Array.isArray(value[key])) return asObject(value[key])
30
+ }
31
+ if (wrapperKeys.some((key) => typeof value[key] === 'string')) return null
29
32
  return value
30
33
  }
31
34
  return null
32
35
  }
33
36
 
37
+ function textFromContent(value) {
38
+ if (typeof value === 'string') return value
39
+ if (Array.isArray(value)) {
40
+ return value.map((block) => {
41
+ if (!block || typeof block !== 'object' || block.type === 'reasoning') return ''
42
+ if (typeof block.text === 'string') return block.text
43
+ if (typeof block.value === 'string') return block.value
44
+ if (block.type === 'tool-result') return textFromContent(block.content)
45
+ return ''
46
+ }).filter(Boolean).join('\n\n')
47
+ }
48
+ if (value && typeof value === 'object') {
49
+ if (typeof value.text === 'string') return value.text
50
+ if (typeof value.value === 'string') return value.value
51
+ for (const key of ['content', 'result', 'output', 'data', 'message']) {
52
+ if (value[key] !== undefined) return textFromContent(value[key])
53
+ }
54
+ }
55
+ return ''
56
+ }
57
+
58
+ function findJsonObject(text) {
59
+ const candidates = []
60
+ const fenced = String(text || '').match(/```(?:json)?\s*([\s\S]*?)```/i)
61
+ if (fenced) candidates.push(fenced[1])
62
+ candidates.push(String(text || ''))
63
+ for (const candidate of candidates) {
64
+ for (let start = 0; start < candidate.length; start += 1) {
65
+ if (candidate[start] !== '{') continue
66
+ let depth = 0
67
+ let inString = false
68
+ let escaped = false
69
+ for (let index = start; index < candidate.length; index += 1) {
70
+ const char = candidate[index]
71
+ if (inString) {
72
+ if (escaped) escaped = false
73
+ else if (char === '\\') escaped = true
74
+ else if (char === '"') inString = false
75
+ continue
76
+ }
77
+ if (char === '"') {
78
+ inString = true
79
+ continue
80
+ }
81
+ if (char === '{') depth += 1
82
+ else if (char === '}') {
83
+ depth -= 1
84
+ if (depth === 0) {
85
+ const fragment = candidate.slice(start, index + 1)
86
+ try {
87
+ const parsed = JSON.parse(fragment)
88
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed
89
+ } catch {
90
+ // Try the next opening brace in case prose contained an example.
91
+ }
92
+ break
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+ return null
99
+ }
100
+
34
101
  export function parseStructuredOutput(value) {
35
102
  const object = asObject(value)
36
103
  if (object) return object
37
- const text = String(value || '').trim().replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '')
38
- const start = text.indexOf('{')
39
- const end = text.lastIndexOf('}')
40
- if (start < 0 || end <= start) throw new Error('模型没有返回 JSON 对象。')
41
- try {
42
- return JSON.parse(text.slice(start, end + 1))
43
- } catch (error) {
44
- throw new Error(`模型 JSON 无法解析:${error.message}`)
45
- }
104
+ const text = textFromContent(value).replace(/^\uFEFF/, '').trim()
105
+ const parsed = findJsonObject(text)
106
+ if (parsed) return parsed
107
+ if (!text) throw new Error('模型没有返回 JSON 对象。')
108
+ throw new Error('模型返回了文本,但其中没有可解析的 JSON 对象。')
46
109
  }
47
110
 
48
111
  function sourceRefsFromChunk(source, chunk) {
@@ -139,20 +202,54 @@ function followUpPrompt(node, source, targetSessionId, strict) {
139
202
  ].join('\n\n')
140
203
  }
141
204
 
205
+ function messageText(message) {
206
+ if (!message || typeof message !== 'object') return ''
207
+ return textFromContent(message.content) || textFromContent(message.text) || textFromContent(message.value) || textFromContent(message.message)
208
+ }
209
+
142
210
  function extractAgentText(surface) {
143
211
  const events = Array.isArray(surface?.events) ? surface.events : []
144
212
  for (let index = events.length - 1; index >= 0; index -= 1) {
145
213
  const event = events[index]
146
214
  if (event?.type === 'assistant/message') {
147
- const message = event.data?.message || event.data || {}
148
- const content = message.content
149
- if (Array.isArray(content)) return content.filter((block) => block?.type !== 'reasoning').map((block) => block?.text || block?.value || '').join('')
150
- if (typeof content === 'string') return content
215
+ const data = event.data && typeof event.data === 'object' ? event.data : {}
216
+ const text = messageText(data.message || data) || messageText(data)
217
+ if (text) return text
151
218
  }
152
219
  }
220
+ const chunks = events.filter((event) => event?.type === 'assistant/chunk').map((event) => {
221
+ const data = event.data && typeof event.data === 'object' ? event.data : {}
222
+ return textFromContent(data.chunk || data.text || data.content)
223
+ }).filter(Boolean)
224
+ return chunks.join('')
225
+ }
226
+
227
+ async function readAgentText(sessionQuery, sessionId, signal) {
228
+ for (let attempt = 0; attempt < 6; attempt += 1) {
229
+ if (signal?.aborted) throw signal.reason || new Error('生成已取消。')
230
+ const surface = await sessionQuery?.readSurface?.(sessionId)
231
+ const text = extractAgentText(surface)
232
+ if (text.trim()) return text
233
+ if (attempt < 5) await new Promise((resolve) => setTimeout(resolve, 40 * (attempt + 1)))
234
+ }
235
+ if (typeof sessionQuery?.readSession === 'function') {
236
+ const log = await sessionQuery.readSession(sessionId)
237
+ const text = extractAgentText(log)
238
+ if (text.trim()) return text
239
+ }
153
240
  return ''
154
241
  }
155
242
 
243
+ function normalizeModelSelection(value) {
244
+ if (value === undefined || value === null || value === '') return null
245
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('模型选择必须是包含 Provider 和 Model 的对象。')
246
+ const provider = String(value.provider || '').trim()
247
+ const model = String(value.model || '').trim()
248
+ if (!provider && !model) return null
249
+ if (!provider || !model) throw new Error('Provider 和 Model 必须同时填写。')
250
+ return { provider, model }
251
+ }
252
+
156
253
  export class KnowledgeGenerationOrchestrator {
157
254
  constructor({
158
255
  sessionQuery,
@@ -161,6 +258,7 @@ export class KnowledgeGenerationOrchestrator {
161
258
  agentDefaultModel,
162
259
  storage,
163
260
  modelRunner,
261
+ sessionEventSource,
164
262
  sourceReader = readSelectedSurfaces,
165
263
  now = () => Date.now(),
166
264
  idFactory = () => randomUUID()
@@ -171,6 +269,7 @@ export class KnowledgeGenerationOrchestrator {
171
269
  this.agentDefaultModel = agentDefaultModel
172
270
  this.storage = storage
173
271
  this.modelRunner = modelRunner
272
+ this.sessionEventSource = sessionEventSource
174
273
  this.sourceReader = sourceReader
175
274
  this.now = now
176
275
  this.idFactory = idFactory
@@ -278,6 +377,7 @@ export class KnowledgeGenerationOrchestrator {
278
377
  cwd: request.cwd,
279
378
  strict: request.strict === true,
280
379
  selectedSessionIds: request.selectedSessionIds,
380
+ model: request.model,
281
381
  signal: task.controller.signal
282
382
  })
283
383
  summaries.push(normalizeSummary(value, source, chunk))
@@ -294,6 +394,7 @@ export class KnowledgeGenerationOrchestrator {
294
394
  cwd: request.cwd,
295
395
  strict: request.strict === true,
296
396
  selectedSessionIds: request.selectedSessionIds,
397
+ model: request.model,
297
398
  signal: task.controller.signal
298
399
  })
299
400
  mindMap = validateMindMap(value, { selectedSessionIds: request.selectedSessionIds, strict: request.strict === true })
@@ -306,6 +407,7 @@ export class KnowledgeGenerationOrchestrator {
306
407
  cwd: request.cwd,
307
408
  strict: request.strict === true,
308
409
  selectedSessionIds: request.selectedSessionIds,
410
+ model: request.model,
309
411
  signal: task.controller.signal
310
412
  })
311
413
  knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds: request.selectedSessionIds, strict: request.strict === true })
@@ -342,39 +444,54 @@ export class KnowledgeGenerationOrchestrator {
342
444
  async runModel(input) {
343
445
  if (typeof this.modelRunner === 'function') return parseStructuredOutput(await this.modelRunner(input))
344
446
  if (!this.agents?.create) throw new Error('当前 DSH Runtime 未提供 agents.create,无法生成知识视图。')
345
- let selection = {}
346
- try { selection = this.agentDefaultModel?.currentSelection?.() || {} } catch { selection = {} }
347
- const provider = String(selection.provider || '').trim()
348
- const model = String(selection.model || '').trim()
447
+ let selection = normalizeModelSelection(input.model)
448
+ if (!selection) {
449
+ try { selection = normalizeModelSelection(this.agentDefaultModel?.currentSelection?.()) } catch { selection = null }
450
+ }
451
+ const provider = String(selection?.provider || '').trim()
452
+ const model = String(selection?.model || '').trim()
349
453
  if (!provider || !model) throw new Error('当前没有可用的默认 Provider/Model。')
350
454
  const sessionId = `knowledge-map-${this.idFactory()}`
351
- const handle = await this.agents.create({
352
- sessionId,
353
- meta: { cwd: input.cwd, origin: 'subagent' },
354
- agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? 2500 : 6000 },
355
- signal: input.signal,
356
- setup: async (agentCtx) => {
357
- agentCtx?.systemPrompt?.section?.({
358
- name: 'knowledge-map:protocol',
359
- order: 0,
360
- text: '你是 DSH 知识视图生成器。只输出调用方要求的 JSON;不要调用外部网络、文件写入或其他 Agent 工具。'
455
+ const liveEvents = []
456
+ let unsubscribe
457
+ if (typeof this.sessionEventSource === 'function') {
458
+ try {
459
+ unsubscribe = this.sessionEventSource((session, event) => {
460
+ const eventSessionId = String(session?.id || session?.header?.id || '')
461
+ if (eventSessionId === sessionId && event) liveEvents.push(event)
361
462
  })
362
- try {
363
- agentCtx?.tools?.restrict?.({ deny: ['multi_agent_discuss', 'shell', 'filesystem', 'web_search', 'browser'] })
364
- } catch { /* older runtimes may not expose tool restriction */ }
365
- }
366
- })
463
+ } catch { /* event subscription is an optimization; surface reads remain the fallback */ }
464
+ }
465
+ let handle
367
466
  try {
467
+ handle = await this.agents.create({
468
+ sessionId,
469
+ meta: { cwd: input.cwd, origin: 'subagent' },
470
+ agentOptions: { provider, model, maxTokens: input.kind === 'summary' ? 2500 : 6000 },
471
+ signal: input.signal,
472
+ setup: async (agentCtx) => {
473
+ agentCtx?.systemPrompt?.section?.({
474
+ name: 'knowledge-map:protocol',
475
+ order: 0,
476
+ text: '你是 DSH 知识视图生成器。只输出调用方要求的 JSON;不要调用外部网络、文件写入或其他 Agent 工具。'
477
+ })
478
+ try {
479
+ agentCtx?.tools?.restrict?.({ deny: ['multi_agent_discuss', 'shell', 'filesystem', 'web_search', 'browser'] })
480
+ } catch { /* older runtimes may not expose tool restriction */ }
481
+ }
482
+ })
368
483
  handle.agent.followup(makeUserMessage(input.prompt, `${sessionId}-${input.kind}`))
369
484
  await handle.agent.whenIdle()
370
- const surface = await this.sessionQuery?.readSurface?.(sessionId)
371
- return parseStructuredOutput(extractAgentText(surface))
485
+ const liveText = extractAgentText({ events: liveEvents })
486
+ if (liveText.trim()) return parseStructuredOutput(liveText)
487
+ return parseStructuredOutput(await readAgentText(this.sessionQuery, sessionId, input.signal))
372
488
  } finally {
373
- await handle.dispose?.()
489
+ await unsubscribe?.()
490
+ await handle?.dispose?.()
374
491
  }
375
492
  }
376
493
 
377
- async formFollowUp({ cwd, node, targetSessionId, strict = true, signal }) {
494
+ async formFollowUp({ cwd, node, targetSessionId, strict = true, model, signal }) {
378
495
  const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
379
496
  cwd,
380
497
  sessionIds: [targetSessionId],
@@ -386,6 +503,7 @@ export class KnowledgeGenerationOrchestrator {
386
503
  cwd,
387
504
  strict,
388
505
  selectedSessionIds: [targetSessionId],
506
+ model,
389
507
  signal
390
508
  })
391
509
  const result = parseStructuredOutput(value)
package/lib/index.js CHANGED
@@ -41,6 +41,25 @@ function normalizeOutputMode(value) {
41
41
  return mode
42
42
  }
43
43
 
44
+ function normalizeModelSelection(value) {
45
+ if (value === undefined || value === null || value === '') return null
46
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error('模型选择必须是包含 Provider 和 Model 的对象。')
47
+ const provider = String(value.provider || '').trim()
48
+ const model = String(value.model || '').trim()
49
+ if (!provider && !model) return null
50
+ if (!provider || !model) throw new Error('Provider 和 Model 必须同时填写。')
51
+ if (provider.length > 160 || model.length > 240) throw new Error('Provider 或 Model 名称过长。')
52
+ return { provider, model }
53
+ }
54
+
55
+ function currentModelSelection(service) {
56
+ try {
57
+ return normalizeModelSelection(service?.currentSelection?.())
58
+ } catch {
59
+ return null
60
+ }
61
+ }
62
+
44
63
  function normalizeGenerationInput(body = {}) {
45
64
  const selectedSessionIds = [...new Set((Array.isArray(body.selectedSessionIds) ? body.selectedSessionIds : []).map((id) => String(id || '').trim()).filter(Boolean))]
46
65
  if (!selectedSessionIds.length) throw new Error('至少选择一个对话。')
@@ -54,6 +73,7 @@ function normalizeGenerationInput(body = {}) {
54
73
  prompt,
55
74
  strict: body.strict !== false,
56
75
  includeSubagents: body.includeSubagents === true,
76
+ model: normalizeModelSelection(body.model),
57
77
  expectedRevision: Number.isInteger(expectedRevision) && expectedRevision >= 0 ? expectedRevision : 0
58
78
  }
59
79
  }
@@ -67,6 +87,7 @@ function canonicalPayload(value) {
67
87
  prompt: value.prompt,
68
88
  strict: value.strict === true,
69
89
  includeSubagents: value.includeSubagents === true,
90
+ model: value.model ? { provider: value.model.provider, model: value.model.model } : null,
70
91
  expectedRevision: Number(value.expectedRevision || 0)
71
92
  })
72
93
  }
@@ -79,23 +100,26 @@ function statusForError(error) {
79
100
 
80
101
  export function createHost(options = {}) {
81
102
  const host = {
82
- inject: ['agentDefaultModel', 'agents', 'sessionQuery', 'sessions', 'skills', 'webServer'],
103
+ inject: ['agentDefaultModel', 'agents', 'llm', 'sessionQuery', 'sessions', 'skills', 'webServer'],
83
104
 
84
105
  apply(ctx) {
85
106
  const sessionQuery = options.sessionQuery || getService(ctx, 'sessionQuery')
86
107
  const sessions = options.sessions || getService(ctx, 'sessions')
87
108
  const agents = options.agents || getService(ctx, 'agents')
88
109
  const agentDefaultModel = options.agentDefaultModel || getService(ctx, 'agentDefaultModel')
110
+ const llm = options.llm || getService(ctx, 'llm')
89
111
  const skills = options.skills || getService(ctx, 'skills')
90
112
  const webServer = options.webServer || getService(ctx, 'webServer')
91
113
  const storage = options.storage || new WorkspaceStorage(options.storageOptions)
114
+ const sessionEventSource = options.sessionEventSource || (typeof ctx?.on === 'function' ? (listener) => ctx.on('session/event', listener) : null)
92
115
  const orchestrator = options.orchestrator || new KnowledgeGenerationOrchestrator({
93
116
  sessionQuery,
94
117
  sessions,
95
118
  agents,
96
119
  agentDefaultModel,
97
120
  storage,
98
- modelRunner: options.modelRunner
121
+ modelRunner: options.modelRunner,
122
+ sessionEventSource
99
123
  })
100
124
  const confirmations = new Map()
101
125
  const sseClients = new Set()
@@ -109,6 +133,35 @@ export function createHost(options = {}) {
109
133
  return { ready: true, state: 'ready', sessionId: String(header.id), cwd, origin: String(header.origin || '') }
110
134
  }
111
135
 
136
+ async function modelCatalog() {
137
+ const defaultModel = currentModelSelection(agentDefaultModel)
138
+ const providers = typeof llm?.listProviders === 'function' ? llm.listProviders() : []
139
+ const groups = (await Promise.all(providers.map(async (provider) => {
140
+ const providerId = String(provider?.id || '').trim()
141
+ if (!providerId || typeof llm?.listModels !== 'function') return null
142
+ try {
143
+ const models = await llm.listModels(providerId)
144
+ const entries = (Array.isArray(models) ? models : []).map((item) => ({
145
+ id: String(item?.id || '').trim(),
146
+ name: String(item?.name || item?.id || '').trim()
147
+ })).filter((item) => item.id)
148
+ return entries.length ? {
149
+ id: providerId,
150
+ name: String(provider?.name || providerId).trim(),
151
+ models: entries
152
+ } : null
153
+ } catch {
154
+ return null
155
+ }
156
+ }))).filter(Boolean)
157
+ if (defaultModel) {
158
+ const group = groups.find((item) => item.id === defaultModel.provider)
159
+ if (!group) groups.unshift({ id: defaultModel.provider, name: defaultModel.provider, models: [{ id: defaultModel.model, name: defaultModel.model }] })
160
+ else if (!group.models.some((item) => item.id === defaultModel.model)) group.models.unshift({ id: defaultModel.model, name: defaultModel.model })
161
+ }
162
+ return { default: defaultModel, groups }
163
+ }
164
+
112
165
  async function sessionsFor(body) {
113
166
  const context = await contextFor(body.anchorSessionId)
114
167
  if (!context.ready) throw new Error('请先打开一个有明确工作路径的已有对话。')
@@ -122,7 +175,9 @@ export function createHost(options = {}) {
122
175
  for (const id of input.selectedSessionIds) if (!allowed.has(id)) throw new Error(`所选对话不属于当前工作路径或已不可用:${id}`)
123
176
  const state = await storage.readState(context.cwd)
124
177
  if (input.expectedRevision !== state.revision) throw new WorkspaceRevisionError(input.expectedRevision, state.revision)
125
- const payload = { ...input, anchorSessionId: context.sessionId, cwd: context.cwd, expectedRevision: state.revision }
178
+ const model = input.model || currentModelSelection(agentDefaultModel)
179
+ if (!model) throw new Error('当前没有可用的 Provider/Model,请先在模型设置中配置默认模型。')
180
+ const payload = { ...input, model, anchorSessionId: context.sessionId, cwd: context.cwd, expectedRevision: state.revision }
126
181
  const token = randomUUID()
127
182
  confirmations.set(token, { payload, expiresAt: Date.now() + CONFIRMATION_TTL, used: false })
128
183
  return {
@@ -133,6 +188,7 @@ export function createHost(options = {}) {
133
188
  selectedSessions: available.filter((item) => input.selectedSessionIds.includes(item.id)),
134
189
  outputMode: input.outputMode,
135
190
  strict: input.strict,
191
+ model,
136
192
  promptSummary: shortText(input.prompt, 300),
137
193
  overwrite: state.exists
138
194
  }
@@ -141,7 +197,7 @@ export function createHost(options = {}) {
141
197
  function consumeConfirmation(token, body) {
142
198
  const value = confirmations.get(String(token || ''))
143
199
  if (!value || value.used || value.expiresAt < Date.now()) throw new Error('生成确认已过期,请返回配置重新确认。')
144
- const supplied = normalizeGenerationInput({ ...body, anchorSessionId: body.anchorSessionId || value.payload.anchorSessionId, selectedSessionIds: body.selectedSessionIds || value.payload.selectedSessionIds, outputMode: body.outputMode || value.payload.outputMode, prompt: body.prompt ?? value.payload.prompt, strict: body.strict ?? value.payload.strict, includeSubagents: body.includeSubagents ?? value.payload.includeSubagents, expectedRevision: body.expectedRevision ?? value.payload.expectedRevision })
200
+ const supplied = normalizeGenerationInput({ ...body, anchorSessionId: body.anchorSessionId || value.payload.anchorSessionId, selectedSessionIds: body.selectedSessionIds || value.payload.selectedSessionIds, outputMode: body.outputMode || value.payload.outputMode, prompt: body.prompt ?? value.payload.prompt, strict: body.strict ?? value.payload.strict, includeSubagents: body.includeSubagents ?? value.payload.includeSubagents, model: body.model ?? value.payload.model, expectedRevision: body.expectedRevision ?? value.payload.expectedRevision })
145
201
  const expected = canonicalPayload(value.payload)
146
202
  const actual = canonicalPayload({ ...supplied, cwd: value.payload.cwd })
147
203
  if (expected !== actual) throw new Error('确认内容已变化,请返回配置重新确认。')
@@ -164,6 +220,9 @@ export function createHost(options = {}) {
164
220
  const sessionId = parseUrl(req).searchParams.get('sessionId') || ''
165
221
  return jsonResponse(res, 200, { ok: true, context: await contextFor(sessionId) })
166
222
  }
223
+ if (path[0] === 'models' && method === 'GET') {
224
+ return jsonResponse(res, 200, { ok: true, catalog: await modelCatalog() })
225
+ }
167
226
  if (path[0] === 'sessions' && method === 'GET') {
168
227
  const anchorSessionId = parseUrl(req).searchParams.get('anchorSessionId') || ''
169
228
  const includeSubagents = parseUrl(req).searchParams.get('includeSubagents') === 'true'
@@ -204,7 +263,7 @@ export function createHost(options = {}) {
204
263
  if (!node) throw new Error('思维导图节点不存在。')
205
264
  const targetSessionId = safeId(body.targetSessionId || node.primarySourceSessionId || node.sourceRefs?.[0]?.sessionId, '目标 Session ID')
206
265
  if (!state.manifest?.sourceSessionIds?.includes(targetSessionId)) throw new Error('目标对话不是本次生成的来源对话。')
207
- const result = await orchestrator.formFollowUp({ cwd: context.cwd, node, targetSessionId, strict: state.manifest.strict !== false })
266
+ const result = await orchestrator.formFollowUp({ cwd: context.cwd, node, targetSessionId, strict: state.manifest.strict !== false, model: state.manifest.model })
208
267
  return jsonResponse(res, 200, { ok: true, followUp: result, context, revision: state.revision })
209
268
  }
210
269
  if (path[0] === 'navigation' && path[1] === 'confirm' && method === 'POST') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@p-dsh-market/conversation-knowledge-map",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "DSH 多对话思维导图与静态知识图谱",
5
5
  "keywords": [
6
6
  "dsh",
@@ -8,6 +8,7 @@ description: 在用户明确要求整理多个同工作路径对话、生成思
8
8
  - 只有用户明确点击确认后,才能读取所选对话正文、调用模型或写入 `.g-dsh-market-knowledge`。
9
9
  - 只使用锚点 Session 的 `header.cwd` 和用户从同一 `cwd` 选择的 Session;默认排除 `origin: subagent`。
10
10
  - 默认读取 `sessionQuery.readSurface()`,不要读取或复制完整原始 JSONL。
11
+ - 生成模型由用户在配置面板中选择;确认时固定 Provider / Model,并将其传给每一次摘要、图谱和追问调用。
11
12
  - 思维导图节点必须包含标题、阶段性说明和来源引用;知识图谱关系必须包含证据和置信度。
12
13
  - 知识图谱是静态结果,不自动更新、不在图上编辑、不从节点发散。
13
14
  - 思维导图节点只生成后续问题;导航确认后不自动发送。