增加供应商智能添加功能
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user