增加供应商智能添加功能

This commit is contained in:
hero920103 committed 2026-09-04 00:09:32 +08:00
1 parent d8936d9730
commit 20be7f4817
29 files changed
+4317 -1476

No files matched your search

@@ -42,6 +42,13 @@
:value="m"
/>
</el-select>
<el-tooltip :content="mcpTip" placement="bottom">
<span class="mcp-indicator" :class="{ 'has-mcp': activeMcpTools.length > 0 }">
<el-icon><Cpu /></el-icon>
<span class="mcp-ind-text">MCP</span>
<span v-if="activeMcpTools.length > 0" class="mcp-ind-count">{{ activeMcpTools.length }}</span>
</span>
</el-tooltip>
</div>
</div>
@@ -81,13 +88,46 @@
</div>
<!-- 正常显示 -->
<template v-else>
<div class="message-bubble" :class="{ typing: msg.role === 'assistant' && !msg.content && sending }">
<div
class="message-bubble"
:class="{
typing: msg.role === 'assistant' && !msg.content && sending,
'has-tools': msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length
}"
>
<template v-if="msg.role === 'assistant' && !msg.content && sending">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</template>
<pre v-else class="message-text">{{ msg.content }}</pre>
<template v-else>
<div v-if="msg.role === 'assistant' && msg.toolCalls && msg.toolCalls.length" class="tool-call-list">
<div v-for="tc in msg.toolCalls" :key="tc.id" class="tool-call-card">
<div class="tool-call-head" @click="tc.expanded = !tc.expanded">
<el-icon class="tool-call-icon"><Cpu /></el-icon>
<span class="tool-call-name">
<span v-if="tc.serverName" class="tool-server">{{ tc.serverName }}</span>
{{ tc.tool }}
</span>
<el-icon v-if="tc.status === 'running'" class="is-loading tool-status-icon"><Loading /></el-icon>
<el-tag v-else-if="tc.status === 'ok'" type="success" size="small" effect="plain">完成</el-tag>
<el-tag v-else-if="tc.status === 'error'" type="danger" size="small" effect="plain">失败</el-tag>
<span class="tool-call-toggle">{{ tc.expanded ? '收起' : '详情' }}</span>
</div>
<div v-if="tc.expanded" class="tool-call-detail">
<div v-if="tc.args && Object.keys(tc.args).length" class="tool-call-block">
<div class="tool-call-label">参数</div>
<pre class="tool-call-json">{{ JSON.stringify(tc.args, null, 2) }}</pre>
</div>
<div v-if="tc.status !== 'running'" class="tool-call-block">
<div class="tool-call-label">结果</div>
<pre class="tool-call-json" :class="{ 'is-error': tc.status === 'error' }">{{ tc.status === 'error' ? tc.error : tc.result }}</pre>
</div>
</div>
</div>
</div>
<pre v-if="msg.content" class="message-text">{{ msg.content }}</pre>
</template>
</div>
<div class="message-actions">
<el-tooltip content="复制" placement="top">
@@ -140,8 +180,8 @@
<script setup>
import { ref, nextTick, watch, computed, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { User, Cpu, Promotion, Edit, CopyDocument, RefreshRight, Delete, UserFilled } from '@element-plus/icons-vue'
import { getChatMessageList, sendChatMessage, getAiProviderList, deleteChatMessage, getAiPresetList } from '@/api/ai'
import { User, Cpu, Promotion, Edit, CopyDocument, RefreshRight, Delete, UserFilled, Loading } from '@element-plus/icons-vue'
import { getChatMessageList, sendChatMessage, getAiProviderList, deleteChatMessage, getAiPresetList, getMcpServerList } from '@/api/ai'
import PresetDialog from './presetDialog.vue'
const props = defineProps({
@@ -171,6 +211,16 @@ const selectedModel = ref('')
const presetVisible = ref(false)
const defaultPreset = ref(null)
// MCP 接入状态
const activeMcpTools = ref([])
const mcpTip = computed(() => {
if (activeMcpTools.value.length === 0) {
return '当前未启用 MCP 工具,可在侧边栏 CPU 按钮中配置'
}
const servers = [...new Set(activeMcpTools.value.map((t) => t.server_name).filter(Boolean))]
return `已接入 ${activeMcpTools.value.length} 个 MCP 工具(${servers.join('、')}),会话中 AI 会自动调用`
})
const currentModels = computed(() => {
const p = providerList.value.find(item => item.id === selectedProviderId.value)
return p && p.models ? p.models : []
@@ -179,8 +229,25 @@ const currentModels = computed(() => {
onMounted(() => {
fetchProviders()
fetchDefaultPreset()
fetchMcpStatus()
})
async function fetchMcpStatus() {
try {
const res = await getMcpServerList()
if (res.code === 200 && res.data && res.data.list) {
const enabled = res.data.list.filter((s) => s.enabled === 1)
const tools = []
enabled.forEach((s) => {
tools.push({ key: '', server_id: s.id, server_name: s.name, tool_name: '' })
})
activeMcpTools.value = tools
}
} catch (e) {
// 静默失败
}
}
async function fetchDefaultPreset() {
try {
const res = await getAiPresetList()
@@ -386,7 +453,7 @@ async function doSend(content) {
// 添加用户消息
messages.value.push({ role: 'user', content })
// 添加空的AI消息,用于流式更新
const aiMsg = { id: null, role: 'assistant', content: '' }
const aiMsg = { id: null, role: 'assistant', content: '', toolCalls: [] }
messages.value.push(aiMsg)
sending.value = true
scrollToBottom()
@@ -434,6 +501,35 @@ async function doSend(content) {
} else if (eventType === 'content') {
aiMsg.content += parsed.content
scrollToBottom()
} else if (eventType === 'tool_list') {
// 当前会话启用的 MCP 工具
if (parsed.tools) {
activeMcpTools.value = parsed.tools.filter((t) => t.tool_name)
}
} else if (eventType === 'tool_start') {
// AI 开始调用某个 MCP 工具
const tc = {
id: parsed.id,
key: parsed.key,
tool: parsed.tool,
serverName: parsed.server_name,
args: parsed.args || {},
status: 'running',
expanded: false,
result: '',
error: ''
}
aiMsg.toolCalls.push(tc)
scrollToBottom()
} else if (eventType === 'tool_result') {
// 工具调用返回结果
const tc = aiMsg.toolCalls.find((t) => t.id === parsed.id)
if (tc) {
tc.status = parsed.ok ? 'ok' : 'error'
tc.result = parsed.result || ''
tc.error = parsed.error || ''
}
scrollToBottom()
} else if (eventType === 'done') {
aiMsg.id = parsed.message_id
} else if (eventType === 'error') {
@@ -743,4 +839,130 @@ defineExpose({
margin-top: 8px;
}
}
// MCP 状态指示
.mcp-indicator {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 16px;
border: 1px solid var(--el-border-color);
color: var(--el-text-color-secondary);
font-size: 12px;
cursor: default;
height: 32px;
&.has-mcp {
background: var(--el-color-success-light-9);
border-color: var(--el-color-success);
color: var(--el-color-success);
}
.mcp-ind-count {
background: var(--el-color-success);
color: #fff;
border-radius: 8px;
font-size: 11px;
line-height: 16px;
padding: 0 5px;
min-width: 16px;
text-align: center;
}
}
// 工具调用卡片
.tool-call-list {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 8px;
}
.tool-call-card {
border: 1px solid var(--el-border-color-lighter);
border-radius: 8px;
background: var(--el-bg-color);
overflow: hidden;
.tool-call-head {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
cursor: pointer;
font-size: 13px;
.tool-call-icon {
color: var(--el-color-primary);
flex-shrink: 0;
}
.tool-call-name {
flex: 1;
font-family: 'JetBrains Mono', Consolas, monospace;
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.tool-server {
color: var(--el-text-color-secondary);
font-family: inherit;
margin-right: 4px;
}
}
.tool-status-icon {
color: var(--el-color-primary);
}
.tool-call-toggle {
font-size: 12px;
color: var(--el-text-color-secondary);
flex-shrink: 0;
}
}
.tool-call-detail {
border-top: 1px dashed var(--el-border-color-lighter);
padding: 8px 10px;
.tool-call-block {
margin-bottom: 6px;
&:last-child {
margin-bottom: 0;
}
}
.tool-call-label {
font-size: 12px;
color: var(--el-text-color-secondary);
margin-bottom: 4px;
}
.tool-call-json {
margin: 0;
background: #f5f7fa;
border-radius: 6px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow: auto;
&.is-error {
color: var(--el-color-danger);
}
}
}
}
.message-bubble.has-tools {
display: block;
max-width: 100%;
}
</style>