feat: 新增图表工具组件,支持图表结果展示

This commit is contained in:
Wenjie Zhang 2026-03-05 02:29:01 +08:00
parent 30df84efc1
commit c4beda2ef1
6 changed files with 102 additions and 10 deletions

View File

@ -1,7 +1,6 @@
from langgraph.types import interrupt
from src.agents.common.toolkits.registry import tool
from src.utils import logger
@tool(category="debug", tags=["内置", "审批"], display_name="人工审批")

View File

@ -20,7 +20,6 @@ async def test_delete_knowledge_base_cleanup():
"""测试删除知识库时 MinIO 文件清理"""
from src.storage.minio import get_minio_client
from src.knowledge import knowledge_base
from src.knowledge.utils.kb_utils import is_minio_url
# 初始化
minio_client = get_minio_client()

View File

@ -5,6 +5,9 @@
<!-- 网页搜索 -->
<WebSearchTool v-else-if="isWebSearchResult" :tool-call="toolCall" />
<!-- Chart -->
<ChartTool v-else-if="isChartResult" :tool-call="toolCall" />
<!-- 知识库列表 -->
<ListKbsTool v-else-if="toolName === 'list_kbs'" :tool-call="toolCall" />
@ -72,6 +75,7 @@ import ListKbsTool from './tools/ListKbsTool.vue'
import GetMindmapTool from './tools/GetMindmapTool.vue'
import QueryKbTool from './tools/QueryKbTool.vue'
import KnowledgeGraphTool from './tools/KnowledgeGraphTool.vue'
import ChartTool from './tools/ChartTool.vue'
import CalculatorTool from './tools/CalculatorTool.vue'
import TodoListTool from './tools/TodoListTool.vue'
import ImageTool from './tools/ImageTool.vue'
@ -141,10 +145,18 @@ const isCalculatorResult = computed(() => {
return name.includes('calculator') || name.includes('calc') || name.includes('math')
})
const isImageResult = computed(() => {
const isChartResult = computed(() => {
const name = toolName.value.toLowerCase()
if (!name.includes('chart')) return false
const data = parseData(props.toolCall.tool_call_result?.content)
// chart [{ type: "text", text: "url", id: "..." }]
return Array.isArray(data) && data.length > 0 && data[0].type === 'text'
})
const isImageResult = computed(() => {
const name = toolName.value.toLowerCase()
if (!name.includes('text_to_img')) return false
const data = parseData(props.toolCall.tool_call_result?.content)
return data && typeof data === 'string' && data.startsWith('http')
})

View File

@ -0,0 +1,60 @@
<template>
<BaseToolCall :tool-call="toolCall">
<template #result="{ resultContent }">
<div class="chart-result">
<img :src="chartUrl" />
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
toolCall: {
type: Object,
required: true
}
})
const parseData = (content) => {
if (typeof content === 'string') {
try {
return JSON.parse(content)
} catch (error) {
return content
}
}
return content
}
const parsedContent = computed(() => {
return parseData(props.toolCall.tool_call_result?.content)
})
const chartUrl = computed(() => {
const content = parsedContent.value
// chart [{ type: "text", text: "url", id: "..." }]
if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
return content[0].text
}
return null
})
</script>
<style lang="less" scoped>
.chart-result {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
padding: 8px;
img {
max-width: 100%;
border-radius: 4px;
}
}
</style>

View File

@ -1,14 +1,18 @@
<template>
<BaseToolCall :tool-call="toolCall">
<template #result="{ resultContent }">
<div class="image-result">
<img :src="parseData(resultContent)" />
<div v-if="imageUrl" class="image-result">
<img :src="imageUrl" />
</div>
<div v-else class="text-result">
{{ parsedContent }}
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
const props = defineProps({
@ -28,6 +32,19 @@ const parseData = (content) => {
}
return content
}
const parsedContent = computed(() => {
return parseData(props.toolCall.tool_call_result?.content)
})
const imageUrl = computed(() => {
const content = parsedContent.value
// text_to_img_qwen_image URL
if (content && typeof content === 'string' && content.startsWith('http')) {
return content
}
return null
})
</script>
<style lang="less" scoped>
@ -43,4 +60,9 @@ const parseData = (content) => {
border-radius: 4px;
}
}
.text-result {
padding: 8px;
color: var(--color-text);
}
</style>

View File

@ -2,8 +2,8 @@
<div class="extensions-view">
<div class="extensions-header">
<a-tabs v-model:activeKey="activeTab" class="extensions-tabs">
<a-tab-pane key="skills" tab="Skills 管理" />
<a-tab-pane key="tools" tab="工具" />
<a-tab-pane key="skills" tab="Skills 管理" />
<a-tab-pane key="mcp" tab="MCP 服务器" />
</a-tabs>
<div class="header-actions">
@ -47,6 +47,9 @@
</div>
<div class="extensions-content">
<div v-show="activeTab === 'tools'" class="tab-panel">
<ToolsManagerComponent ref="toolsRef" @refresh="handleToolsRefresh" />
</div>
<div v-show="activeTab === 'skills'" class="tab-panel">
<SkillsManagerComponent
ref="skillsRef"
@ -54,9 +57,6 @@
@refresh="handleSkillsRefresh"
/>
</div>
<div v-show="activeTab === 'tools'" class="tab-panel">
<ToolsManagerComponent ref="toolsRef" @refresh="handleToolsRefresh" />
</div>
<div v-show="activeTab === 'mcp'" class="tab-panel">
<McpServersComponent ref="mcpRef" @add="handleMcpAdd" @refresh="handleMcpRefresh" />
</div>
@ -71,7 +71,7 @@ import SkillsManagerComponent from '@/components/SkillsManagerComponent.vue'
import ToolsManagerComponent from '@/components/ToolsManagerComponent.vue'
import McpServersComponent from '@/components/McpServersComponent.vue'
const activeTab = ref('skills')
const activeTab = ref('tools')
const skillsRef = ref(null)
const toolsRef = ref(null)
const mcpRef = ref(null)