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,58 +1,47 @@
<template> <template>
<div ref="processRef" class="process-block" :class="{ 'is-streaming': streaming }"> <div ref="processRef" class="process-block" :class="{ 'is-streaming': streaming }">
<!-- Thinking Steps --> <!-- Single loop: render all steps in index order for proper alternation -->
<div <template v-for="item in orderedItems">
v-for="item in thinkingItems" <!-- Thinking Step -->
:key="item.key" <div v-if="item.type === 'thinking'" :key="`thinking-${item.key}`" class="step-item thinking">
class="step-item thinking" <div class="step-header" @click="toggleExpand(item.key)">
> <span v-html="brainIcon"></span>
<div class="step-header" @click="toggleExpand(item.key)"> <span class="step-label">思考中</span>
<span v-html="brainIcon"></span> <span class="step-brief">{{ item.brief || '正在思考...' }}</span>
<span class="step-label">思考中</span> <span v-if="streaming && item.key === lastThinkingKey" class="loading-dots">...</span>
<span class="step-brief">{{ item.brief || '正在思考...' }}</span> <span class="arrow" :class="{ open: expandedKeys.has(item.key) }" v-html="chevronDown"></span>
<span v-if="streaming && item.key === lastThinkingKey" class="loading-dots">...</span>
<span class="arrow" :class="{ open: expandedKeys.has(item.key) }" v-html="chevronDown"></span>
</div>
<div v-if="expandedKeys.has(item.key)" class="step-content">
<div class="thinking-text">{{ item.content }}</div>
</div>
</div>
<!-- Tool Call Steps -->
<div
v-for="item in toolCallItems"
:key="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>
<span class="step-brief">{{ item.brief || '' }}</span>
<span v-if="item.loading" class="loading-dots">...</span>
<span v-else-if="item.isSuccess === true" class="step-badge success">成功</span>
<span v-else-if="item.isSuccess === false" class="step-badge error">失败</span>
<span class="arrow" :class="{ open: expandedKeys.has(item.key) }" v-html="chevronDown"></span>
</div>
<div v-if="expandedKeys.has(item.key)" class="step-content">
<div class="tool-detail">
<span class="detail-label">参数</span>
<pre>{{ formatArgs(item.args) }}</pre>
</div> </div>
<div v-if="item.resultSummary || item.fullResult" class="tool-detail" style="margin-top: 8px;"> <div v-if="expandedKeys.has(item.key)" class="step-content">
<span class="detail-label">结果</span> <div class="thinking-text">{{ item.content }}</div>
<pre>{{ item.fullResult || item.resultSummary }}</pre>
</div> </div>
</div> </div>
</div>
<!-- Text Steps --> <!-- Tool Call Step -->
<div <div v-else-if="item.type === 'tool_call'" :key="`tool-${item.key}`" class="step-item tool_call" :class="{ loading: item.loading }">
v-for="item in textItems" <div class="step-header" @click="toggleExpand(item.key)">
:key="item.key" <span v-html="toolIcon"></span>
class="text-content" <span class="step-label">{{ item.name || '工具调用' }}</span>
v-html="renderMarkdown(item.content)" <span class="step-brief">{{ item.brief || '' }}</span>
></div> <span v-if="item.loading" class="loading-dots">...</span>
<span v-else-if="item.isSuccess === true" class="step-badge success">成功</span>
<span v-else-if="item.isSuccess === false" class="step-badge error">失败</span>
<span class="arrow" :class="{ open: expandedKeys.has(item.key) }" v-html="chevronDown"></span>
</div>
<div v-if="expandedKeys.has(item.key)" class="step-content">
<div class="tool-detail">
<span class="detail-label">参数</span>
<pre>{{ formatArgs(item.args) }}</pre>
</div>
<div v-if="item.resultSummary || item.fullResult" class="tool-detail" style="margin-top: 8px;">
<span class="detail-label">结果</span>
<pre>{{ item.fullResult || item.resultSummary }}</pre>
</div>
</div>
</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 --> <!-- Streaming indicator -->
<div v-if="streaming && !hasContent" class="streaming-indicator"> <div v-if="streaming && !hasContent" class="streaming-indicator">
@ -85,6 +74,7 @@ const allItems = computed(() => {
items.push({ items.push({
key: step.id || `thinking-${step.index}`, key: step.id || `thinking-${step.index}`,
type: 'thinking', type: 'thinking',
index: step.index,
content: step.content || '', content: step.content || '',
brief: step.content ? step.content.slice(0, 50) + (step.content.length > 50 ? '...' : '') : '', brief: step.content ? step.content.slice(0, 50) + (step.content.length > 50 ? '...' : '') : '',
}) })
@ -92,9 +82,10 @@ const allItems = computed(() => {
items.push({ items.push({
key: step.id || `tool-${step.index}`, key: step.id || `tool-${step.index}`,
type: 'tool_call', type: 'tool_call',
index: step.index,
id: step.id, id: step.id,
name: step.name, name: step.name,
args: step.args, args: step.arguments || step.args || '{}', // arguments
brief: step.name || '', brief: step.name || '',
loading: step.loading, loading: step.loading,
isSuccess: step.isSuccess, isSuccess: step.isSuccess,
@ -115,9 +106,10 @@ const allItems = computed(() => {
items.push({ items.push({
key: `result-${step.id || step.index}`, key: `result-${step.id || step.index}`,
type: 'tool_call', type: 'tool_call',
index: step.index,
id: step.id_ref || step.id, id: step.id_ref || step.id,
name: step.name || '工具结果', name: step.name || '工具结果',
args: '{}', args: '{}', //
brief: step.name || '工具结果', brief: step.name || '工具结果',
loading: false, loading: false,
isSuccess: true, isSuccess: true,
@ -129,6 +121,7 @@ const allItems = computed(() => {
items.push({ items.push({
key: step.id || `text-${step.index}`, key: step.id || `text-${step.index}`,
type: 'text', type: 'text',
index: step.index,
content: step.content || '', content: step.content || '',
}) })
} }
@ -150,14 +143,33 @@ const allItems = computed(() => {
return items return items
}) })
const thinkingItems = computed(() => allItems.value.filter(i => i.type === 'thinking')) // Ordered by index for proper step alternation
const toolCallItems = computed(() => allItems.value.filter(i => i.type === 'tool_call')) const orderedItems = computed(() =>
const textItems = computed(() => allItems.value.filter(i => i.type === 'text')) [...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 hasContent = computed(() => allItems.value.length > 0)
const lastThinkingKey = computed(() => { const lastThinkingKey = computed(() => {
const thinkingItems = allItems.value.filter(i => i.type === 'thinking') const items = allItems.value.filter(i => i.type === 'thinking')
return thinkingItems.length > 0 ? thinkingItems[thinkingItems.length - 1].key : null return items.length > 0 ? items[items.length - 1].key : null
}) })
function toggleExpand(key) { function toggleExpand(key) {

View File

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

View File

@ -71,7 +71,15 @@ onMounted(fetchData)
</script> </script>
<style scoped> <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); } .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; } .stats { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
.stat { font-size: 1.1rem; color: var(--text); } .stat { font-size: 1.1rem; color: var(--text); }

View File

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