refactor: 移除 message thinking 字段并修复流式渲染显示

This commit is contained in:
ViperEkura 2026-03-26 21:32:29 +08:00
parent 6ffbb29ec7
commit e57b4b7d9a
7 changed files with 31 additions and 75 deletions

View File

@ -64,8 +64,8 @@ class ChatService:
msg_id = str(uuid.uuid4())
tool_calls_list = []
# Send thinking_start event to clear previous thinking in frontend
yield f"event: thinking_start\ndata: {{}}\n\n"
# Clear state for new iteration
# (frontend resets via onProcessStep when first step arrives)
try:
with app.app_context():
@ -102,7 +102,6 @@ class ChatService:
reasoning = delta.get("reasoning_content", "")
if reasoning:
full_thinking += reasoning
yield f"event: thinking\ndata: {json.dumps({'content': reasoning}, ensure_ascii=False)}\n\n"
# Accumulate text content for this iteration
text = delta.get("content", "")

View File

@ -465,8 +465,6 @@ def process_tool_calls(self, tool_calls, context=None):
| 事件 | 说明 |
|------|------|
| `thinking_start` | 新一轮思考开始,前端应清空之前的思考缓冲 |
| `thinking` | 思维链增量内容(启用时) |
| `message` | 回复内容的增量片段 |
| `tool_calls` | 工具调用信息 |
| `tool_result` | 工具执行结果 |

View File

@ -44,7 +44,6 @@
:messages="messages"
:streaming="streaming"
:streaming-content="streamContent"
:streaming-tool-calls="streamToolCalls"
:streaming-process-steps="streamProcessSteps"
:has-more-messages="hasMoreMessages"
:loading-more="loadingMessages"
@ -342,6 +341,11 @@ function createStreamCallbacks(convId, { updateConvList = true } = {}) {
steps[step.index] = step
return steps
})
// When text is finalized as a process_step, reset streaming content
// to prevent duplication (the text is now rendered via processSteps).
if (step.type === 'text') {
updateStreamField(convId, 'streamContent', streamContent, () => '')
}
},
async onDone(data) {
streamStates.delete(convId)

View File

@ -29,10 +29,10 @@ async function request(url, options = {}) {
* Shared SSE stream processor - parses SSE events and dispatches to callbacks
* @param {string} url - API URL (without BASE prefix)
* @param {object} body - Request body
* @param {object} callbacks - Event handlers: { onThinkingStart, onThinking, onMessage, onToolCalls, onToolResult, onProcessStep, onDone, onError }
* @param {object} callbacks - Event handlers: { onMessage, onToolCalls, onToolResult, onProcessStep, onDone, onError }
* @returns {{ abort: () => void }}
*/
function createSSEStream(url, body, { onThinkingStart, onThinking, onMessage, onToolCalls, onToolResult, onProcessStep, onDone, onError }) {
function createSSEStream(url, body, { onMessage, onToolCalls, onToolResult, onProcessStep, onDone, onError }) {
const controller = new AbortController()
const promise = (async () => {
@ -67,11 +67,7 @@ function createSSEStream(url, body, { onThinkingStart, onThinking, onMessage, on
currentEvent = line.slice(7).trim()
} else if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (currentEvent === 'thinking_start' && onThinkingStart) {
onThinkingStart()
} else if (currentEvent === 'thinking' && onThinking) {
onThinking(data.content)
} else if (currentEvent === 'message' && onMessage) {
if (currentEvent === 'message' && onMessage) {
onMessage(data.content)
} else if (currentEvent === 'tool_calls' && onToolCalls) {
onToolCalls(data.calls)

View File

@ -47,7 +47,6 @@
<div class="avatar">claw</div>
<div class="message-body">
<ProcessBlock
:tool-calls="streamingToolCalls"
:process-steps="streamingProcessSteps"
:streaming-content="streamingContent"
:streaming="streaming"
@ -88,7 +87,6 @@ const props = defineProps({
messages: { type: Array, required: true },
streaming: { type: Boolean, default: false },
streamingContent: { type: String, default: '' },
streamingToolCalls: { type: Array, default: () => [] },
streamingProcessSteps: { type: Array, default: () => [] },
hasMoreMessages: { type: Boolean, default: false },
loadingMore: { type: Boolean, default: false },

View File

@ -16,7 +16,6 @@
<ProcessBlock
v-if="processSteps && processSteps.length > 0"
:process-steps="processSteps"
:tool-calls="toolCalls"
/>
<!-- Fallback path: old messages without processSteps in DB, -->
<!-- render toolCalls via ProcessBlock and text separately -->

View File

@ -1,17 +1,7 @@
<template>
<div ref="processRef" class="process-block" :class="{ 'is-streaming': streaming }">
<!-- Placeholder while waiting for the first process step to arrive -->
<div v-if="streaming && processItems.length === 0" class="streaming-placeholder">
<div class="streaming-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
</svg>
</div>
<span class="streaming-text">正在思考中<span class="dots">...</span></span>
</div>
<!-- Render all steps in order: thinking, text, tool_call, tool_result interleaved -->
<template v-else>
<template v-if="processItems.length > 0">
<template v-for="item in processItems" :key="item.key">
<!-- Thinking block -->
<div v-if="item.type === 'thinking'" class="step-item thinking">
@ -60,14 +50,15 @@
<div v-else-if="item.type === 'text'" class="step-item text-content md-content" v-html="item.rendered"></div>
</template>
<!-- Active streaming indicator (cursor) -->
</template>
<!-- Active streaming indicator always visible during streaming, even before any content arrives -->
<div v-if="streaming" class="streaming-indicator">
<svg class="spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>正在生成...</span>
</div>
</template>
</div>
</template>
@ -231,44 +222,6 @@ watch(() => props.streamingContent?.length, () => {
width: 100%;
}
/* Streaming placeholder while waiting for first step */
.streaming-placeholder {
padding: 16px 20px;
display: flex;
align-items: center;
gap: 12px;
background: var(--bg-hover);
border-radius: 8px;
border: 1px solid var(--border-light);
}
.streaming-icon {
width: 28px;
height: 28px;
border-radius: 6px;
background: #fef3c7;
color: #f59e0b;
display: flex;
align-items: center;
justify-content: center;
}
.streaming-text {
font-size: 14px;
color: var(--text-secondary);
font-weight: 500;
}
.streaming-text .dots {
display: inline-block;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* Step items (shared) */
.step-item {
margin-bottom: 8px;
@ -278,6 +231,11 @@ watch(() => props.streamingContent?.length, () => {
margin-bottom: 0;
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
/* Thinking and tool call step headers */
.thinking .step-header,
.tool_call .step-header {
@ -429,10 +387,14 @@ watch(() => props.streamingContent?.length, () => {
display: flex;
align-items: center;
gap: 8px;
margin-top: 8px;
padding: 8px 0 0;
border-top: 1px solid var(--border-light);
font-size: 12px;
color: var(--text-tertiary);
}
/* Add separator only when there are step items above the indicator */
.process-block:has(.step-item) .streaming-indicator {
margin-top: 8px;
padding: 8px 0 0;
border-top: 1px solid var(--border-light);
}
</style>