Version: 0.9.55.dev.260429
后端: 1. analyze_health 候选复诊改为轻量 after brief 口径,候选模拟执行后只展示基础诊断结论,不再递归触发全局候选枚举,避免 score-only 评估阶段重复扫描和误判继续优化。 前端: 2. thinking_summary 支持同一条回复内多轮思考块——按 backendKey 维护独立阶段状态,final 后再次收到摘要会新建 reasoning block,避免多轮思考被合并、去重状态互相吞掉。 3. timeline 历史恢复改为复用后端事件 seq,正文、工具、状态、思考块都按原始顺序回放,减少刷新后消息块错位、插队和布局跳变。 4. 思考流式态下沉到当前活跃 reasoning block,只让正在输出的思考块闪光、显示游标和接收逐字追加,旧思考块完成后稳定折叠,不再被新一轮思考“复活”。 5. 清理助手消息重置逻辑,补齐 reasoning block、短摘要、耗时、折叠态和流式队列的状态回收,降低连续会话/重发时的残留干扰。
This commit is contained in:
@@ -206,6 +206,38 @@ func buildAnalyzeHealthFinalDecisionBrief(
|
|||||||
return decision
|
return decision
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildAnalyzeHealthCandidateAfterBrief 生成候选执行后的轻量复诊摘要。
|
||||||
|
//
|
||||||
|
// 职责边界:
|
||||||
|
// 1. 只负责为 candidate.after / candidate.summary 提供“执行后看起来如何”的展示结论;
|
||||||
|
// 2. 不负责再次枚举 move/swap 候选,也不决定顶层 analyze_health 是否继续优化;
|
||||||
|
// 3. 输入是已经模拟执行后的 state/snapshot,输出沿用 analyzeHealthDecisionBase 的字段语义。
|
||||||
|
func buildAnalyzeHealthCandidateAfterBrief(
|
||||||
|
state *ScheduleState,
|
||||||
|
snapshot analyzeHealthSnapshot,
|
||||||
|
) analyzeHealthDecisionBase {
|
||||||
|
base := buildAnalyzeHealthDecisionBase(state, snapshot)
|
||||||
|
decision := analyzeHealthDecisionBase{
|
||||||
|
ShouldContinueOptimize: base.ShouldContinueOptimize,
|
||||||
|
PrimaryProblem: base.PrimaryProblem,
|
||||||
|
ProblemScope: base.ProblemScope,
|
||||||
|
IsForcedImperfection: base.IsForcedImperfection,
|
||||||
|
RecommendedOperation: base.RecommendedOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
if !shouldEnterHealthCandidateLoop(base) {
|
||||||
|
decision.ShouldContinueOptimize = false
|
||||||
|
return decision
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. candidate.after 位于候选模拟内层,不能再跑全局候选枚举。
|
||||||
|
// 2. 顶层 buildAnalyzeHealthDecisionV2 已经负责严谨筛选“是否值得继续优化”;
|
||||||
|
// 这里保留基础诊断即可,避免每个候选递归触发 score-only 全局扫描。
|
||||||
|
// 3. 若仍存在基础诊断认为可优化的问题,则如实展示给前端和 LLM;下一轮会再次调用
|
||||||
|
// analyze_health 做正式复诊,作为真正的收口依据。
|
||||||
|
return decision
|
||||||
|
}
|
||||||
|
|
||||||
// pickPrimaryHealthProblem 选择当前最值得处理的局部问题。
|
// pickPrimaryHealthProblem 选择当前最值得处理的局部问题。
|
||||||
func pickPrimaryHealthProblem(state *ScheduleState, snapshot analyzeHealthSnapshot) (analyzeHealthProblem, bool) {
|
func pickPrimaryHealthProblem(state *ScheduleState, snapshot analyzeHealthSnapshot) (analyzeHealthProblem, bool) {
|
||||||
best := analyzeHealthProblem{}
|
best := analyzeHealthProblem{}
|
||||||
@@ -868,7 +900,7 @@ func evaluateHealthCandidateOutcome(
|
|||||||
operation string,
|
operation string,
|
||||||
moveCost int,
|
moveCost int,
|
||||||
) (string, int, analyzeHealthDecisionBase, bool) {
|
) (string, int, analyzeHealthDecisionBase, bool) {
|
||||||
afterDecision := buildAnalyzeHealthFinalDecisionBrief(afterState, after)
|
afterDecision := buildAnalyzeHealthCandidateAfterBrief(afterState, after)
|
||||||
effect, score, ok := evaluateHealthCandidateScoreOnly(
|
effect, score, ok := evaluateHealthCandidateScoreOnly(
|
||||||
baseline,
|
baseline,
|
||||||
after,
|
after,
|
||||||
|
|||||||
@@ -187,6 +187,13 @@ interface AssistantContentBlock {
|
|||||||
text: string
|
text: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ThinkingSummaryBlockState {
|
||||||
|
blockId: string
|
||||||
|
lastSummarySeq: number
|
||||||
|
lastSignature: string
|
||||||
|
finished: boolean
|
||||||
|
}
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
initialHistoryWidth?: number
|
initialHistoryWidth?: number
|
||||||
@@ -258,6 +265,7 @@ const reasoningCollapsedMap = reactive<Record<string, boolean>>({})
|
|||||||
const reasoningStartedAtMap = reactive<Record<string, number>>({})
|
const reasoningStartedAtMap = reactive<Record<string, number>>({})
|
||||||
const reasoningCurrentShortSummaryMap = reactive<Record<string, string>>({})
|
const reasoningCurrentShortSummaryMap = reactive<Record<string, string>>({})
|
||||||
const reasoningDurationMap = reactive<Record<string, number>>({})
|
const reasoningDurationMap = reactive<Record<string, number>>({})
|
||||||
|
const activeReasoningBlockIdMap = reactive<Record<string, string>>({})
|
||||||
const confirmOnlyStreamMap = reactive<Record<string, boolean>>({})
|
const confirmOnlyStreamMap = reactive<Record<string, boolean>>({})
|
||||||
const confirmVisiblePrefixMap = reactive<Record<string, boolean>>({})
|
const confirmVisiblePrefixMap = reactive<Record<string, boolean>>({})
|
||||||
const toolTraceEventsMap = reactive<Record<string, ToolTraceEvent[]>>({})
|
const toolTraceEventsMap = reactive<Record<string, ToolTraceEvent[]>>({})
|
||||||
@@ -275,10 +283,8 @@ const scheduleResultMap = reactive<Record<string, SchedulePreviewData>>({})
|
|||||||
const scheduleResultSeqMap = reactive<Record<string, number>>({})
|
const scheduleResultSeqMap = reactive<Record<string, number>>({})
|
||||||
const businessCardEventsMap = reactive<Record<string, TimelineBusinessCardPayload[]>>({})
|
const businessCardEventsMap = reactive<Record<string, TimelineBusinessCardPayload[]>>({})
|
||||||
|
|
||||||
// thinking_summary 协议:每条 message 唯一一个 reasoning blockId
|
// thinking_summary 协议:同一条 message 可能发生多轮思考,每轮都需要独立 reasoning block。
|
||||||
const messageThinkingBlockIdMap = reactive<Record<string, string>>({})
|
const thinkingSummaryBlockStateMap = reactive<Record<string, Record<string, ThinkingSummaryBlockState>>>({})
|
||||||
// summary_seq 按 messageId + backendKey(block_id|stage|'thinking') 维度去重
|
|
||||||
const thinkingSummarySeqMap = reactive<Record<string, Record<string, number>>>({})
|
|
||||||
|
|
||||||
// 长摘要逐字流式状态:用普通 Map 存储,避免每字符触发 reactive 收集
|
// 长摘要逐字流式状态:用普通 Map 存储,避免每字符触发 reactive 收集
|
||||||
interface ThinkingStreamState { queue: string; timerId: number | null }
|
interface ThinkingStreamState { queue: string; timerId: number | null }
|
||||||
@@ -742,12 +748,13 @@ function clearToolTraceState(messageId: string) {
|
|||||||
delete statusTraceEventsMap[messageId]
|
delete statusTraceEventsMap[messageId]
|
||||||
delete assistantReasoningSeqMap[messageId]
|
delete assistantReasoningSeqMap[messageId]
|
||||||
delete assistantContentBlocksMap[messageId]
|
delete assistantContentBlocksMap[messageId]
|
||||||
|
delete assistantReasoningBlocksMap[messageId]
|
||||||
|
delete activeReasoningBlockIdMap[messageId]
|
||||||
delete assistantTimelineLastKindMap[messageId]
|
delete assistantTimelineLastKindMap[messageId]
|
||||||
delete scheduleResultMap[messageId]
|
delete scheduleResultMap[messageId]
|
||||||
delete scheduleResultSeqMap[messageId]
|
delete scheduleResultSeqMap[messageId]
|
||||||
delete businessCardEventsMap[messageId]
|
delete businessCardEventsMap[messageId]
|
||||||
delete messageThinkingBlockIdMap[messageId]
|
delete thinkingSummaryBlockStateMap[messageId]
|
||||||
delete thinkingSummarySeqMap[messageId]
|
|
||||||
for (const key of Object.keys(toolTraceExpandedMap)) {
|
for (const key of Object.keys(toolTraceExpandedMap)) {
|
||||||
if (key.startsWith(`${messageId}:tool:`)) {
|
if (key.startsWith(`${messageId}:tool:`)) {
|
||||||
delete toolTraceExpandedMap[key]
|
delete toolTraceExpandedMap[key]
|
||||||
@@ -764,6 +771,18 @@ function clearToolTraceState(messageId: string) {
|
|||||||
thinkingSummaryStreamMap.delete(blockId)
|
thinkingSummaryStreamMap.delete(blockId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const key of Object.keys(reasoningCollapsedMap)) {
|
||||||
|
if (key.startsWith(prefix)) delete reasoningCollapsedMap[key]
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(reasoningStartedAtMap)) {
|
||||||
|
if (key.startsWith(prefix)) delete reasoningStartedAtMap[key]
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(reasoningCurrentShortSummaryMap)) {
|
||||||
|
if (key.startsWith(prefix)) delete reasoningCurrentShortSummaryMap[key]
|
||||||
|
}
|
||||||
|
for (const key of Object.keys(reasoningDurationMap)) {
|
||||||
|
if (key.startsWith(prefix)) delete reasoningDurationMap[key]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendToolTraceEvent(
|
function appendToolTraceEvent(
|
||||||
@@ -774,6 +793,7 @@ function appendToolTraceEvent(
|
|||||||
toolName = '',
|
toolName = '',
|
||||||
argumentView?: ToolView,
|
argumentView?: ToolView,
|
||||||
resultView?: ToolView,
|
resultView?: ToolView,
|
||||||
|
seq?: number,
|
||||||
) {
|
) {
|
||||||
const normalizedSummary = summary.trim()
|
const normalizedSummary = summary.trim()
|
||||||
if (!normalizedSummary) {
|
if (!normalizedSummary) {
|
||||||
@@ -800,7 +820,7 @@ function appendToolTraceEvent(
|
|||||||
if (resultView) matchedPendingEvent.resultView = resultView
|
if (resultView) matchedPendingEvent.resultView = resultView
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const eventSeq = nextAssistantTimelineSeq()
|
const eventSeq = typeof seq === 'number' && seq > 0 ? seq : nextAssistantTimelineSeq()
|
||||||
const eventId = `${messageId}:tool:${eventSeq}`
|
const eventId = `${messageId}:tool:${eventSeq}`
|
||||||
|
|
||||||
// 如果上一个阶段是推理,则结束并折叠它
|
// 如果上一个阶段是推理,则结束并折叠它
|
||||||
@@ -898,6 +918,7 @@ function appendStatusTraceEvent(
|
|||||||
code: string,
|
code: string,
|
||||||
summary: string,
|
summary: string,
|
||||||
stage = '',
|
stage = '',
|
||||||
|
seq?: number,
|
||||||
) {
|
) {
|
||||||
const normalizedSummary = summary.trim()
|
const normalizedSummary = summary.trim()
|
||||||
if (!normalizedSummary) {
|
if (!normalizedSummary) {
|
||||||
@@ -915,7 +936,7 @@ function appendStatusTraceEvent(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventSeq = nextAssistantTimelineSeq()
|
const eventSeq = typeof seq === 'number' && seq > 0 ? seq : nextAssistantTimelineSeq()
|
||||||
// 如果上一个阶段是推理,则结束并折叠它
|
// 如果上一个阶段是推理,则结束并折叠它
|
||||||
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
||||||
finishCurrentReasoningBlock(messageId)
|
finishCurrentReasoningBlock(messageId)
|
||||||
@@ -931,7 +952,7 @@ function appendStatusTraceEvent(
|
|||||||
assistantTimelineLastKindMap[messageId] = 'status'
|
assistantTimelineLastKindMap[messageId] = 'status'
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendAssistantContentChunk(messageId: string, chunk: string) {
|
function appendAssistantContentChunk(messageId: string, chunk: string, seq?: number) {
|
||||||
if (!chunk) {
|
if (!chunk) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -949,10 +970,10 @@ function appendAssistantContentChunk(messageId: string, chunk: string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const seq = nextAssistantTimelineSeq()
|
const blockSeq = typeof seq === 'number' && seq > 0 ? seq : nextAssistantTimelineSeq()
|
||||||
blocks.push({
|
blocks.push({
|
||||||
id: `${messageId}:content:${seq}`,
|
id: `${messageId}:content:${blockSeq}`,
|
||||||
seq,
|
seq: blockSeq,
|
||||||
text: chunk,
|
text: chunk,
|
||||||
})
|
})
|
||||||
assistantTimelineLastKindMap[messageId] = 'content'
|
assistantTimelineLastKindMap[messageId] = 'content'
|
||||||
@@ -1063,64 +1084,140 @@ interface ApplyThinkingSummaryOptions {
|
|||||||
backendBlockId?: string
|
backendBlockId?: string
|
||||||
stage?: string
|
stage?: string
|
||||||
summary: ThinkingSummaryPayload
|
summary: ThinkingSummaryPayload
|
||||||
|
/** 历史回放时使用 timeline 事件自身顺序;实时流未传时按真实到达顺序分配 */
|
||||||
|
eventSeq?: number
|
||||||
/** true 表示来自 timeline 历史恢复:不写短摘要、不更新思考态、长摘要一次性写入 */
|
/** true 表示来自 timeline 历史恢复:不写短摘要、不更新思考态、长摘要一次性写入 */
|
||||||
fromHistory?: boolean
|
fromHistory?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function getThinkingBackendKey(opts: Pick<ApplyThinkingSummaryOptions, 'backendBlockId' | 'stage'>) {
|
||||||
* 把后端 thinking_summary 事件(实时或历史)落到现有 reasoning UI 上:
|
|
||||||
* - 多 block_id 合并到该消息的同一个 reasoning block(友商风格)
|
|
||||||
* - 短摘要覆盖式更新(仅实时流;历史不恢复)
|
|
||||||
* - 长摘要 append(实时走逐字流式,历史一次性写入)
|
|
||||||
* - summary_seq 按 backendKey 维度去重,乱序/重复事件丢弃
|
|
||||||
*/
|
|
||||||
function applyThinkingSummary(messageId: string, opts: ApplyThinkingSummaryOptions) {
|
|
||||||
const backendKeyRaw = (opts.backendBlockId || opts.stage || 'thinking').trim()
|
const backendKeyRaw = (opts.backendBlockId || opts.stage || 'thinking').trim()
|
||||||
const backendKey = backendKeyRaw || 'thinking'
|
return backendKeyRaw || 'thinking'
|
||||||
|
}
|
||||||
|
|
||||||
// 1. 找/建该消息唯一的 reasoning block
|
function buildThinkingSummarySignature(summary: ThinkingSummaryPayload) {
|
||||||
let frontendBlockId = messageThinkingBlockIdMap[messageId]
|
return [
|
||||||
if (!frontendBlockId) {
|
typeof summary.summary_seq === 'number' ? summary.summary_seq : '',
|
||||||
if (!assistantReasoningBlocksMap[messageId]) {
|
(summary.short_summary || '').trim(),
|
||||||
assistantReasoningBlocksMap[messageId] = []
|
(summary.detail_summary || '').trim(),
|
||||||
}
|
typeof summary.duration_seconds === 'number' ? summary.duration_seconds : '',
|
||||||
const seq = assistantReasoningSeqMap[messageId] || nextAssistantTimelineSeq()
|
summary.final === true ? '1' : '0',
|
||||||
frontendBlockId = opts.fromHistory ? `${messageId}:reasoning:${seq}` : getPendingAssistantIndicatorId(messageId)
|
].join('\u001f')
|
||||||
assistantReasoningBlocksMap[messageId].push({ id: frontendBlockId, seq, text: '' })
|
}
|
||||||
reasoningStartedAtMap[frontendBlockId] = Date.now()
|
|
||||||
reasoningCollapsedMap[frontendBlockId] = true
|
function ensureThinkingSummaryBlockBucket(messageId: string) {
|
||||||
messageThinkingBlockIdMap[messageId] = frontendBlockId
|
if (!thinkingSummaryBlockStateMap[messageId]) {
|
||||||
// 写入 seq 索引:保持 pending 小圆点与真实 reasoning 使用同一顺序,替换时不触发布局重排。
|
thinkingSummaryBlockStateMap[messageId] = {}
|
||||||
if (!assistantReasoningSeqMap[messageId]) {
|
}
|
||||||
assistantReasoningSeqMap[messageId] = seq
|
return thinkingSummaryBlockStateMap[messageId]
|
||||||
}
|
}
|
||||||
assistantTimelineLastKindMap[messageId] = 'reasoning'
|
|
||||||
|
function createThinkingReasoningBlock(messageId: string, seq: number) {
|
||||||
|
if (!assistantReasoningBlocksMap[messageId]) {
|
||||||
|
assistantReasoningBlocksMap[messageId] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. summary_seq 按 backendKey 去重
|
const frontendBlockId = `${messageId}:reasoning:${seq}`
|
||||||
if (!thinkingSummarySeqMap[messageId]) thinkingSummarySeqMap[messageId] = {}
|
assistantReasoningBlocksMap[messageId].push({ id: frontendBlockId, seq, text: '' })
|
||||||
const lastSeq = thinkingSummarySeqMap[messageId][backendKey] || 0
|
reasoningStartedAtMap[frontendBlockId] = Date.now()
|
||||||
const seqRaw = opts.summary.summary_seq
|
reasoningCollapsedMap[frontendBlockId] = true
|
||||||
const incomingSeq = typeof seqRaw === 'number' ? seqRaw : lastSeq + 1
|
activeReasoningBlockIdMap[messageId] = frontendBlockId
|
||||||
if (incomingSeq <= lastSeq) return
|
|
||||||
thinkingSummarySeqMap[messageId][backendKey] = incomingSeq
|
|
||||||
|
|
||||||
// 3. 思考态(仅实时流)
|
// 1. 仅记录该消息最早的 reasoning seq,用于旧的占位/合并判断兜底。
|
||||||
|
// 2. 真正展示顺序由每个 block 自己的 seq 决定,避免多轮思考互相挤到一起。
|
||||||
|
const currentSeq = assistantReasoningSeqMap[messageId]
|
||||||
|
if (typeof currentSeq !== 'number' || currentSeq <= 0 || seq < currentSeq) {
|
||||||
|
assistantReasoningSeqMap[messageId] = seq
|
||||||
|
}
|
||||||
|
|
||||||
|
assistantTimelineLastKindMap[messageId] = 'reasoning'
|
||||||
|
return frontendBlockId
|
||||||
|
}
|
||||||
|
|
||||||
|
function markThinkingSummaryBlockFinished(messageId: string, blockId: string) {
|
||||||
|
const bucket = thinkingSummaryBlockStateMap[messageId]
|
||||||
|
if (!bucket) return
|
||||||
|
Object.values(bucket).forEach((state) => {
|
||||||
|
if (state.blockId === blockId) {
|
||||||
|
state.finished = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把后端 thinking_summary 事件(实时或历史)落到现有 reasoning UI 上:
|
||||||
|
* - 同一 backendKey 连续摘要进入同一块;backendKey 变化、final 后续写入或 summary_seq 重启都会新建块
|
||||||
|
* - 短摘要覆盖式更新(仅实时流;历史不恢复)
|
||||||
|
* - 长摘要 append(实时走逐字流式,历史一次性写入)
|
||||||
|
* - summary_seq 只在当前思考阶段内去重,避免下一轮思考被上一轮的序号状态吞掉
|
||||||
|
*/
|
||||||
|
function applyThinkingSummary(messageId: string, opts: ApplyThinkingSummaryOptions) {
|
||||||
|
const backendKey = getThinkingBackendKey(opts)
|
||||||
|
const stateBucket = ensureThinkingSummaryBlockBucket(messageId)
|
||||||
|
const existingState = stateBucket[backendKey]
|
||||||
|
const seqRaw = opts.summary.summary_seq
|
||||||
|
const incomingSeq = typeof seqRaw === 'number' ? seqRaw : (existingState?.lastSummarySeq || 0) + 1
|
||||||
|
const signature = buildThinkingSummarySignature(opts.summary)
|
||||||
|
|
||||||
|
// 1. 同一阶段的完全重复事件直接丢弃;若序号回退但内容不同,视为后端开启了新一轮思考。
|
||||||
|
if (
|
||||||
|
existingState &&
|
||||||
|
incomingSeq <= existingState.lastSummarySeq &&
|
||||||
|
incomingSeq === existingState.lastSummarySeq &&
|
||||||
|
signature === existingState.lastSignature
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingBlock = existingState
|
||||||
|
? assistantReasoningBlocksMap[messageId]?.find((block) => block.id === existingState.blockId)
|
||||||
|
: null
|
||||||
|
const shouldStartNewBlock = !existingState ||
|
||||||
|
!existingBlock ||
|
||||||
|
existingState.finished ||
|
||||||
|
incomingSeq <= existingState.lastSummarySeq
|
||||||
|
|
||||||
|
let frontendBlockId = existingState?.blockId || ''
|
||||||
|
let currentState = existingState
|
||||||
|
|
||||||
|
if (shouldStartNewBlock) {
|
||||||
|
if (assistantTimelineLastKindMap[messageId] === 'reasoning') {
|
||||||
|
finishCurrentReasoningBlock(messageId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockSeq = typeof opts.eventSeq === 'number' && opts.eventSeq > 0
|
||||||
|
? opts.eventSeq
|
||||||
|
: nextAssistantTimelineSeq()
|
||||||
|
frontendBlockId = createThinkingReasoningBlock(messageId, blockSeq)
|
||||||
|
currentState = {
|
||||||
|
blockId: frontendBlockId,
|
||||||
|
lastSummarySeq: 0,
|
||||||
|
lastSignature: '',
|
||||||
|
finished: false,
|
||||||
|
}
|
||||||
|
stateBucket[backendKey] = currentState
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentState) return
|
||||||
|
currentState.lastSummarySeq = incomingSeq
|
||||||
|
currentState.lastSignature = signature
|
||||||
|
|
||||||
|
// 2. 思考态(仅实时流)
|
||||||
if (!opts.fromHistory) {
|
if (!opts.fromHistory) {
|
||||||
thinkingMessageMap[messageId] = opts.summary.final !== true
|
thinkingMessageMap[messageId] = opts.summary.final !== true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 短摘要覆盖(仅实时流)
|
// 3. 短摘要覆盖(仅实时流)
|
||||||
if (!opts.fromHistory) {
|
if (!opts.fromHistory) {
|
||||||
const shortText = (opts.summary.short_summary || '').trim()
|
const shortText = (opts.summary.short_summary || '').trim()
|
||||||
if (shortText) reasoningCurrentShortSummaryMap[frontendBlockId] = shortText
|
if (shortText) reasoningCurrentShortSummaryMap[frontendBlockId] = shortText
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 长摘要:实时走逐字流式队列;历史一次性写入
|
// 4. 长摘要:实时走逐字流式队列;历史一次性写入。
|
||||||
const detailText = (opts.summary.detail_summary || '').trim()
|
const detailText = (opts.summary.detail_summary || '').trim()
|
||||||
if (detailText) {
|
if (detailText) {
|
||||||
if (opts.fromHistory) {
|
if (opts.fromHistory) {
|
||||||
const block = assistantReasoningBlocksMap[messageId].find(b => b.id === frontendBlockId)
|
const block = assistantReasoningBlocksMap[messageId].find((b) => b.id === frontendBlockId)
|
||||||
if (block) {
|
if (block) {
|
||||||
block.text = block.text ? `${block.text}\n\n${detailText}` : detailText
|
block.text = block.text ? `${block.text}\n\n${detailText}` : detailText
|
||||||
}
|
}
|
||||||
@@ -1129,13 +1226,13 @@ function applyThinkingSummary(messageId: string, opts: ApplyThinkingSummaryOptio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 累计耗时
|
// 5. 累计耗时
|
||||||
if (typeof opts.summary.duration_seconds === 'number') {
|
if (typeof opts.summary.duration_seconds === 'number') {
|
||||||
reasoningDurationMap[frontendBlockId] = Math.max(1, Math.round(opts.summary.duration_seconds))
|
reasoningDurationMap[frontendBlockId] = Math.max(1, Math.round(opts.summary.duration_seconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. final 收口(仅实时流;先 flush 残留再 mark)
|
// 6. final 收口:先 flush 残留再 mark,后续同 backendKey 摘要会开启新的思考块。
|
||||||
if (!opts.fromHistory && opts.summary.final === true) {
|
if (opts.summary.final === true) {
|
||||||
flushThinkingStream(messageId, frontendBlockId)
|
flushThinkingStream(messageId, frontendBlockId)
|
||||||
markReasoningFinished(frontendBlockId, messageId)
|
markReasoningFinished(frontendBlockId, messageId)
|
||||||
}
|
}
|
||||||
@@ -1656,7 +1753,11 @@ function markReasoningFinished(blockId: string, messageId: string) {
|
|||||||
if (startedAt && !reasoningDurationMap[blockId]) {
|
if (startedAt && !reasoningDurationMap[blockId]) {
|
||||||
reasoningDurationMap[blockId] = Math.max(1, Math.round((Date.now() - startedAt) / 1000))
|
reasoningDurationMap[blockId] = Math.max(1, Math.round((Date.now() - startedAt) / 1000))
|
||||||
}
|
}
|
||||||
thinkingMessageMap[messageId] = false
|
if (activeReasoningBlockIdMap[messageId] === blockId) {
|
||||||
|
delete activeReasoningBlockIdMap[messageId]
|
||||||
|
thinkingMessageMap[messageId] = false
|
||||||
|
}
|
||||||
|
markThinkingSummaryBlockFinished(messageId, blockId)
|
||||||
|
|
||||||
// 若被展开,则思考完毕后自动闭合
|
// 若被展开,则思考完毕后自动闭合
|
||||||
if (reasoningCollapsedMap[blockId] === false) {
|
if (reasoningCollapsedMap[blockId] === false) {
|
||||||
@@ -1679,7 +1780,7 @@ function getReasoningDurationSeconds(blockId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getReasoningStatusLabel(block: DisplayAssistantBlock) {
|
function getReasoningStatusLabel(block: DisplayAssistantBlock) {
|
||||||
const isThinking = block.sourceId === activeStreamingMessageId.value && thinkingMessageMap[block.sourceId]
|
const isThinking = isReasoningBlockActive(block)
|
||||||
|
|
||||||
if (isThinking) {
|
if (isThinking) {
|
||||||
// 状态栏显示当前阶段的短摘要
|
// 状态栏显示当前阶段的短摘要
|
||||||
@@ -1690,6 +1791,22 @@ function getReasoningStatusLabel(block: DisplayAssistantBlock) {
|
|||||||
return '已完成深度思考'
|
return '已完成深度思考'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isReasoningBlockActive(block: DisplayAssistantBlock) {
|
||||||
|
const sourceId = block.sourceId || ''
|
||||||
|
if (!sourceId) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. message 级 thinking 只表示“这条回复仍在思考”,不能直接套到所有 reasoning block。
|
||||||
|
// 2. 只有当前活跃 block 才允许闪光、显示游标和继续逐字更新,避免新一轮思考把旧块复活。
|
||||||
|
return (
|
||||||
|
block.type === 'reasoning' &&
|
||||||
|
sourceId === activeStreamingMessageId.value &&
|
||||||
|
thinkingMessageMap[sourceId] === true &&
|
||||||
|
activeReasoningBlockIdMap[sourceId] === block.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 结束当前消息正在进行的推理块
|
* 结束当前消息正在进行的推理块
|
||||||
* 1. 计算耗时
|
* 1. 计算耗时
|
||||||
@@ -1699,7 +1816,8 @@ function finishCurrentReasoningBlock(messageId: string) {
|
|||||||
const blocks = assistantReasoningBlocksMap[messageId] || []
|
const blocks = assistantReasoningBlocksMap[messageId] || []
|
||||||
if (blocks.length === 0) return
|
if (blocks.length === 0) return
|
||||||
const lastBlock = blocks[blocks.length - 1]
|
const lastBlock = blocks[blocks.length - 1]
|
||||||
|
|
||||||
|
flushThinkingStream(messageId, lastBlock.id)
|
||||||
markReasoningFinished(lastBlock.id, messageId)
|
markReasoningFinished(lastBlock.id, messageId)
|
||||||
reasoningCollapsedMap[lastBlock.id] = true
|
reasoningCollapsedMap[lastBlock.id] = true
|
||||||
}
|
}
|
||||||
@@ -1870,11 +1988,16 @@ function getDisplayAssistantBlocks(dm: DisplayMessage): DisplayAssistantBlock[]
|
|||||||
|
|
||||||
const sortedBlocks = blocks.sort((left, right) => left.seq - right.seq)
|
const sortedBlocks = blocks.sort((left, right) => left.seq - right.seq)
|
||||||
|
|
||||||
// 核心修复:确保全消息流中只有一个点。
|
// 1. reasoning 的流式状态下沉到具体 block,避免新一轮思考把旧 reasoning block 一起点亮。
|
||||||
// 只有当整个 DisplayMessage 处于流式状态,且当前块是最后一块时,才标记为 isStreaming。
|
// 2. 没有活跃 reasoning 时,仍沿用“最后一个块流式输出”的正文展示逻辑。
|
||||||
if (isDisplayStreaming(dm) && sortedBlocks.length > 0) {
|
if (isDisplayStreaming(dm) && sortedBlocks.length > 0) {
|
||||||
const lastBlock = sortedBlocks[sortedBlocks.length - 1] as any
|
const activeReasoningBlock = sortedBlocks.find((block) => isReasoningBlockActive(block)) as any
|
||||||
lastBlock.isStreaming = true
|
if (activeReasoningBlock) {
|
||||||
|
activeReasoningBlock.isStreaming = true
|
||||||
|
} else {
|
||||||
|
const lastBlock = sortedBlocks[sortedBlocks.length - 1] as any
|
||||||
|
lastBlock.isStreaming = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedBlocks
|
return sortedBlocks
|
||||||
@@ -2389,7 +2512,7 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
|||||||
if (chunk) {
|
if (chunk) {
|
||||||
currentAssistantMessage.content += chunk
|
currentAssistantMessage.content += chunk
|
||||||
// 同时存入 blocks 以支持和工具交错显示
|
// 同时存入 blocks 以支持和工具交错显示
|
||||||
appendAssistantContentChunk(mid, chunk)
|
appendAssistantContentChunk(mid, chunk, event.seq)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -2397,14 +2520,14 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
|||||||
case 'tool_call':
|
case 'tool_call':
|
||||||
if (event.payload?.tool) {
|
if (event.payload?.tool) {
|
||||||
const t = event.payload.tool
|
const t = event.payload.tool
|
||||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
|
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view, event.seq)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'tool_result':
|
case 'tool_result':
|
||||||
if (event.payload?.tool) {
|
if (event.payload?.tool) {
|
||||||
const t = event.payload.tool
|
const t = event.payload.tool
|
||||||
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view)
|
appendToolTraceEvent(mid, mapToolEventState(t.status), normalizeToolSummary(t), buildToolDetail(t), t.name, t.argument_view, t.result_view, event.seq)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -2451,6 +2574,7 @@ function rebuildStateFromTimeline(conversationId: string, events: TimelineEvent[
|
|||||||
duration_seconds: payload.duration_seconds,
|
duration_seconds: payload.duration_seconds,
|
||||||
final: payload.final,
|
final: payload.final,
|
||||||
},
|
},
|
||||||
|
eventSeq: event.seq,
|
||||||
fromHistory: true,
|
fromHistory: true,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
@@ -2918,10 +3042,10 @@ function prepareAssistantMessageForStreaming(message: AssistantMessage, createdA
|
|||||||
delete reasoningStartedAtMap[message.id]
|
delete reasoningStartedAtMap[message.id]
|
||||||
delete reasoningDurationMap[message.id]
|
delete reasoningDurationMap[message.id]
|
||||||
clearToolTraceState(message.id)
|
clearToolTraceState(message.id)
|
||||||
assistantReasoningSeqMap[message.id] = nextAssistantTimelineSeq()
|
|
||||||
toolTraceEventsMap[message.id] = []
|
toolTraceEventsMap[message.id] = []
|
||||||
statusTraceEventsMap[message.id] = []
|
statusTraceEventsMap[message.id] = []
|
||||||
assistantContentBlocksMap[message.id] = []
|
assistantContentBlocksMap[message.id] = []
|
||||||
|
assistantReasoningBlocksMap[message.id] = []
|
||||||
assistantTimelineLastKindMap[message.id] = 'other'
|
assistantTimelineLastKindMap[message.id] = 'other'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3296,8 +3420,7 @@ async function sendMessageInternal(options: SendMessageOptions = {}) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// thinking 态由 applyThinkingSummary 在第一段 thinking_summary 到达时接管,
|
// thinking 态由 applyThinkingSummary 在第一段 thinking_summary 到达时接管,
|
||||||
// 发送瞬间不预设,避免空占位 block 闪现后被真实 block 替换。
|
// 发送瞬间不预设 seq,避免后到的思考块排到先到的日志/工具事件前面。
|
||||||
assistantReasoningSeqMap[assistantMessage.id] = nextAssistantTimelineSeq()
|
|
||||||
reasoningCollapsedMap[assistantMessage.id] = true
|
reasoningCollapsedMap[assistantMessage.id] = true
|
||||||
activeStreamingMessageId.value = assistantMessage.id
|
activeStreamingMessageId.value = assistantMessage.id
|
||||||
shouldAutoFollowMessages.value = true
|
shouldAutoFollowMessages.value = true
|
||||||
@@ -3658,7 +3781,7 @@ onBeforeUnmount(() => {
|
|||||||
<div class="chat-message__reasoning-head">
|
<div class="chat-message__reasoning-head">
|
||||||
<div
|
<div
|
||||||
class="chat-message__reasoning-title"
|
class="chat-message__reasoning-title"
|
||||||
:class="{ 'chat-message__reasoning-title--shimmering': activeStreamingMessageId === block.sourceId && thinkingMessageMap[block.sourceId] }"
|
:class="{ 'chat-message__reasoning-title--shimmering': isReasoningBlockActive(block) }"
|
||||||
>
|
>
|
||||||
<span class="chat-message__reasoning-icon">
|
<span class="chat-message__reasoning-icon">
|
||||||
<svg
|
<svg
|
||||||
@@ -3716,7 +3839,7 @@ onBeforeUnmount(() => {
|
|||||||
:class="{ 'chat-message__markdown--streaming': (block as any).isStreaming }"
|
:class="{ 'chat-message__markdown--streaming': (block as any).isStreaming }"
|
||||||
v-html="renderMessageMarkdown(block.text || '', (block as any).isStreaming)"
|
v-html="renderMessageMarkdown(block.text || '', (block as any).isStreaming)"
|
||||||
/>
|
/>
|
||||||
<div v-else class="chat-message__streaming chat-message__streaming--reasoning">
|
<div v-else-if="isReasoningBlockActive(block)" class="chat-message__streaming chat-message__streaming--reasoning">
|
||||||
<div class="thinking-dot"></div>
|
<div class="thinking-dot"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user