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

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,
@@ -278,6 +375,7 @@ export class KnowledgeGenerationOrchestrator {
278
375
  cwd: request.cwd,
279
376
  strict: request.strict === true,
280
377
  selectedSessionIds: request.selectedSessionIds,
378
+ model: request.model,
281
379
  signal: task.controller.signal
282
380
  })
283
381
  summaries.push(normalizeSummary(value, source, chunk))
@@ -294,6 +392,7 @@ export class KnowledgeGenerationOrchestrator {
294
392
  cwd: request.cwd,
295
393
  strict: request.strict === true,
296
394
  selectedSessionIds: request.selectedSessionIds,
395
+ model: request.model,
297
396
  signal: task.controller.signal
298
397
  })
299
398
  mindMap = validateMindMap(value, { selectedSessionIds: request.selectedSessionIds, strict: request.strict === true })
@@ -306,6 +405,7 @@ export class KnowledgeGenerationOrchestrator {
306
405
  cwd: request.cwd,
307
406
  strict: request.strict === true,
308
407
  selectedSessionIds: request.selectedSessionIds,
408
+ model: request.model,
309
409
  signal: task.controller.signal
310
410
  })
311
411
  knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds: request.selectedSessionIds, strict: request.strict === true })
@@ -342,10 +442,12 @@ export class KnowledgeGenerationOrchestrator {
342
442
  async runModel(input) {
343
443
  if (typeof this.modelRunner === 'function') return parseStructuredOutput(await this.modelRunner(input))
344
444
  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()
445
+ let selection = normalizeModelSelection(input.model)
446
+ if (!selection) {
447
+ try { selection = normalizeModelSelection(this.agentDefaultModel?.currentSelection?.()) } catch { selection = null }
448
+ }
449
+ const provider = String(selection?.provider || '').trim()
450
+ const model = String(selection?.model || '').trim()
349
451
  if (!provider || !model) throw new Error('当前没有可用的默认 Provider/Model。')
350
452
  const sessionId = `knowledge-map-${this.idFactory()}`
351
453
  const handle = await this.agents.create({
@@ -367,14 +469,13 @@ export class KnowledgeGenerationOrchestrator {
367
469
  try {
368
470
  handle.agent.followup(makeUserMessage(input.prompt, `${sessionId}-${input.kind}`))
369
471
  await handle.agent.whenIdle()
370
- const surface = await this.sessionQuery?.readSurface?.(sessionId)
371
- return parseStructuredOutput(extractAgentText(surface))
472
+ return parseStructuredOutput(await readAgentText(this.sessionQuery, sessionId, input.signal))
372
473
  } finally {
373
474
  await handle.dispose?.()
374
475
  }
375
476
  }
376
477
 
377
- async formFollowUp({ cwd, node, targetSessionId, strict = true, signal }) {
478
+ async formFollowUp({ cwd, node, targetSessionId, strict = true, model, signal }) {
378
479
  const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
379
480
  cwd,
380
481
  sessionIds: [targetSessionId],
@@ -386,6 +487,7 @@ export class KnowledgeGenerationOrchestrator {
386
487
  cwd,
387
488
  strict,
388
489
  selectedSessionIds: [targetSessionId],
490
+ model,
389
491
  signal
390
492
  })
391
493
  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,13 +100,14 @@ 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)
@@ -109,6 +131,35 @@ export function createHost(options = {}) {
109
131
  return { ready: true, state: 'ready', sessionId: String(header.id), cwd, origin: String(header.origin || '') }
110
132
  }
111
133
 
134
+ async function modelCatalog() {
135
+ const defaultModel = currentModelSelection(agentDefaultModel)
136
+ const providers = typeof llm?.listProviders === 'function' ? llm.listProviders() : []
137
+ const groups = (await Promise.all(providers.map(async (provider) => {
138
+ const providerId = String(provider?.id || '').trim()
139
+ if (!providerId || typeof llm?.listModels !== 'function') return null
140
+ try {
141
+ const models = await llm.listModels(providerId)
142
+ const entries = (Array.isArray(models) ? models : []).map((item) => ({
143
+ id: String(item?.id || '').trim(),
144
+ name: String(item?.name || item?.id || '').trim()
145
+ })).filter((item) => item.id)
146
+ return entries.length ? {
147
+ id: providerId,
148
+ name: String(provider?.name || providerId).trim(),
149
+ models: entries
150
+ } : null
151
+ } catch {
152
+ return null
153
+ }
154
+ }))).filter(Boolean)
155
+ if (defaultModel) {
156
+ const group = groups.find((item) => item.id === defaultModel.provider)
157
+ if (!group) groups.unshift({ id: defaultModel.provider, name: defaultModel.provider, models: [{ id: defaultModel.model, name: defaultModel.model }] })
158
+ else if (!group.models.some((item) => item.id === defaultModel.model)) group.models.unshift({ id: defaultModel.model, name: defaultModel.model })
159
+ }
160
+ return { default: defaultModel, groups }
161
+ }
162
+
112
163
  async function sessionsFor(body) {
113
164
  const context = await contextFor(body.anchorSessionId)
114
165
  if (!context.ready) throw new Error('请先打开一个有明确工作路径的已有对话。')
@@ -122,7 +173,9 @@ export function createHost(options = {}) {
122
173
  for (const id of input.selectedSessionIds) if (!allowed.has(id)) throw new Error(`所选对话不属于当前工作路径或已不可用:${id}`)
123
174
  const state = await storage.readState(context.cwd)
124
175
  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 }
176
+ const model = input.model || currentModelSelection(agentDefaultModel)
177
+ if (!model) throw new Error('当前没有可用的 Provider/Model,请先在模型设置中配置默认模型。')
178
+ const payload = { ...input, model, anchorSessionId: context.sessionId, cwd: context.cwd, expectedRevision: state.revision }
126
179
  const token = randomUUID()
127
180
  confirmations.set(token, { payload, expiresAt: Date.now() + CONFIRMATION_TTL, used: false })
128
181
  return {
@@ -133,6 +186,7 @@ export function createHost(options = {}) {
133
186
  selectedSessions: available.filter((item) => input.selectedSessionIds.includes(item.id)),
134
187
  outputMode: input.outputMode,
135
188
  strict: input.strict,
189
+ model,
136
190
  promptSummary: shortText(input.prompt, 300),
137
191
  overwrite: state.exists
138
192
  }
@@ -141,7 +195,7 @@ export function createHost(options = {}) {
141
195
  function consumeConfirmation(token, body) {
142
196
  const value = confirmations.get(String(token || ''))
143
197
  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 })
198
+ 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
199
  const expected = canonicalPayload(value.payload)
146
200
  const actual = canonicalPayload({ ...supplied, cwd: value.payload.cwd })
147
201
  if (expected !== actual) throw new Error('确认内容已变化,请返回配置重新确认。')
@@ -164,6 +218,9 @@ export function createHost(options = {}) {
164
218
  const sessionId = parseUrl(req).searchParams.get('sessionId') || ''
165
219
  return jsonResponse(res, 200, { ok: true, context: await contextFor(sessionId) })
166
220
  }
221
+ if (path[0] === 'models' && method === 'GET') {
222
+ return jsonResponse(res, 200, { ok: true, catalog: await modelCatalog() })
223
+ }
167
224
  if (path[0] === 'sessions' && method === 'GET') {
168
225
  const anchorSessionId = parseUrl(req).searchParams.get('anchorSessionId') || ''
169
226
  const includeSubagents = parseUrl(req).searchParams.get('includeSubagents') === 'true'
@@ -204,7 +261,7 @@ export function createHost(options = {}) {
204
261
  if (!node) throw new Error('思维导图节点不存在。')
205
262
  const targetSessionId = safeId(body.targetSessionId || node.primarySourceSessionId || node.sourceRefs?.[0]?.sessionId, '目标 Session ID')
206
263
  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 })
264
+ const result = await orchestrator.formFollowUp({ cwd: context.cwd, node, targetSessionId, strict: state.manifest.strict !== false, model: state.manifest.model })
208
265
  return jsonResponse(res, 200, { ok: true, followUp: result, context, revision: state.revision })
209
266
  }
210
267
  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.1",
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
  - 思维导图节点只生成后续问题;导航确认后不自动发送。