@p-dsh-market/conversation-knowledge-map 0.1.6 → 0.1.8
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/lib/client.js +11 -3
- package/lib/generation-orchestrator.js +44 -21
- package/lib/session-source.js +51 -15
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -268,9 +268,17 @@ window.__ModuleLoader__.load({
|
|
|
268
268
|
|
|
269
269
|
function GenerationStrip(props) {
|
|
270
270
|
var generation = props.store.generation
|
|
271
|
+
var progress = generation.progress || { percent: 0, current: 0, total: 0, label: '' }
|
|
272
|
+
var percent = Math.max(0, Math.min(100, Number(progress.percent) || 0))
|
|
271
273
|
return React.createElement('div', { className: 'ckm-generation-strip', 'data-status': generation.status }, [
|
|
272
|
-
React.createElement('
|
|
273
|
-
|
|
274
|
+
React.createElement('div', { key: 'body', className: 'ckm-generation-body' }, [
|
|
275
|
+
React.createElement('div', { key: 'status-row', className: 'ckm-generation-status-row' }, [
|
|
276
|
+
React.createElement('strong', { key: 'status' }, generation.message || generation.status),
|
|
277
|
+
React.createElement('span', { key: 'detail' }, progress.total ? (progress.current + '/' + progress.total + ' 个对话 · ' + progress.label) : progress.label)
|
|
278
|
+
]),
|
|
279
|
+
React.createElement('div', { key: 'progress', className: 'ckm-progress-track', role: 'progressbar', 'aria-valuemin': 0, 'aria-valuemax': 100, 'aria-valuenow': percent, 'aria-label': '知识视图生成进度' }, React.createElement('span', { className: 'ckm-progress-value', style: { width: percent + '%' } }))
|
|
280
|
+
]),
|
|
281
|
+
generation.error ? React.createElement('span', { key: 'error', className: 'ckm-generation-error' }, generation.error) : null,
|
|
274
282
|
['failed', 'cancelled'].includes(generation.status) ? null : Button({ key: 'cancel', className: 'ckm-danger', onClick: function () { request('/generations/' + encodeURIComponent(generation.id) + '/cancel', { method: 'POST' }).then(function (body) { applyGeneration(props.store, body.generation) }).catch(function (error) { props.store.error = error.message; notifyStore(props.store) }) } }, '取消')
|
|
275
283
|
])
|
|
276
284
|
}
|
|
@@ -563,7 +571,7 @@ window.__ModuleLoader__.load({
|
|
|
563
571
|
'.ckm-page-header h2{margin:0;font-size:16px}.ckm-page-header p{margin:3px 0 0;color:var(--dsw-alias-label-secondary);font-size:11px;word-break:break-all}' +
|
|
564
572
|
'.ckm-page-actions,.ckm-modal-actions{display:flex;align-items:center;gap:8px}.ckm-tab,.ckm-secondary,.ckm-primary,.ckm-danger,.ckm-header-action{border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:7px 11px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer;font:inherit;font-size:12px}.ckm-tab[data-active=true],.ckm-primary{border-color:var(--dsw-alias-button-info-fill);background:var(--dsw-alias-button-info-fill);color:var(--dsw-alias-label-primary-foreground)}.ckm-tab:hover,.ckm-secondary:hover,.ckm-header-action:hover{background:var(--dsw-alias-interactive-bg-hover)}.ckm-primary:hover{background:var(--dsw-alias-button-info-hover)}.ckm-danger{border-color:#d36b6b;color:#ffb7b7}.ckm-danger:hover{background:#d94a4a22}.ckm-page button:disabled,.ckm-modal button:disabled{opacity:.5;cursor:not-allowed}' +
|
|
565
573
|
'.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}' +
|
|
566
|
-
'.ckm-generation-strip{display:flex;align-items:center;gap:12px;padding:
|
|
574
|
+
'.ckm-generation-strip{display:flex;align-items:center;gap:12px;padding:10px 20px;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-state-business-tertiary);font-size:12px}.ckm-generation-body{display:grid;gap:7px;flex:1;min-width:0}.ckm-generation-status-row{display:flex;justify-content:space-between;gap:12px}.ckm-generation-status-row span{color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ckm-progress-track{height:6px;overflow:hidden;border-radius:999px;background:var(--dsw-alias-bg-layer-1)}.ckm-progress-value{display:block;height:100%;border-radius:inherit;background:var(--dsw-alias-state-business-primary);transition:width .25s ease}.ckm-generation-error{color:#ff9898;max-width:38%;overflow-wrap:anywhere}.ckm-generation-strip .ckm-danger{margin-left:auto}' +
|
|
567
575
|
'.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}' +
|
|
568
576
|
'.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}' +
|
|
569
577
|
'.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)}' +
|
|
@@ -178,6 +178,7 @@ function outputPrompt(kind, summaries, prompt, strict) {
|
|
|
178
178
|
if (kind === 'mind-map') {
|
|
179
179
|
return [
|
|
180
180
|
'请根据以下多个对话摘要生成阶段性思维导图。节点不是关键词,narrative 必须是至少一段完整说明,包含背景、当前认识和下一步/未决点。',
|
|
181
|
+
'最多生成 12 个节点;每个 narrative 控制在 300 字以内。不要输出 schema 之外的字段。确保最终内容是可被 JSON.parse 直接解析的完整 JSON。',
|
|
181
182
|
'JSON 形状:{"rootId":"...","nodes":[{"id":"...","parentId":null,"type":"theme|stage|question|decision|solution|risk|conclusion","title":"...","narrative":"...","primarySourceSessionId":"...","sourceRefs":[{"sessionId":"...","eventSeqs":[1]}],"openQuestions":[]}]}。',
|
|
182
183
|
...rules,
|
|
183
184
|
`额外要求:${shortText(prompt, 2000) || '没有额外要求。'}`,
|
|
@@ -397,6 +398,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
397
398
|
createdAt: task.createdAt,
|
|
398
399
|
updatedAt: task.updatedAt,
|
|
399
400
|
revision: task.revision || 0,
|
|
401
|
+
progress: task.progress ? clone(task.progress) : null,
|
|
400
402
|
result: task.result ? clone(task.result) : null,
|
|
401
403
|
events: task.events.slice(-40)
|
|
402
404
|
}
|
|
@@ -437,6 +439,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
437
439
|
message: phaseMessage('created'),
|
|
438
440
|
error: '',
|
|
439
441
|
result: null,
|
|
442
|
+
progress: { percent: 0, current: 0, total: Array.isArray(request.selectedSessionIds) ? request.selectedSessionIds.length : 0, label: '准备生成' },
|
|
440
443
|
revision: 0,
|
|
441
444
|
createdAt: this.now(),
|
|
442
445
|
updatedAt: this.now(),
|
|
@@ -470,17 +473,24 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
470
473
|
|
|
471
474
|
async run(task, request) {
|
|
472
475
|
try {
|
|
473
|
-
this.update(task, 'reading-sources')
|
|
476
|
+
this.update(task, 'reading-sources', { progress: { percent: 5, current: 0, total: request.selectedSessionIds.length, label: '读取对话记录' } })
|
|
474
477
|
const sources = await this.sourceReader({ sessionQuery: this.sessionQuery, sessions: this.sessions }, {
|
|
475
478
|
cwd: request.cwd,
|
|
476
479
|
sessionIds: request.selectedSessionIds,
|
|
480
|
+
fallbackSessionId: request.anchorSessionId,
|
|
477
481
|
includeSubagents: request.includeSubagents === true
|
|
478
482
|
})
|
|
483
|
+
const sourceSessionIds = [...new Set(sources.map((source) => source.sessionId))]
|
|
479
484
|
this.assertNotCancelled(task)
|
|
480
485
|
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))
|
|
481
|
-
this.update(task, 'summarizing', { sourceCount: sources.length })
|
|
486
|
+
this.update(task, 'summarizing', { sourceCount: sources.length, progress: { percent: 10, current: 0, total: sources.length, label: '准备整理对话' } })
|
|
482
487
|
const summaries = []
|
|
483
|
-
for (
|
|
488
|
+
for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex += 1) {
|
|
489
|
+
const source = sources[sourceIndex]
|
|
490
|
+
this.update(task, 'summarizing', {
|
|
491
|
+
sourceCount: sources.length,
|
|
492
|
+
progress: { percent: 10 + Math.floor(60 * sourceIndex / sources.length), current: sourceIndex, total: sources.length, label: source.title }
|
|
493
|
+
})
|
|
484
494
|
for (const chunk of chunkSourceText(source)) {
|
|
485
495
|
this.assertNotCancelled(task)
|
|
486
496
|
const value = await this.runModel({
|
|
@@ -488,51 +498,64 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
488
498
|
prompt: summaryPrompt(source, chunk, request.strict === true),
|
|
489
499
|
cwd: request.cwd,
|
|
490
500
|
strict: request.strict === true,
|
|
491
|
-
selectedSessionIds:
|
|
501
|
+
selectedSessionIds: sourceSessionIds,
|
|
492
502
|
model: request.model,
|
|
493
503
|
signal: task.controller.signal
|
|
494
504
|
})
|
|
495
505
|
summaries.push(normalizeSummary(value, source, chunk))
|
|
496
506
|
}
|
|
507
|
+
this.update(task, 'summarizing', {
|
|
508
|
+
sourceCount: sources.length,
|
|
509
|
+
progress: { percent: 10 + Math.floor(60 * (sourceIndex + 1) / sources.length), current: sourceIndex + 1, total: sources.length, label: source.title }
|
|
510
|
+
})
|
|
497
511
|
}
|
|
498
512
|
this.assertNotCancelled(task)
|
|
499
513
|
let mindMap = null
|
|
500
514
|
let knowledgeGraph = null
|
|
501
515
|
if (request.outputMode === 'mind-map' || request.outputMode === 'both') {
|
|
502
|
-
this.update(task, 'building-mind-map')
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
strict: request.strict === true,
|
|
508
|
-
selectedSessionIds: request.selectedSessionIds,
|
|
509
|
-
model: request.model,
|
|
510
|
-
signal: task.controller.signal
|
|
516
|
+
this.update(task, 'building-mind-map', { progress: { percent: 78, current: sources.length, total: sources.length, label: '生成思维导图' } })
|
|
517
|
+
const mindMapPrompt = outputPrompt('mind-map', summaries, request.prompt, request.strict === true)
|
|
518
|
+
const generateMindMap = async (prompt) => this.runModel({
|
|
519
|
+
kind: 'mind-map', prompt, cwd: request.cwd, strict: request.strict === true,
|
|
520
|
+
selectedSessionIds: sourceSessionIds, model: request.model, signal: task.controller.signal
|
|
511
521
|
})
|
|
512
|
-
|
|
522
|
+
try {
|
|
523
|
+
const value = await generateMindMap(mindMapPrompt)
|
|
524
|
+
mindMap = validateMindMap(value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
525
|
+
} catch (error) {
|
|
526
|
+
this.assertNotCancelled(task)
|
|
527
|
+
logMessage(this.logger, 'warn', 'mind map first attempt invalid id=%s error=%s', logId(task.id), errorMessage(error))
|
|
528
|
+
this.update(task, 'building-mind-map', {
|
|
529
|
+
message: '首次结构无效,正在自动修复思维导图…',
|
|
530
|
+
progress: { percent: 82, current: sources.length, total: sources.length, label: '自动修复结构' }
|
|
531
|
+
})
|
|
532
|
+
const retryPrompt = `${mindMapPrompt}\n\n上一次输出未通过校验:${shortText(errorMessage(error), 500)}。请从头重新生成,不要复用上一次文本;只输出完整、严格合法的 JSON。`
|
|
533
|
+
const retryValue = await generateMindMap(retryPrompt)
|
|
534
|
+
mindMap = validateMindMap(retryValue, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
535
|
+
}
|
|
513
536
|
}
|
|
514
537
|
if (request.outputMode === 'knowledge-graph' || request.outputMode === 'both') {
|
|
515
|
-
this.update(task, 'building-knowledge-graph')
|
|
538
|
+
this.update(task, 'building-knowledge-graph', { progress: { percent: 86, current: sources.length, total: sources.length, label: '生成知识图谱' } })
|
|
516
539
|
const value = await this.runModel({
|
|
517
540
|
kind: 'knowledge-graph',
|
|
518
541
|
prompt: outputPrompt('knowledge-graph', summaries, request.prompt, request.strict === true),
|
|
519
542
|
cwd: request.cwd,
|
|
520
543
|
strict: request.strict === true,
|
|
521
|
-
selectedSessionIds:
|
|
544
|
+
selectedSessionIds: sourceSessionIds,
|
|
522
545
|
model: request.model,
|
|
523
546
|
signal: task.controller.signal
|
|
524
547
|
})
|
|
525
|
-
knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds:
|
|
548
|
+
knowledgeGraph = validateKnowledgeGraph(value, { selectedSessionIds: sourceSessionIds, strict: request.strict === true })
|
|
526
549
|
}
|
|
527
550
|
assertOutputSourceRefs({ mindMap, knowledgeGraph }, sources)
|
|
528
551
|
this.assertNotCancelled(task)
|
|
529
|
-
this.update(task, 'validating')
|
|
530
|
-
this.update(task, 'saving')
|
|
552
|
+
this.update(task, 'validating', { progress: { percent: 92, current: sources.length, total: sources.length, label: '校验结构和来源' } })
|
|
553
|
+
this.update(task, 'saving', { progress: { percent: 97, current: sources.length, total: sources.length, label: '保存知识视图' } })
|
|
531
554
|
const saved = await this.storage.saveBundle({
|
|
532
555
|
cwd: request.cwd,
|
|
533
556
|
expectedRevision: request.expectedRevision,
|
|
534
557
|
generationId: task.id,
|
|
535
|
-
sourceSessionIds
|
|
558
|
+
sourceSessionIds,
|
|
536
559
|
prompt: request.prompt,
|
|
537
560
|
strict: request.strict,
|
|
538
561
|
outputMode: request.outputMode,
|
|
@@ -543,7 +566,7 @@ export class KnowledgeGenerationOrchestrator {
|
|
|
543
566
|
task.revision = saved.revision
|
|
544
567
|
task.result = { revision: saved.revision, manifest: saved.manifest, mindMap, knowledgeGraph }
|
|
545
568
|
logMessage(this.logger, 'info', 'generation completed id=%s revision=%d elapsedMs=%d', logId(task.id), saved.revision, this.now() - task.createdAt)
|
|
546
|
-
this.update(task, 'completed', { revision: saved.revision, result: task.result })
|
|
569
|
+
this.update(task, 'completed', { revision: saved.revision, result: task.result, progress: { percent: 100, current: sources.length, total: sources.length, label: '生成完成' } })
|
|
547
570
|
} catch (error) {
|
|
548
571
|
if (task.controller.signal.aborted || /取消|cancel/i.test(errorMessage(error))) {
|
|
549
572
|
this.update(task, 'cancelled', { error: '' })
|
package/lib/session-source.js
CHANGED
|
@@ -112,8 +112,10 @@ function sourceSeqsOf(event) {
|
|
|
112
112
|
return Array.isArray(value) ? value.filter((item) => Number.isInteger(item) && item >= 0) : []
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
export function surfaceEventView(event) {
|
|
115
|
+
export function surfaceEventView(event, { fullSession = false } = {}) {
|
|
116
116
|
const role = eventRole(String(event?.type || ''))
|
|
117
|
+
const data = event?.data?.message || event?.data || {}
|
|
118
|
+
if (fullSession && role === 'user' && data?.source?.kind !== 'user') return null
|
|
117
119
|
const text = shortText(eventMessageText(event), 12000)
|
|
118
120
|
if (!role || !text) return null
|
|
119
121
|
return {
|
|
@@ -125,42 +127,76 @@ export function surfaceEventView(event) {
|
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
|
|
128
|
-
export async function readSelectedSurfaces({ sessionQuery, sessions }, { cwd, sessionIds, includeSubagents = false }) {
|
|
130
|
+
export async function readSelectedSurfaces({ sessionQuery, sessions }, { cwd, sessionIds, fallbackSessionId = '', includeSubagents = false }) {
|
|
129
131
|
const selected = [...new Set((sessionIds || []).map((id) => String(id || '').trim()).filter(Boolean))]
|
|
130
132
|
if (selected.length === 0) throw new Error('至少选择一个对话。')
|
|
131
133
|
const normalizedCwd = normalizeWorkspacePath(cwd)
|
|
132
134
|
if (!normalizedCwd) throw new Error('当前工作路径无效。')
|
|
135
|
+
const fallbackId = String(fallbackSessionId || '').trim()
|
|
136
|
+
const resolvedIds = [...new Set([...selected, ...(fallbackId ? [fallbackId] : [])])]
|
|
133
137
|
const records = []
|
|
134
138
|
if (typeof sessionQuery?.filterSessions === 'function') {
|
|
135
|
-
records.push(...await sessionQuery.filterSessions([{ kind: 'id', values:
|
|
139
|
+
records.push(...await sessionQuery.filterSessions([{ kind: 'id', values: resolvedIds }]))
|
|
136
140
|
} else if (typeof sessionQuery?.listSessions === 'function') {
|
|
137
141
|
const all = await sessionQuery.listSessions()
|
|
138
|
-
records.push(...all.filter((record) =>
|
|
142
|
+
records.push(...all.filter((record) => resolvedIds.includes(String(headerOf(record)?.id || ''))))
|
|
139
143
|
}
|
|
140
144
|
const recordMap = new Map(records.map((record) => [String(headerOf(record)?.id || record?.id || ''), record]))
|
|
141
|
-
const
|
|
142
|
-
for (const id of selected) {
|
|
145
|
+
const readSource = async (id) => {
|
|
143
146
|
const record = recordMap.get(id)
|
|
144
147
|
const header = headerOf(record) || sessions?.get?.(id)?.header
|
|
145
148
|
if (!header?.id) throw new Error(`所选对话不存在或已不可读:${id}`)
|
|
146
149
|
if (!sameWorkspacePath(header.cwd, normalizedCwd)) throw new Error(`所选对话不属于当前工作路径:${id}`)
|
|
147
150
|
if (!includeSubagents && header.origin === 'subagent') throw new Error(`不能选择子 Agent 对话:${id}`)
|
|
148
|
-
if (typeof sessionQuery?.readSurface !== 'function'
|
|
149
|
-
|
|
151
|
+
if (typeof sessionQuery?.readSurface !== 'function' && typeof sessionQuery?.readSession !== 'function') {
|
|
152
|
+
throw new Error('当前 DSH Runtime 未提供 sessionQuery.readSurface/readSession。')
|
|
153
|
+
}
|
|
154
|
+
let snapshot
|
|
155
|
+
let events = []
|
|
156
|
+
let surfaceError = null
|
|
150
157
|
try {
|
|
151
|
-
|
|
158
|
+
if (typeof sessionQuery?.readSurface === 'function') {
|
|
159
|
+
snapshot = await sessionQuery.readSurface(id)
|
|
160
|
+
events = (snapshot?.events || []).map((event) => surfaceEventView(event)).filter(Boolean)
|
|
161
|
+
}
|
|
152
162
|
} catch (error) {
|
|
153
|
-
|
|
163
|
+
surfaceError = error
|
|
154
164
|
}
|
|
155
|
-
|
|
156
|
-
|
|
165
|
+
if (events.length === 0 && typeof sessionQuery?.readSession === 'function') {
|
|
166
|
+
try {
|
|
167
|
+
const full = await sessionQuery.readSession(id)
|
|
168
|
+
snapshot = full || snapshot
|
|
169
|
+
events = (full?.events || []).map((event) => surfaceEventView(event, { fullSession: true })).filter(Boolean)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (surfaceError) throw new Error(`读取对话“${id}”失败:surface=${errorMessage(surfaceError)};session=${errorMessage(error)}`)
|
|
172
|
+
throw new Error(`读取对话“${id}”完整记录失败:${errorMessage(error)}`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (surfaceError && events.length === 0) throw new Error(`读取对话“${id}”失败:${errorMessage(surfaceError)}`)
|
|
176
|
+
return {
|
|
157
177
|
sessionId: id,
|
|
158
178
|
title: `对话 ${id.slice(0, 8)}`,
|
|
159
|
-
cwd: normalizeWorkspacePath(
|
|
160
|
-
capturedThroughSeq: Number.isInteger(
|
|
179
|
+
cwd: normalizeWorkspacePath(snapshot?.session?.cwd || header.cwd),
|
|
180
|
+
capturedThroughSeq: Number.isInteger(snapshot?.capturedThroughSeq) ? snapshot.capturedThroughSeq : null,
|
|
161
181
|
events,
|
|
162
182
|
text: events.map((event) => `${event.role === 'user' ? '用户' : '助手'}:${event.text}`).join('\n\n')
|
|
163
|
-
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const sources = []
|
|
186
|
+
let needsFallback = false
|
|
187
|
+
for (const id of selected) {
|
|
188
|
+
const source = await readSource(id)
|
|
189
|
+
if (source.events.length > 0) sources.push(source)
|
|
190
|
+
else needsFallback = true
|
|
191
|
+
}
|
|
192
|
+
if (needsFallback && fallbackId && !sources.some((source) => source.sessionId === fallbackId)) {
|
|
193
|
+
const fallback = await readSource(fallbackId)
|
|
194
|
+
if (fallback.events.length > 0) sources.push(fallback)
|
|
195
|
+
}
|
|
196
|
+
if (sources.length === 0) {
|
|
197
|
+
throw new Error(fallbackId
|
|
198
|
+
? `所选对话及当前对话都没有可读取的用户/助手消息:${selected.join(', ')}`
|
|
199
|
+
: `所选对话没有可读取的用户/助手消息:${selected.join(', ')}`)
|
|
164
200
|
}
|
|
165
201
|
if (typeof sessionQuery?.readTitleSnapshots === 'function' && sources.length) {
|
|
166
202
|
const results = await sessionQuery.readTitleSnapshots(sources.map((source) => source.sessionId))
|