fix: 修复工具调用和样式渲染的问题

This commit is contained in:
ViperEkura 2026-04-13 07:47:14 +08:00
parent c1788f1ba3
commit 805f8c86da
4 changed files with 115 additions and 84 deletions

View File

@ -1,11 +1,9 @@
<template>
<div ref="processRef" class="process-block" :class="{ 'is-streaming': streaming }">
<!-- Thinking Steps -->
<div
v-for="item in thinkingItems"
:key="item.key"
class="step-item thinking"
>
<!-- Single loop: render all steps in index order for proper alternation -->
<template v-for="item in orderedItems">
<!-- Thinking Step -->
<div v-if="item.type === 'thinking'" :key="`thinking-${item.key}`" class="step-item thinking">
<div class="step-header" @click="toggleExpand(item.key)">
<span v-html="brainIcon"></span>
<span class="step-label">思考中</span>
@ -18,13 +16,8 @@
</div>
</div>
<!-- Tool Call Steps -->
<div
v-for="item in toolCallItems"
:key="item.key"
class="step-item tool_call"
:class="{ loading: item.loading }"
>
<!-- Tool Call Step -->
<div v-else-if="item.type === 'tool_call'" :key="`tool-${item.key}`" class="step-item tool_call" :class="{ loading: item.loading }">
<div class="step-header" @click="toggleExpand(item.key)">
<span v-html="toolIcon"></span>
<span class="step-label">{{ item.name || '工具调用' }}</span>
@ -46,13 +39,9 @@
</div>
</div>
<!-- Text Steps -->
<div
v-for="item in textItems"
:key="item.key"
class="text-content"
v-html="renderMarkdown(item.content)"
></div>
<!-- Text Step -->
<div v-else-if="item.type === 'text'" :key="`text-${item.key}`" class="text-content md-content" v-html="renderMarkdown(item.content)"></div>
</template>
<!-- Streaming indicator -->
<div v-if="streaming && !hasContent" class="streaming-indicator">
@ -85,6 +74,7 @@ const allItems = computed(() => {
items.push({
key: step.id || `thinking-${step.index}`,
type: 'thinking',
index: step.index,
content: step.content || '',
brief: step.content ? step.content.slice(0, 50) + (step.content.length > 50 ? '...' : '') : '',
})
@ -92,9 +82,10 @@ const allItems = computed(() => {
items.push({
key: step.id || `tool-${step.index}`,
type: 'tool_call',
index: step.index,
id: step.id,
name: step.name,
args: step.args,
args: step.arguments || step.args || '{}', // arguments
brief: step.name || '',
loading: step.loading,
isSuccess: step.isSuccess,
@ -115,9 +106,10 @@ const allItems = computed(() => {
items.push({
key: `result-${step.id || step.index}`,
type: 'tool_call',
index: step.index,
id: step.id_ref || step.id,
name: step.name || '工具结果',
args: '{}',
args: '{}', //
brief: step.name || '工具结果',
loading: false,
isSuccess: true,
@ -129,6 +121,7 @@ const allItems = computed(() => {
items.push({
key: step.id || `text-${step.index}`,
type: 'text',
index: step.index,
content: step.content || '',
})
}
@ -150,14 +143,33 @@ const allItems = computed(() => {
return items
})
const thinkingItems = computed(() => allItems.value.filter(i => i.type === 'thinking'))
const toolCallItems = computed(() => allItems.value.filter(i => i.type === 'tool_call'))
const textItems = computed(() => allItems.value.filter(i => i.type === 'text'))
// Ordered by index for proper step alternation
const orderedItems = computed(() =>
[...allItems.value].sort((a, b) => (a.index || 0) - (b.index || 0))
)
const orderedThinkingItems = computed(() =>
allItems.value
.filter(i => i.type === 'thinking')
.sort((a, b) => (a.index || 0) - (b.index || 0))
)
const orderedToolCallItems = computed(() =>
allItems.value
.filter(i => i.type === 'tool_call')
.sort((a, b) => (a.index || 0) - (b.index || 0))
)
const orderedTextItems = computed(() =>
allItems.value
.filter(i => i.type === 'text')
.sort((a, b) => (a.index || 0) - (b.index || 0))
)
const hasContent = computed(() => allItems.value.length > 0)
const lastThinkingKey = computed(() => {
const thinkingItems = allItems.value.filter(i => i.type === 'thinking')
return thinkingItems.length > 0 ? thinkingItems[thinkingItems.length - 1].key : null
const items = allItems.value.filter(i => i.type === 'thinking')
return items.length > 0 ? items[items.length - 1].key : null
})
function toggleExpand(key) {

View File

@ -84,7 +84,7 @@ export function createSSEStream(url, body, { onProcessStep, onDone, onError }) {
} else if (line.startsWith('data: ')) {
const data = JSON.parse(line.slice(6))
if (currentEvent === 'process_step' && onProcessStep) {
onProcessStep(data)
onProcessStep(data.step)
} else if (currentEvent === 'done' && onDone) {
completed = true
onDone(data)

View File

@ -71,7 +71,15 @@ onMounted(fetchData)
</script>
<style scoped>
.tools { padding: 0; }
.tools {
padding: 0;
height: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.tools > * { flex-shrink: 0; }
.grid { flex: 1; min-height: 0; }
.tools h1 { font-size: 2rem; margin: 0 0 1.5rem; color: var(--text-h); }
.stats { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
.stat { font-size: 1.1rem; color: var(--text); }

View File

@ -135,9 +135,11 @@ class ChatService:
full_thinking = ""
tool_calls_list = []
# Generate new step IDs for each iteration to track multiple thoughts/tools
iteration_thinking_step_id = f"thinking-{iteration}"
iteration_text_step_id = f"text-{iteration}"
# Step tracking - use unified step-{index} format
thinking_step_id = None
thinking_step_idx = None
text_step_id = None
text_step_idx = None
async for sse_line in llm.stream_call(
model=model,
@ -185,31 +187,37 @@ class ChatService:
# Handle reasoning (thinking)
reasoning = delta.get("reasoning_content", "")
if reasoning:
prev_thinking_len = len(full_thinking)
full_thinking += reasoning
if thinking_step_id is None:
thinking_step_id = iteration_thinking_step_id
if prev_thinking_len == 0: # New thinking stream started
thinking_step_idx = step_index
thinking_step_id = f"step-{step_index}"
step_index += 1
yield _sse_event("process_step", {
"step": {
"id": thinking_step_id,
"index": thinking_step_idx,
"type": "thinking",
"content": full_thinking
}
})
# Handle content
content = delta.get("content", "")
if content:
prev_content_len = len(full_content)
full_content += content
if text_step_id is None:
if prev_content_len == 0: # New text stream started
text_step_idx = step_index
text_step_id = iteration_text_step_id
text_step_id = f"step-{step_index}"
step_index += 1
yield _sse_event("process_step", {
"step": {
"id": text_step_id,
"index": text_step_idx,
"type": "text",
"content": full_content
}
})
# Accumulate tool calls
@ -250,42 +258,45 @@ class ChatService:
if tool_calls_list:
all_tool_calls.extend(tool_calls_list)
# Yield tool_call steps
# Yield tool_call steps - use unified step-{index} format
tool_call_step_ids = [] # Track step IDs for tool calls
for tc in tool_calls_list:
call_step_id = f"tool-{iteration}-{tc.get('function', {}).get('name', 'unknown')}"
call_step_idx = step_index
call_step_id = f"step-{step_index}"
tool_call_step_ids.append(call_step_id)
step_index += 1
call_step = {
"id": call_step_id,
"index": step_index,
"index": call_step_idx,
"type": "tool_call",
"id_ref": tc.get("id", ""),
"name": tc["function"]["name"],
"arguments": tc["function"]["arguments"]
}
all_steps.append(call_step)
yield _sse_event("process_step", call_step)
step_index += 1
yield _sse_event("process_step", {"step": call_step})
# Execute tools
tool_results = self.tool_executor.process_tool_calls_parallel(
tool_calls_list, {}
)
# Yield tool_result steps
# Yield tool_result steps - use unified step-{index} format
for i, tr in enumerate(tool_results):
tool_call_step_id = tool_call_step_ids[i] if i < len(tool_call_step_ids) else f"tool-{i}"
tool_call_step_id = tool_call_step_ids[i] if i < len(tool_call_step_ids) else f"step-{i}"
result_step_idx = step_index
result_step_id = f"step-{step_index}"
step_index += 1
result_step = {
"id": f"result-{iteration}-{tr.get('name', 'unknown')}",
"index": step_index,
"id": result_step_id,
"index": result_step_idx,
"type": "tool_result",
"id_ref": tool_call_step_id, # Reference to the tool_call step
"name": tr.get("name", ""),
"content": tr.get("content", "")
}
all_steps.append(result_step)
yield _sse_event("process_step", result_step)
step_index += 1
yield _sse_event("process_step", {"step": result_step})
all_tool_results.append({
"role": "tool",