feat: 优化

This commit is contained in:
2026-02-22 21:36:47 +08:00
parent 227dd4f78d
commit ff11f04b43
12 changed files with 364 additions and 134 deletions

View File

@@ -0,0 +1,84 @@
/**
* AI 智能体 API
*/
import request from '@/api/http'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import tokenManager from '@gold/utils/token-manager'
import { API_BASE } from '@gold/config/api'
const BASE_URL = `${API_BASE.APP_TIK}`
/**
* 获取启用的智能体列表
*/
export function getAgentList() {
return request({
url: `${BASE_URL}/agent/list`,
method: 'get'
})
}
/**
* 流式对话SSE
* @param {Object} options - 请求配置
* @param {number} options.agentId - 智能体ID
* @param {string} options.content - 用户输入内容
* @param {string} [options.conversationId] - 会话ID可选首次对话不传
* @param {AbortController} [options.ctrl] - 取消控制器
* @param {Function} options.onMessage - 消息回调
* @param {Function} [options.onError] - 错误回调
* @param {Function} [options.onClose] - 关闭回调
*/
export async function sendChatStream(options) {
const {
agentId,
content,
conversationId,
ctrl,
onMessage,
onError,
onClose
} = options || {}
const token = tokenManager.getAccessToken()
return fetchEventSource(`${BASE_URL}/dify/chat/stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
'tenant-id': import.meta.env?.VITE_TENANT_ID
},
openWhenHidden: true,
body: JSON.stringify({
agentId,
content,
conversationId
}),
onmessage: (event) => {
if (typeof onMessage === 'function') {
try {
const data = JSON.parse(event.data)
// 解析 CommonResult 包装
const result = data.code === 0 ? data.data : data
onMessage(result)
} catch (e) {
console.error('解析 SSE 数据失败:', e)
}
}
},
onerror: (err) => {
if (typeof onError === 'function') {
onError(err)
}
throw err // 不重试
},
onclose: () => {
if (typeof onClose === 'function') {
onClose()
}
},
signal: ctrl ? ctrl.signal : undefined
})
}

View File

@@ -70,8 +70,8 @@
<!-- 加载中 -->
<div v-if="loading" class="message-item message-item--assistant">
<div class="agent-avatar-small msg-avatar">
<RobotOutlined v-if="!agent?.avatar" class="avatar-icon" />
<img v-else :src="agent?.avatar" :alt="agent?.name" />
<img v-if="agent?.avatar" :src="agent?.avatar" :alt="agent?.name" />
<RobotOutlined v-else class="avatar-icon" />
</div>
<div class="message-bubble message-bubble--assistant">
<div class="typing-indicator">
@@ -135,6 +135,7 @@ import {
ThunderboltFilled
} from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { sendChatStream } from '@/api/agent'
const props = defineProps({
visible: {
@@ -156,9 +157,16 @@ const loading = ref(false)
const messages = ref([])
const messagesRef = ref(null)
const userAvatar = ref('王')
const conversationId = ref(null) // 会话ID用于连续对话
const abortController = ref(null) // 用于取消请求
// 方法
const handleClose = () => {
// 取消正在进行的请求
if (abortController.value) {
abortController.value.abort()
abortController.value = null
}
emit('update:visible', false)
}
@@ -181,24 +189,74 @@ const handleSend = async () => {
await scrollToBottom()
// 模拟 AI 响应
setTimeout(() => {
const assistantMessage = {
role: 'assistant',
content: generateMockResponse(question),
isPro: modelMode.value === 'pro',
actions: true
}
messages.value.push(assistantMessage)
loading.value = false
nextTick(() => scrollToBottom())
}, 1500)
// 创建 AI 消息占位
const assistantMessage = {
role: 'assistant',
content: '',
isPro: modelMode.value === 'pro',
actions: false
}
messages.value.push(assistantMessage)
emit('send', {
agentId: props.agent?.id,
content: question,
modelMode: modelMode.value
})
// 创建取消控制器
abortController.value = new AbortController()
// 调用流式对话 API
try {
await sendChatStream({
agentId: props.agent?.id,
content: question,
conversationId: conversationId.value,
ctrl: abortController.value,
onMessage: (result) => {
if (result.event === 'message' && result.content) {
// 追加消息内容
assistantMessage.content += result.content
scrollToBottom()
} else if (result.event === 'done') {
// 对话完成
conversationId.value = result.conversationId
assistantMessage.actions = true
} else if (result.event === 'error') {
// 错误处理
message.error(result.errorMessage || '对话出错')
}
},
onError: (error) => {
console.error('发送消息失败:', error)
message.error('发送消息失败,请重试')
// 移除失败的 AI 消息
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg?.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
},
onClose: () => {
loading.value = false
abortController.value = null
nextTick(() => scrollToBottom())
}
})
emit('send', {
agentId: props.agent?.id,
content: question,
modelMode: modelMode.value
})
} catch (error) {
// 用户取消不需要提示
if (error.name !== 'AbortError') {
console.error('发送消息失败:', error)
message.error('发送消息失败,请重试')
// 移除失败的 AI 消息
const lastMsg = messages.value[messages.value.length - 1]
if (lastMsg?.role === 'assistant' && !lastMsg.content) {
messages.value.pop()
}
}
} finally {
loading.value = false
}
}
const handleKeyDown = (e) => {
@@ -213,9 +271,14 @@ const handleCopy = (content) => {
message.success('已复制到剪贴板')
}
const handleRegenerate = (index) => {
// TODO: 重新生成消息
message.info('重新生成中...')
const handleRegenerate = async (index) => {
// 重新生成:移除当前消息,重新发送上一条用户消息
if (index > 0 && messages.value[index - 1]?.role === 'user') {
const userMsg = messages.value[index - 1]
messages.value.splice(index - 1, 2) // 移除用户消息和 AI 回复
inputText.value = userMsg.content
await handleSend()
}
}
const scrollToBottom = async () => {
@@ -225,20 +288,13 @@ const scrollToBottom = async () => {
}
}
const generateMockResponse = (question) => {
const responses = [
'当夜深人静的时候,我们卸下了一天的铠甲,那才是真实的自己。成年人的世界里,连崩溃都要调成静音模式。',
'根据您的需求,我为您生成以下内容:这是一个经过精心设计的文案,结合了情感共鸣和产品卖点。',
'让我帮您分析一下这个问题。首先,我们需要考虑目标受众的需求和痛点...'
]
return responses[Math.floor(Math.random() * responses.length)]
}
// 监听 visible 变化,重置状态
watch(() => props.visible, (newVal) => {
if (newVal) {
messages.value = []
inputText.value = ''
conversationId.value = null // 重置会话ID
abortController.value = null
nextTick(() => scrollToBottom())
}
})

View File

@@ -70,9 +70,9 @@
<!-- 描述 -->
<p class="card-description">{{ agent.description }}</p>
<!-- 底部使用量 + 按钮 -->
<!-- 底部按钮 -->
<div class="card-footer">
<span class="usage-count">🔥 {{ formatNumber(agent.usage) }}+ 使用</span>
<span class="usage-count">{{ agent.categoryName }}</span>
<button class="chat-btn" @click.stop="handleChat(agent)">
开始对话
</button>
@@ -105,6 +105,7 @@ import {
import { message } from 'ant-design-vue'
import FullWidthLayout from '@/layouts/components/FullWidthLayout.vue'
import ChatDrawer from '@/components/agents/ChatDrawer.vue'
import { getAgentList } from '@/api/agent'
// 状态管理
const loading = ref(false)
@@ -113,92 +114,45 @@ const searchKeyword = ref('')
const chatDrawerVisible = ref(false)
const currentAgent = ref(null)
// 分类数据
const categories = ref([
{ id: 'all', name: '全部智能体', count: 0 },
{ id: 'writing', name: '文案创作', count: 0 },
{ id: 'analysis', name: '数据分析', count: 0 },
{ id: 'coding', name: '代码助手', count: 0 },
{ id: 'design', name: '设计创作', count: 0 },
{ id: 'education', name: '教育学习', count: 0 }
])
// 智能体列表数据(从 API 获取)
const agentList = ref([])
// 智能体列表数据
const agentList = ref([
{
id: 1,
name: '文案写作助手',
description: '专业的内容创作智能体,帮助您撰写各类文案,包括广告语、产品介绍、营销文案等。',
avatar: '',
categoryId: 'writing',
categoryName: '文案创作',
tagColor: 'blue',
usage: 12580
},
{
id: 2,
name: '数据分析专家',
description: '强大的数据分析工具,支持多种数据格式,提供专业的数据洞察和可视化建议。',
avatar: '',
categoryId: 'analysis',
categoryName: '数据分析',
tagColor: 'green',
usage: 8920
},
{
id: 3,
name: '代码生成器',
description: '智能代码生成工具,支持多种编程语言,提高开发效率,减少重复工作。',
avatar: '',
categoryId: 'coding',
categoryName: '代码助手',
tagColor: 'purple',
usage: 15230
},
{
id: 4,
name: '海报设计助手',
description: '专业的海报设计工具,提供丰富的模板和设计建议,轻松制作精美海报。',
avatar: '',
categoryId: 'design',
categoryName: '设计创作',
tagColor: 'pink',
usage: 6750
},
{
id: 5,
name: '英语学习伙伴',
description: '个性化英语学习助手,提供口语练习、语法讲解、词汇积累等功能。',
avatar: '',
categoryId: 'education',
categoryName: '教育学习',
tagColor: 'amber',
usage: 9840
},
{
id: 6,
name: '短视频脚本',
description: '专业的短视频脚本创作工具,支持多种视频类型,助您打造爆款内容。',
avatar: '',
categoryId: 'writing',
categoryName: '文案创作',
tagColor: 'blue',
usage: 7680
}
])
// 分类数据(动态从 agentList 提取)
const categories = computed(() => {
const categoryMap = new Map()
// 统计各分类数量
agentList.value.forEach(agent => {
const cat = agent.categoryName || '其他'
categoryMap.set(cat, (categoryMap.get(cat) || 0) + 1)
})
// 构建分类列表
const cats = [
{ id: 'all', name: '全部智能体', count: agentList.value.length }
]
categoryMap.forEach((count, name) => {
cats.push({ id: name, name, count })
})
return cats
})
// 计算属性:过滤后的列表
const filteredAgentList = computed(() => {
let list = agentList.value
// 按分类过滤
if (activeCategory.value !== 'all') {
list = list.filter(a => a.categoryId === activeCategory.value)
list = list.filter(a => a.categoryName === activeCategory.value)
}
// 按关键词搜索(前端过滤)
if (searchKeyword.value) {
const keyword = searchKeyword.value.toLowerCase()
list = list.filter(a =>
a.name.toLowerCase().includes(keyword) ||
a.name.toLowerCase().includes(keyword) ||
a.description.toLowerCase().includes(keyword)
)
}
@@ -206,18 +160,54 @@ const filteredAgentList = computed(() => {
return list
})
// 获取智能体列表
const fetchAgentList = async () => {
loading.value = true
try {
const res = await getAgentList()
if (res.code === 0 && res.data) {
// 字段映射agentName → name, icon → avatar
agentList.value = res.data.map(item => ({
id: item.id,
agentId: item.agentId,
name: item.agentName,
description: item.description,
avatar: item.icon,
categoryName: item.categoryName || '其他',
tagColor: getTagColor(item.categoryName)
}))
}
} catch (error) {
console.error('获取智能体列表失败:', error)
message.error('获取智能体列表失败')
} finally {
loading.value = false
}
}
// 根据分类名称获取标签颜色
const getTagColor = (categoryName) => {
const colorMap = {
'文案创作': 'blue',
'数据分析': 'green',
'代码助手': 'purple',
'设计创作': 'pink',
'教育学习': 'amber'
}
return colorMap[categoryName] || 'blue'
}
// 方法
const handleCategoryChange = (categoryId) => {
activeCategory.value = categoryId
}
const handleSearch = () => {
// 搜索逻辑通过 computed 自动处理这里可以留作扩展如埋点、API请求等
// 搜索逻辑通过 computed 自动处理
}
const handleAgentClick = (agent) => {
console.log('查看智能体详情:', agent)
// TODO: 跳转到智能体详情页
}
const handleChat = (agent) => {
@@ -227,26 +217,11 @@ const handleChat = (agent) => {
const handleSendMessage = (data) => {
console.log('发送消息:', data)
// TODO: 调用 API 发送消息
}
const formatNumber = (num) => {
if (num >= 10000) {
return (num / 10000).toFixed(0) + 'w'
}
return (num / 1000).toFixed(1) + 'w'
}
// 初始化
onMounted(() => {
// 更新分类数量
categories.value.forEach(cat => {
if (cat.id === 'all') {
cat.count = agentList.value.length
} else {
cat.count = agentList.value.filter(a => a.categoryId === cat.id).length
}
})
fetchAgentList()
})
</script>