fix: 修复Python模块导入错误并优化Chat功能

- 修复 core/agents/api 模块导入问题
- 优化 ChatInput 组件交互体验
- 增强 agent_handler 和 agent_service 功能
- 调整 Chat 页面样式和布局

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-16 10:27:07 +08:00
parent 52a9d02342
commit 3a4876ab00
9 changed files with 461 additions and 19 deletions

View File

@@ -1,15 +1,137 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
const props = defineProps<{
modelValue: string
loading: boolean
agents?: { id: string | number; name: string; avatar: string }[]
mentionedAgents?: { id: string | number; name: string; avatar: string }[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: string): void
(e: 'send'): void
(e: 'triggerMention'): void
(e: 'removeMention', agentId: string | number): void
}>()
const showMentionPopup = ref(false)
const lastAtPosition = ref(-1)
const selectedIndex = ref(0)
// 过滤后的智能体列表(排除已提及的)
const filteredAgents = computed(() => {
if (!props.agents) return []
const mentionedIds = props.mentionedAgents?.map(a => a.id) || []
return props.agents.filter(a => !mentionedIds.includes(a.id))
})
// 解析消息中的 @ 提及
const parseMentions = (text: string) => {
const mentions: { id: string | number; name: string; avatar: string }[] = []
const regex = /@(\S+)/g
let match
while ((match = regex.exec(text)) !== null) {
const name = match[1]
const agent = props.agents?.find(a => a.name === name)
if (agent && !mentions.find(m => m.id === agent.id)) {
mentions.push(agent)
}
}
return mentions
}
// 监听输入
const handleInput = (e: Event) => {
const target = e.target as HTMLTextAreaElement
const value = target.value
const cursorPos = target.selectionStart
emit('update:modelValue', value)
autoResize(e)
// 检测是否输入了 @
const textBeforeCursor = value.slice(0, cursorPos)
const lastAtIndex = textBeforeCursor.lastIndexOf('@')
if (lastAtIndex !== -1) {
// 检查 @ 后面是否有空格或是否在单词中间
const textAfterAt = textBeforeCursor.slice(lastAtIndex + 1)
if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) {
showMentionPopup.value = true
lastAtPosition.value = lastAtIndex
selectedIndex.value = 0
emit('triggerMention')
} else {
showMentionPopup.value = false
}
} else {
showMentionPopup.value = false
}
}
// 选择智能体
const selectAgent = (agent: { id: string | number; name: string; avatar: string }) => {
if (!props.modelValue || lastAtPosition.value === -1) return
// 获取光标位置前的文本和后的文本
const beforeAt = props.modelValue.slice(0, lastAtPosition.value)
const afterCursor = props.modelValue.slice((document.querySelector('.chat-input-textarea') as HTMLTextAreaElement)?.selectionStart || 0)
// 替换 @xxx 为 @智能体名
const newValue = beforeAt + '@' + agent.name + ' ' + afterCursor
emit('update:modelValue', newValue)
showMentionPopup.value = false
lastAtPosition.value = -1
selectedIndex.value = 0
// 聚焦输入框
setTimeout(() => {
const textarea = document.querySelector('.chat-input-textarea') as HTMLTextAreaElement
if (textarea) {
textarea.focus()
}
}, 50)
}
// 移除提及
const removeMention = (agentId: string | number) => {
emit('removeMention', agentId)
}
const handleKeydown = (e: KeyboardEvent) => {
// @ 提及弹窗打开时处理方向键
if (showMentionPopup.value && filteredAgents.value.length > 0) {
if (e.key === 'ArrowDown') {
e.preventDefault()
selectedIndex.value = (selectedIndex.value + 1) % filteredAgents.value.length
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
selectedIndex.value = selectedIndex.value === 0
? filteredAgents.value.length - 1
: selectedIndex.value - 1
return
}
if (e.key === 'Enter') {
e.preventDefault()
const agent = filteredAgents.value[selectedIndex.value]
if (agent) {
selectAgent(agent)
}
return
}
if (e.key === 'Escape') {
e.preventDefault()
showMentionPopup.value = false
return
}
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
emit('send')
@@ -26,6 +148,26 @@ const autoResize = (e: Event) => {
<template>
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
<div class="max-w-3xl mx-auto">
<!-- 已提及的智能体显示 -->
<div v-if="mentionedAgents && mentionedAgents.length > 0" class="flex flex-wrap gap-2 mb-3">
<div
v-for="agent in mentionedAgents"
:key="agent.id"
class="inline-flex items-center gap-1.5 px-2.5 py-1.5 bg-orange-500/20 border border-orange-500/30 rounded-lg text-sm"
>
<span>{{ agent.avatar }}</span>
<span class="text-orange-400">@{{ agent.name }}</span>
<button
@click="removeMention(agent.id)"
class="ml-1 text-orange-400/60 hover:text-orange-400"
>
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
</div>
<div class="relative bg-[#12121a] rounded-2xl border border-white/[0.08] focus-within:border-orange-500/40 focus-within:shadow-[0_0_30px_rgba(249,115,22,0.08)] transition-all duration-300">
<!-- 附件按钮 -->
<button class="absolute left-4 top-1/2 -translate-y-1/2 text-white/25 hover:text-orange-400 transition-colors p-1">
@@ -37,13 +179,33 @@ const autoResize = (e: Event) => {
<!-- 输入框 -->
<textarea
:value="modelValue"
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value); autoResize($event)"
@input="handleInput"
@keydown="handleKeydown"
placeholder="发送消息..."
placeholder="输入 @ 提及智能体..."
rows="1"
class="chat-input-textarea w-full bg-transparent text-white placeholder-white/25 py-4 pl-12 pr-28 resize-none focus:outline-none text-[15px]"
></textarea>
<!-- @ 提及弹窗 -->
<div
v-if="showMentionPopup && filteredAgents.length > 0"
class="absolute left-0 bottom-full mb-2 w-64 bg-[#1a1a24] border border-white/10 rounded-xl shadow-xl overflow-hidden z-50"
>
<div class="p-2">
<div class="text-xs text-white/40 px-2 py-1">选择智能体</div>
<div
v-for="(agent, index) in filteredAgents"
:key="agent.id"
@click="selectAgent(agent)"
class="flex items-center gap-2 px-2 py-2 rounded-lg cursor-pointer transition-colors"
:class="index === selectedIndex ? 'bg-orange-500/20' : 'hover:bg-white/5'"
>
<span class="text-lg">{{ agent.avatar }}</span>
<span class="text-white text-sm">{{ agent.name }}</span>
</div>
</div>
</div>
<!-- 发送按钮 -->
<button
@click="emit('send')"

View File

@@ -46,6 +46,27 @@ const {
const messagesContainer = ref<HTMLElement | null>(null)
// @ 提及的智能体
const mentionedAgents = ref<{ id: string | number; name: string; avatar: string }[]>([])
// 触发 @ 提及
const onTriggerMention = () => {
// 可以在这里打开智能体选择弹窗,或显示提示
}
// 移除 @ 提及
const onRemoveMention = (agentId: string | number) => {
const index = mentionedAgents.value.findIndex(a => a.id === agentId)
if (index > -1) {
mentionedAgents.value.splice(index, 1)
}
// 从输入框中移除 @ 提及
const agent = chatAgents.value.find(a => a.id === agentId)
if (agent) {
inputMessage.value = inputMessage.value.replace(`@${agent.name}`, '')
}
}
// 构建 API 请求体
const buildRequestBody = (userContent: string) => {
const requestBody: any = {
@@ -61,26 +82,56 @@ const buildRequestBody = (userContent: string) => {
requestBody.session_id = currentSessionId.value
}
// 添加 @ 提及的智能体 ID
if (mentionedAgents.value.length > 0) {
requestBody.mentioned_agent_ids = mentionedAgents.value.map(a => String(a.id))
}
return requestBody
}
// 解析流式响应数据
// 支持格式: data: "content" (JSON字符串) 或 data: {"content": "xxx"} (JSON对象)
const parseStreamData = (rawData: string): string => {
console.log('[Chat] parseStreamData 原始数据:', rawData)
if (!rawData || rawData === '[DONE]') return ''
try {
const parsed = JSON.parse(rawData)
console.log('[Chat] parseStreamData 解析结果:', parsed, '类型:', typeof parsed)
// 如果解析结果是字符串JSON字符串形式直接返回
if (typeof parsed === 'string') {
return parsed
}
return parsed.content || parsed.delta?.content || ''
} catch {
// 如果是对象,尝试获取 content 或 delta.content
if (parsed && typeof parsed === 'object') {
// 兼容多种格式: content, delta.content, text, message.content
return parsed.content || parsed.delta?.content || parsed.text || parsed.message?.content || ''
}
return ''
} catch (e) {
console.error('[Chat] parseStreamData 解析错误:', e)
// 解析失败时,尝试直接返回原始数据(可能是未转义的纯文本)
if (rawData && rawData.length > 0) {
// 尝试移除首尾空格和引号
const trimmed = rawData.trim()
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
return trimmed.slice(1, -1)
}
return trimmed
}
return ''
}
}
// 处理流式响应
const handleStreamResponse = async (response: Response) => {
console.log('[Chat] handleStreamResponse 开始处理流式响应, status:', response.status)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
@@ -98,7 +149,11 @@ const handleStreamResponse = async (response: Response) => {
for (const line of lines) {
if (line.startsWith('data: ')) {
const content = parseStreamData(line.slice(6).trim())
const dataPart = line.slice(6).trim()
console.log('[Chat] 流式数据行:', line)
console.log('[Chat] 流式数据部分:', dataPart)
const content = parseStreamData(dataPart)
console.log('[Chat] 解析后内容:', content)
if (content) {
messages.value[aiMessageIndex].content += content
await nextTick()
@@ -250,6 +305,7 @@ const sendMessage = async () => {
const userContent = inputMessage.value.trim()
inputMessage.value = ''
mentionedAgents.value = []
resetInputHeight()
const userMessage = createUserMessage(userContent)
@@ -351,7 +407,11 @@ onUnmounted(() => {
v-if="currentSessionId"
v-model="inputMessage"
:loading="isLoading"
:agents="chatAgents"
:mentioned-agents="mentionedAgents"
@send="sendMessage"
@trigger-mention="onTriggerMention"
@remove-mention="onRemoveMention"
/>
</div>

View File

@@ -445,7 +445,24 @@ export function useChat() {
// 调用后端 API 创建群聊
const group = await createGroup(name, agentIds)
if (!group) {
if (group) {
// 创建成功,刷新群聊列表
await fetchGroups()
// 创建群聊会话
const session = await createSession(name)
if (session) {
saveSessionId(session.id)
// 显示群聊欢迎消息
messages.value = [
{ id: Date.now(), role: 'assistant', content: `你好!欢迎进入群聊 "${name}"${selectedAgents.value.length} 位智能体已加入。`, timestamp: new Date() }
]
// 保存欢迎消息
await saveMessage('assistant', messages.value[0].content)
}
} else {
// 如果 API 调用失败,使用本地数据
groupChats.value.unshift({
id: Date.now(),