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:
@@ -1,5 +1,5 @@
|
|||||||
"""X-Agents API Module."""
|
"""X-Agents API Module."""
|
||||||
|
|
||||||
from agents.api.routes import router
|
from .routes import router
|
||||||
|
|
||||||
__all__ = ["router"]
|
__all__ = ["router"]
|
||||||
|
|||||||
26
core/agents/api/server.py
Normal file
26
core/agents/api/server.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""X-Agents API Server."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, 'D:/Code/Project/X-Agents/core')
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from .routes import router
|
||||||
|
|
||||||
|
app = FastAPI(title="X-Agents API")
|
||||||
|
|
||||||
|
# Add CORS middleware
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Include the router
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import uvicorn
|
||||||
|
uvicorn.run(app, host="0.0.0.0", port=8001)
|
||||||
@@ -387,7 +387,14 @@ func main() {
|
|||||||
log.Println("Default tools initialized")
|
log.Println("Default tools initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4.3 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口)
|
// 4.3 初始化团队成员智能体
|
||||||
|
if err := agentService.InitTeamMembers(); err != nil {
|
||||||
|
log.Printf("Warning: Failed to init team members: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Println("Team members initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4.4 初始化 skills(已禁用自动加载,如需启用请调用 /skill/sync 接口)
|
||||||
// if err := skillService.InitSkills(); err != nil {
|
// if err := skillService.InitSkills(); err != nil {
|
||||||
// log.Printf("Warning: Failed to init skills: %v", err)
|
// log.Printf("Warning: Failed to init skills: %v", err)
|
||||||
// } else {
|
// } else {
|
||||||
@@ -405,7 +412,7 @@ func main() {
|
|||||||
toolHandler := handler.NewToolHandler(toolService)
|
toolHandler := handler.NewToolHandler(toolService)
|
||||||
mcpHandler := handler.NewMCPHandler(mcpService)
|
mcpHandler := handler.NewMCPHandler(mcpService)
|
||||||
skillHandler := handler.NewSkillHandler(skillService)
|
skillHandler := handler.NewSkillHandler(skillService)
|
||||||
agentHandler := handler.NewAgentHandler(agentService)
|
agentHandler := handler.NewAgentHandler(agentService, agentRepo)
|
||||||
memoryHandler := handler.NewMemoryHandler(memoryService)
|
memoryHandler := handler.NewMemoryHandler(memoryService)
|
||||||
sessionHandler := handler.NewSessionHandler(chatRepo, agentService)
|
sessionHandler := handler.NewSessionHandler(chatRepo, agentService)
|
||||||
|
|
||||||
@@ -590,6 +597,7 @@ func main() {
|
|||||||
{
|
{
|
||||||
agentGroup.GET("/list", agentHandler.ListAgents)
|
agentGroup.GET("/list", agentHandler.ListAgents)
|
||||||
agentGroup.POST("/create", agentHandler.CreateAgent)
|
agentGroup.POST("/create", agentHandler.CreateAgent)
|
||||||
|
agentGroup.POST("/init-team", agentHandler.InitTeamMembers)
|
||||||
agentGroup.PUT("/:id/status", agentHandler.UpdateAgentStatus)
|
agentGroup.PUT("/:id/status", agentHandler.UpdateAgentStatus)
|
||||||
agentGroup.PUT("/:id", agentHandler.UpdateAgent)
|
agentGroup.PUT("/:id", agentHandler.UpdateAgent)
|
||||||
agentGroup.DELETE("/:id", agentHandler.DeleteAgent)
|
agentGroup.DELETE("/:id", agentHandler.DeleteAgent)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# 本地开发配置
|
# 本地开发配置
|
||||||
port: "8082"
|
port: "8080"
|
||||||
jwt_secret: "dev-secret-key"
|
jwt_secret: "dev-secret-key"
|
||||||
|
|
||||||
# 数据库配置 (类型: mysql 或 sqlite)
|
# 数据库配置 (类型: mysql 或 sqlite)
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"x-agents/server/internal/model"
|
||||||
|
"x-agents/server/internal/repository"
|
||||||
"x-agents/server/internal/service"
|
"x-agents/server/internal/service"
|
||||||
|
"x-agents/server/internal/utils"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -12,22 +17,25 @@ import (
|
|||||||
// AgentHandler Agent 处理器
|
// AgentHandler Agent 处理器
|
||||||
type AgentHandler struct {
|
type AgentHandler struct {
|
||||||
agentService *service.AgentService
|
agentService *service.AgentService
|
||||||
|
agentRepo *repository.AgentRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAgentHandler 创建 Agent 处理器
|
// NewAgentHandler 创建 Agent 处理器
|
||||||
func NewAgentHandler(agentService *service.AgentService) *AgentHandler {
|
func NewAgentHandler(agentService *service.AgentService, agentRepo *repository.AgentRepository) *AgentHandler {
|
||||||
return &AgentHandler{
|
return &AgentHandler{
|
||||||
agentService: agentService,
|
agentService: agentService,
|
||||||
|
agentRepo: agentRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatRequest 对话请求
|
// ChatRequest 对话请求
|
||||||
type ChatRequest struct {
|
type ChatRequest struct {
|
||||||
AgentID string `json:"agent_id" binding:"required"` // 字符串类型
|
AgentID string `json:"agent_id"` // 字符串类型,支持 UUID,可为空(当使用 mentioned_agent_ids 时)
|
||||||
Message string `json:"message" binding:"required"`
|
Message string `json:"message" binding:"required"`
|
||||||
SessionID string `json:"session_id"`
|
SessionID string `json:"session_id"`
|
||||||
ModelID string `json:"model_id"`
|
ModelID string `json:"model_id"`
|
||||||
UseXBot bool `json:"use_xbot"`
|
UseXBot bool `json:"use_xbot"`
|
||||||
|
MentionedAgentIDs []string `json:"mentioned_agent_ids"` // @ 提及的智能体 ID 列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChatResponse 对话响应
|
// ChatResponse 对话响应
|
||||||
@@ -131,6 +139,29 @@ func (h *AgentHandler) ChatStream(c *gin.Context) {
|
|||||||
// 直接使用字符串类型的 agent_id,支持 UUID
|
// 直接使用字符串类型的 agent_id,支持 UUID
|
||||||
agentID := req.AgentID
|
agentID := req.AgentID
|
||||||
|
|
||||||
|
// 优先使用前端传递的 mentioned_agent_ids
|
||||||
|
if len(req.MentionedAgentIDs) > 0 {
|
||||||
|
// 如果有多个 @ 提及,使用第一个
|
||||||
|
mentionedAgentID := req.MentionedAgentIDs[0]
|
||||||
|
log.Printf("[ChatStream] Using mentioned_agent_ids: %v", req.MentionedAgentIDs)
|
||||||
|
agentID = mentionedAgentID
|
||||||
|
// 清理消息,移除 @ 提及
|
||||||
|
mentionParser := utils.NewMentionParser()
|
||||||
|
req.Message = mentionParser.RemoveMentions(req.Message)
|
||||||
|
} else if agentID == "" {
|
||||||
|
// 兼容:解析消息中的 @ 提及(备用方案)
|
||||||
|
mentionParser := utils.NewMentionParser()
|
||||||
|
mentions := mentionParser.ParseMentions(req.Message)
|
||||||
|
if len(mentions) > 0 {
|
||||||
|
mentionedAgent := h.findAgentByName(mentions[0])
|
||||||
|
if mentionedAgent != nil {
|
||||||
|
log.Printf("[ChatStream] Detected @mention: %s, routing to agent: %s", mentions[0], mentionedAgent.ID)
|
||||||
|
agentID = mentionedAgent.ID
|
||||||
|
}
|
||||||
|
req.Message = mentionParser.RemoveMentions(req.Message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 构建 SSE 流
|
// 构建 SSE 流
|
||||||
c.Header("Content-Type", "text/event-stream")
|
c.Header("Content-Type", "text/event-stream")
|
||||||
c.Header("Cache-Control", "no-cache")
|
c.Header("Cache-Control", "no-cache")
|
||||||
@@ -144,6 +175,37 @@ func (h *AgentHandler) ChatStream(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findAgentByName 根据用户名查找智能体
|
||||||
|
func (h *AgentHandler) findAgentByName(name string) *model.Agent {
|
||||||
|
log.Printf("[findAgentByName] Searching for agent: %s, agentRepo: %v", name, h.agentRepo)
|
||||||
|
|
||||||
|
if h.agentRepo == nil {
|
||||||
|
log.Printf("[findAgentByName] ERROR: agentRepo is nil!")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先尝试精确匹配
|
||||||
|
agents, err := h.agentRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, agent := range agents {
|
||||||
|
if agent.Name == name {
|
||||||
|
return &agent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 再尝试模糊匹配(忽略大小写)
|
||||||
|
for _, agent := range agents {
|
||||||
|
if strings.Contains(strings.ToLower(agent.Name), strings.ToLower(name)) {
|
||||||
|
return &agent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// TeamChatRequest 多智能体群聊请求
|
// TeamChatRequest 多智能体群聊请求
|
||||||
type TeamChatRequest struct {
|
type TeamChatRequest struct {
|
||||||
SupervisorAgentID int `json:"supervisor_agent_id" binding:"required"`
|
SupervisorAgentID int `json:"supervisor_agent_id" binding:"required"`
|
||||||
@@ -236,6 +298,30 @@ func (h *AgentHandler) CreateAgent(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, result)
|
c.JSON(http.StatusOK, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InitTeamMembersResponse 初始化团队成员响应
|
||||||
|
type InitTeamMembersResponse struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitTeamMembers 初始化团队成员智能体
|
||||||
|
// @Summary 初始化团队成员智能体
|
||||||
|
// @Tags 智能体管理
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} InitTeamMembersResponse
|
||||||
|
// @Router /api/agent/init-team [post]
|
||||||
|
func (h *AgentHandler) InitTeamMembers(c *gin.Context) {
|
||||||
|
if err := h.agentService.InitTeamMembers(); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, InitTeamMembersResponse{
|
||||||
|
Message: "Team members initialized successfully",
|
||||||
|
Count: 1, // 小荣
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ListAgents 获取智能体列表
|
// ListAgents 获取智能体列表
|
||||||
// @Summary 获取智能体列表
|
// @Summary 获取智能体列表
|
||||||
// @Tags 智能体管理
|
// @Tags 智能体管理
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) {
|
|||||||
log.Printf("[AgentService] Sending to Python: model_id=%s, api_key=%s, base_url=%s, provider=%s, model=%s",
|
log.Printf("[AgentService] Sending to Python: model_id=%s, api_key=%s, base_url=%s, provider=%s, model=%s",
|
||||||
req.ModelID, apiKeyPreview, req.BaseURL, req.ModelProvider, req.ModelName)
|
req.ModelID, apiKeyPreview, req.BaseURL, req.ModelProvider, req.ModelName)
|
||||||
|
|
||||||
url := fmt.Sprintf("%s/api/v1/agent/chat", s.pythonURL)
|
url := fmt.Sprintf("%s/agent/chat", s.pythonURL)
|
||||||
|
|
||||||
jsonData, err := json.Marshal(req)
|
jsonData, err := json.Marshal(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -155,7 +155,7 @@ func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) {
|
|||||||
|
|
||||||
// TeamChat 多智能体群聊
|
// TeamChat 多智能体群聊
|
||||||
func (s *AgentService) TeamChat(req TeamChatRequest) (*TeamChatResponse, error) {
|
func (s *AgentService) TeamChat(req TeamChatRequest) (*TeamChatResponse, error) {
|
||||||
url := fmt.Sprintf("%s/api/v1/agent/team/chat", s.pythonURL)
|
url := fmt.Sprintf("%s/agent/team/chat", s.pythonURL)
|
||||||
|
|
||||||
// 设置默认策略
|
// 设置默认策略
|
||||||
if req.Strategy == "" {
|
if req.Strategy == "" {
|
||||||
@@ -233,7 +233,7 @@ func (s *AgentService) ChatStream(c interface{}, agentID string, message, sessio
|
|||||||
log.Printf("[ChatStream] modelID is empty or modelRepo is nil: modelID=%s, modelRepo=%v", modelID, s.modelRepo != nil)
|
log.Printf("[ChatStream] modelID is empty or modelRepo is nil: modelID=%s, modelRepo=%v", modelID, s.modelRepo != nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
streamURL := fmt.Sprintf("%s/api/v1/agent/chat/stream", s.pythonURL)
|
streamURL := fmt.Sprintf("%s/agent/chat/stream", s.pythonURL)
|
||||||
|
|
||||||
jsonData, err := json.Marshal(reqBody)
|
jsonData, err := json.Marshal(reqBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -365,6 +365,89 @@ func (s *AgentService) CreateAgent(req CreateAgentRequest, userID int) (*CreateA
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TeamMemberInitRequest 团队成员初始化请求
|
||||||
|
type TeamMemberInitRequest struct {
|
||||||
|
Name string
|
||||||
|
Description string
|
||||||
|
Avatar string
|
||||||
|
Skills []string
|
||||||
|
RoleDescription string
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitTeamMembers 初始化团队成员智能体
|
||||||
|
func (s *AgentService) InitTeamMembers() error {
|
||||||
|
if s.agentRepo == nil {
|
||||||
|
log.Printf("[AgentService] InitTeamMembers: agentRepo is nil!")
|
||||||
|
return fmt.Errorf("agent repository not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 骚人开发组团队成员配置
|
||||||
|
teamMembers := []TeamMemberInitRequest{
|
||||||
|
{
|
||||||
|
Name: "小荣",
|
||||||
|
Description: "前端开发工程师 - 骚人开发组成员",
|
||||||
|
Avatar: "👨💻",
|
||||||
|
Skills: []string{"Vue 3", "TypeScript", "Element Plus", "Tailwind CSS"},
|
||||||
|
RoleDescription: `你叫小荣,是骚人开发组的前端开发工程师。你细心认真,善于沟通。
|
||||||
|
|
||||||
|
技能专长:
|
||||||
|
- Vue 3 框架开发
|
||||||
|
- TypeScript 类型系统
|
||||||
|
- Element Plus 组件库
|
||||||
|
- Tailwind CSS 样式框架
|
||||||
|
|
||||||
|
性格特点:
|
||||||
|
- 细心认真,注重代码质量
|
||||||
|
- 善于与团队成员沟通协作
|
||||||
|
- 积极解决前端技术难题`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在同名智能体
|
||||||
|
for _, member := range teamMembers {
|
||||||
|
existingAgents, err := s.agentRepo.FindAll()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[AgentService] InitTeamMembers: failed to list agents: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
exists := false
|
||||||
|
for _, a := range existingAgents {
|
||||||
|
if a.Name == member.Name {
|
||||||
|
exists = true
|
||||||
|
log.Printf("[AgentService] InitTeamMembers: agent %s already exists, skipping", member.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
// 创建智能体
|
||||||
|
agent := &model.Agent{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
Name: member.Name,
|
||||||
|
Description: member.Description,
|
||||||
|
OwnerID: "1", // 系统管理员
|
||||||
|
Avatar: member.Avatar,
|
||||||
|
Skills: member.Skills,
|
||||||
|
RoleDescription: member.RoleDescription,
|
||||||
|
ModelProvider: "anthropic",
|
||||||
|
ModelName: "claude-sonnet-4-20250514",
|
||||||
|
IsActive: true,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.agentRepo.Create(agent); err != nil {
|
||||||
|
log.Printf("[AgentService] InitTeamMembers: failed to create agent %s: %v", member.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf("[AgentService] InitTeamMembers: created agent %s (ID: %s)", member.Name, agent.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ListAgentsResponse 获取智能体列表响应
|
// ListAgentsResponse 获取智能体列表响应
|
||||||
type ListAgentsResponse struct {
|
type ListAgentsResponse struct {
|
||||||
Agents []interface{} `json:"agents"`
|
Agents []interface{} `json:"agents"`
|
||||||
|
|||||||
@@ -1,15 +1,137 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
loading: boolean
|
loading: boolean
|
||||||
|
agents?: { id: string | number; name: string; avatar: string }[]
|
||||||
|
mentionedAgents?: { id: string | number; name: string; avatar: string }[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
(e: 'send'): 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) => {
|
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) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
emit('send')
|
emit('send')
|
||||||
@@ -26,6 +148,26 @@ const autoResize = (e: Event) => {
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
|
<div class="p-5 border-t border-white/[0.06] bg-[#0c0c0f]/60 backdrop-blur-xl">
|
||||||
<div class="max-w-3xl mx-auto">
|
<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">
|
<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">
|
<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
|
<textarea
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value); autoResize($event)"
|
@input="handleInput"
|
||||||
@keydown="handleKeydown"
|
@keydown="handleKeydown"
|
||||||
placeholder="发送消息..."
|
placeholder="输入 @ 提及智能体..."
|
||||||
rows="1"
|
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]"
|
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>
|
></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
|
<button
|
||||||
@click="emit('send')"
|
@click="emit('send')"
|
||||||
|
|||||||
@@ -46,6 +46,27 @@ const {
|
|||||||
|
|
||||||
const messagesContainer = ref<HTMLElement | null>(null)
|
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 请求体
|
// 构建 API 请求体
|
||||||
const buildRequestBody = (userContent: string) => {
|
const buildRequestBody = (userContent: string) => {
|
||||||
const requestBody: any = {
|
const requestBody: any = {
|
||||||
@@ -61,26 +82,56 @@ const buildRequestBody = (userContent: string) => {
|
|||||||
requestBody.session_id = currentSessionId.value
|
requestBody.session_id = currentSessionId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加 @ 提及的智能体 ID
|
||||||
|
if (mentionedAgents.value.length > 0) {
|
||||||
|
requestBody.mentioned_agent_ids = mentionedAgents.value.map(a => String(a.id))
|
||||||
|
}
|
||||||
|
|
||||||
return requestBody
|
return requestBody
|
||||||
}
|
}
|
||||||
|
|
||||||
// 解析流式响应数据
|
// 解析流式响应数据
|
||||||
|
// 支持格式: data: "content" (JSON字符串) 或 data: {"content": "xxx"} (JSON对象)
|
||||||
const parseStreamData = (rawData: string): string => {
|
const parseStreamData = (rawData: string): string => {
|
||||||
|
console.log('[Chat] parseStreamData 原始数据:', rawData)
|
||||||
|
|
||||||
if (!rawData || rawData === '[DONE]') return ''
|
if (!rawData || rawData === '[DONE]') return ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(rawData)
|
const parsed = JSON.parse(rawData)
|
||||||
|
console.log('[Chat] parseStreamData 解析结果:', parsed, '类型:', typeof parsed)
|
||||||
|
|
||||||
|
// 如果解析结果是字符串(JSON字符串形式),直接返回
|
||||||
if (typeof parsed === 'string') {
|
if (typeof parsed === 'string') {
|
||||||
return parsed
|
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 ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理流式响应
|
// 处理流式响应
|
||||||
const handleStreamResponse = async (response: Response) => {
|
const handleStreamResponse = async (response: Response) => {
|
||||||
|
console.log('[Chat] handleStreamResponse 开始处理流式响应, status:', response.status)
|
||||||
const reader = response.body.getReader()
|
const reader = response.body.getReader()
|
||||||
const decoder = new TextDecoder('utf-8')
|
const decoder = new TextDecoder('utf-8')
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
@@ -98,7 +149,11 @@ const handleStreamResponse = async (response: Response) => {
|
|||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith('data: ')) {
|
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) {
|
if (content) {
|
||||||
messages.value[aiMessageIndex].content += content
|
messages.value[aiMessageIndex].content += content
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -250,6 +305,7 @@ const sendMessage = async () => {
|
|||||||
|
|
||||||
const userContent = inputMessage.value.trim()
|
const userContent = inputMessage.value.trim()
|
||||||
inputMessage.value = ''
|
inputMessage.value = ''
|
||||||
|
mentionedAgents.value = []
|
||||||
resetInputHeight()
|
resetInputHeight()
|
||||||
|
|
||||||
const userMessage = createUserMessage(userContent)
|
const userMessage = createUserMessage(userContent)
|
||||||
@@ -351,7 +407,11 @@ onUnmounted(() => {
|
|||||||
v-if="currentSessionId"
|
v-if="currentSessionId"
|
||||||
v-model="inputMessage"
|
v-model="inputMessage"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:agents="chatAgents"
|
||||||
|
:mentioned-agents="mentionedAgents"
|
||||||
@send="sendMessage"
|
@send="sendMessage"
|
||||||
|
@trigger-mention="onTriggerMention"
|
||||||
|
@remove-mention="onRemoveMention"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -445,7 +445,24 @@ export function useChat() {
|
|||||||
// 调用后端 API 创建群聊
|
// 调用后端 API 创建群聊
|
||||||
const group = await createGroup(name, agentIds)
|
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 调用失败,使用本地数据
|
// 如果 API 调用失败,使用本地数据
|
||||||
groupChats.value.unshift({
|
groupChats.value.unshift({
|
||||||
id: Date.now(),
|
id: Date.now(),
|
||||||
|
|||||||
Reference in New Issue
Block a user