Compare commits

...

2 Commits

Author SHA1 Message Date
8062144001 feat: 更新 Chat 页面
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:22:47 +08:00
25eb277a2a feat: Agent 服务日志功能和后端更新
- agent/main.py: 添加日志记录功能
- agent/llm: 更新 anthropic, openai, factory
- agent/core/agent.py: 更新
- server: agent_handler, agent_service, chat_service 更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 17:22:40 +08:00
11 changed files with 415 additions and 32 deletions

View File

@@ -1,6 +1,7 @@
""" """
Agent Core - 单智能体核心 Agent Core - 单智能体核心
""" """
import logging
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from pydantic import BaseModel from pydantic import BaseModel
from app.agent.memory.manager import MemoryManager from app.agent.memory.manager import MemoryManager
@@ -8,6 +9,8 @@ from app.agent.skills.router import SkillRouter
from app.agent.skills.executor import SkillExecutor from app.agent.skills.executor import SkillExecutor
from app.agent.llm.factory import LLMFactory from app.agent.llm.factory import LLMFactory
logger = logging.getLogger("agent.core")
class AgentConfig(BaseModel): class AgentConfig(BaseModel):
"""智能体配置""" """智能体配置"""
@@ -16,6 +19,8 @@ class AgentConfig(BaseModel):
role_description: str role_description: str
model_provider: str = "openai" model_provider: str = "openai"
model_name: str = "gpt-4" model_name: str = "gpt-4"
api_key: Optional[str] = None # API Key可选用于覆盖默认配置
base_url: Optional[str] = None # Base URL可选用于覆盖默认配置
skills: List[int] = [] # 技能 ID 列表 skills: List[int] = [] # 技能 ID 列表
knowledge_base_ids: List[int] = [] knowledge_base_ids: List[int] = []
timeout: int = 60 timeout: int = 60
@@ -36,7 +41,12 @@ class AgentCore:
def __init__(self, config: AgentConfig): def __init__(self, config: AgentConfig):
self.config = config self.config = config
self.llm = LLMFactory.create(config.model_provider, config.model_name)
# 记录 LLM 初始化信息
api_key_info = f"{config.api_key[:10]}..." if config.api_key else "None"
logger.info(f"初始化 AgentCore: name={config.name}, provider={config.model_provider}, model={config.model_name}, api_key={api_key_info}, base_url={config.base_url}")
self.llm = LLMFactory.create(config.model_provider, config.model_name, config.api_key, config.base_url)
self.memory = MemoryManager(config.id) self.memory = MemoryManager(config.id)
self.skill_router = SkillRouter(config.skills) self.skill_router = SkillRouter(config.skills)
self.skill_executor = SkillExecutor() self.skill_executor = SkillExecutor()

View File

@@ -2,17 +2,17 @@
Anthropic LLM 实现 Anthropic LLM 实现
""" """
import os import os
from typing import Dict, Any, List from typing import Dict, Any, List, Optional
from anthropic import AsyncAnthropic from anthropic import AsyncAnthropic
class AnthropicLLM: class AnthropicLLM:
"""Anthropic Claude LLM""" """Anthropic Claude LLM"""
def __init__(self, model_name: str = "claude-3-sonnet-20240229"): def __init__(self, model_name: str = "claude-3-sonnet-20240229", api_key: Optional[str] = None):
self.model_name = model_name self.model_name = model_name
self.client = AsyncAnthropic( self.client = AsyncAnthropic(
api_key=os.getenv("ANTHROPIC_API_KEY", "") api_key=api_key or os.getenv("ANTHROPIC_API_KEY", "")
) )
async def decide(self, prompt: str) -> Dict[str, Any]: async def decide(self, prompt: str) -> Dict[str, Any]:

View File

@@ -1,7 +1,7 @@
""" """
LLM Factory - LLM 工厂类 LLM Factory - LLM 工厂类
""" """
from typing import Dict, Any from typing import Optional
from app.agent.llm.openai import OpenAILLM from app.agent.llm.openai import OpenAILLM
from app.agent.llm.anthropic import AnthropicLLM from app.agent.llm.anthropic import AnthropicLLM
@@ -10,21 +10,23 @@ class LLMFactory:
"""LLM 工厂类""" """LLM 工厂类"""
@staticmethod @staticmethod
def create(provider: str, model_name: str): def create(provider: str, model_name: str, api_key: Optional[str] = None, base_url: Optional[str] = None):
""" """
创建 LLM 实例 创建 LLM 实例
Args: Args:
provider: 模型提供商 (openai/anthropic) provider: 模型提供商 (openai/anthropic)
model_name: 模型名称 model_name: 模型名称
api_key: API Key可选
base_url: Base URL可选
Returns: Returns:
LLM 实例 LLM 实例
""" """
if provider.lower() == "openai": if provider.lower() == "openai":
return OpenAILLM(model_name) return OpenAILLM(model_name, api_key, base_url)
elif provider.lower() == "anthropic": elif provider.lower() == "anthropic":
return AnthropicLLM(model_name) return AnthropicLLM(model_name, api_key)
else: else:
# 默认使用 OpenAI # 默认使用 OpenAI
return OpenAILLM(model_name) return OpenAILLM(model_name, api_key, base_url)

View File

@@ -2,18 +2,31 @@
OpenAI LLM 实现 OpenAI LLM 实现
""" """
import os import os
import logging
from typing import Dict, Any, List, Optional from typing import Dict, Any, List, Optional
from openai import AsyncOpenAI from openai import AsyncOpenAI
logger = logging.getLogger("llm.openai")
class OpenAILLM: class OpenAILLM:
"""OpenAI LLM""" """OpenAI LLM"""
def __init__(self, model_name: str = "gpt-4"): def __init__(self, model_name: str = "gpt-4", api_key: Optional[str] = None, base_url: Optional[str] = None):
self.model_name = model_name self.model_name = model_name
# 优先使用传入的参数,否则使用环境变量
self.api_key = api_key or os.getenv("OPENAI_API_KEY", "")
self.base_url = base_url or os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
api_key_info = f"{self.api_key[:10]}..." if self.api_key else "None"
logger.info(f"初始化 OpenAI LLM: model={model_name}, api_key={api_key_info}, base_url={self.base_url}")
if not self.api_key:
logger.warning("⚠️ WARNING: No API key provided!")
self.client = AsyncOpenAI( self.client = AsyncOpenAI(
api_key=os.getenv("OPENAI_API_KEY", ""), api_key=self.api_key,
base_url=os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1") base_url=self.base_url
) )
async def decide(self, prompt: str) -> Dict[str, Any]: async def decide(self, prompt: str) -> Dict[str, Any]:

View File

@@ -2,7 +2,10 @@
FastAPI Agent Engine Server FastAPI Agent Engine Server
""" """
import os import os
import sys
import time import time
import logging
from datetime import datetime
from typing import Optional from typing import Optional
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
@@ -12,6 +15,90 @@ from app.agent.core import AgentCore, Supervisor, AgentConfig
from app.agent.llm import LLMFactory from app.agent.llm import LLMFactory
# 日志目录 - 放在 server/logs 下
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "server", "logs", datetime.now().strftime("%Y-%m-%d"))
os.makedirs(LOG_DIR, exist_ok=True)
# 成功/失败日志文件
today = datetime.now().strftime("%Y-%m-%d")
success_log_file = os.path.join(LOG_DIR, f"python_success.log")
failure_log_file = os.path.join(LOG_DIR, f"python_failure.log")
def setup_logging():
"""配置日志系统"""
log_level = os.getenv("LOG_LEVEL", "INFO")
# 创建格式化器
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
datefmt='%Y-%m-%d %H:%M:%S'
)
# 控制台处理器
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(formatter)
# 成功日志文件处理器
success_handler = logging.FileHandler(success_log_file, encoding='utf-8')
success_handler.setFormatter(formatter)
success_handler.setLevel(logging.INFO)
# 失败日志文件处理器
failure_handler = logging.FileHandler(failure_log_file, encoding='utf-8')
failure_handler.setFormatter(formatter)
failure_handler.setLevel(logging.WARNING)
# 根日志配置
root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, log_level))
root_logger.addHandler(console_handler)
root_logger.addHandler(success_handler)
root_logger.addHandler(failure_handler)
return root_logger
# 成功日志记录器(只记录 INFO 级别到成功日志)
class SuccessLogger:
"""成功日志记录器"""
@staticmethod
def log(message: str):
"""记录成功日志"""
logger = logging.getLogger("success")
logger.setLevel(logging.INFO)
handler = logging.FileHandler(success_log_file, encoding='utf-8')
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(handler)
logger.info(message)
# 同时输出到控制台
print(f"{message}")
# 失败日志记录器
class FailureLogger:
"""失败日志记录器"""
@staticmethod
def log(message: str, error: str = ""):
"""记录失败日志"""
logger = logging.getLogger("failure")
logger.setLevel(logging.WARNING)
handler = logging.FileHandler(failure_log_file, encoding='utf-8')
handler.setFormatter(logging.Formatter('%(asctime)s - %(message)s - %(error)s', datefmt='%Y-%m-%d %H:%M:%S'))
logger.addHandler(handler)
full_message = f"{message} - Error: {error}" if error else message
logger.warning(full_message)
# 同时输出到控制台
print(f"{full_message}")
logger = setup_logging()
app = FastAPI(title="X-Agents Python Engine", version="1.0.0") app = FastAPI(title="X-Agents Python Engine", version="1.0.0")
# CORS # CORS
@@ -32,6 +119,12 @@ class ChatRequest(BaseModel):
message: str message: str
user_id: int = 1 user_id: int = 1
session_id: Optional[str] = None session_id: Optional[str] = None
# 模型参数(可选,如果传了就使用,否则用智能体配置的默认模型)
model_id: Optional[str] = None
model_name: Optional[str] = None
model_provider: Optional[str] = None
api_key: Optional[str] = None
base_url: Optional[str] = None
class TeamChatRequest(BaseModel): class TeamChatRequest(BaseModel):
@@ -76,7 +169,7 @@ _mock_agents = {
} }
def get_agent_config(agent_id: int) -> AgentConfig: def get_agent_config(agent_id: int, api_key: str = None, base_url: str = None) -> AgentConfig:
"""获取智能体配置""" """获取智能体配置"""
agent_data = _mock_agents.get(agent_id) agent_data = _mock_agents.get(agent_id)
if not agent_data: if not agent_data:
@@ -88,6 +181,8 @@ def get_agent_config(agent_id: int) -> AgentConfig:
role_description=agent_data["role_description"], role_description=agent_data["role_description"],
model_provider=agent_data["model_provider"], model_provider=agent_data["model_provider"],
model_name=agent_data["model_name"], model_name=agent_data["model_name"],
api_key=api_key,
base_url=base_url,
skills=agent_data.get("skills", []) skills=agent_data.get("skills", [])
) )
@@ -109,16 +204,42 @@ async def chat(request: ChatRequest):
""" """
单智能体对话 单智能体对话
""" """
chat_logger = logging.getLogger("agent.chat")
# 打印请求参数(隐藏 api_key 敏感信息)
api_key_preview = f"{request.api_key[:10]}..." if request.api_key else "None"
chat_logger.info(f"========== 收到聊天请求 ==========")
chat_logger.info(f"agent_id: {request.agent_id}")
chat_logger.info(f"model_id: {request.model_id}")
chat_logger.info(f"model_provider: {request.model_provider}")
chat_logger.info(f"model_name: {request.model_name}")
chat_logger.info(f"api_key: {api_key_preview}")
chat_logger.info(f"base_url: {request.base_url}")
chat_logger.info(f"message: {request.message[:50]}...")
start_time = time.time() start_time = time.time()
# 获取智能体配置 # 获取智能体配置
try: try:
config = get_agent_config(request.agent_id) config = get_agent_config(request.agent_id, request.api_key, request.base_url)
except HTTPException: chat_logger.info(f"Agent config loaded: provider={config.model_provider}, model={config.model_name}")
except HTTPException as e:
FailureLogger.log(f"Agent not found: agent_id={request.agent_id}", str(e))
chat_logger.error(f"Agent not found: {e}")
raise raise
except Exception as e: except Exception as e:
FailureLogger.log(f"Error loading config: agent_id={request.agent_id}", str(e))
chat_logger.error(f"Error loading config: {e}")
raise HTTPException(status_code=400, detail=str(e)) raise HTTPException(status_code=400, detail=str(e))
# 如果请求中指定了模型,覆盖智能体的默认配置
if request.model_provider:
config.model_provider = request.model_provider
if request.model_name:
config.model_name = request.model_name
chat_logger.info(f"Final LLM config: provider={config.model_provider}, model={config.model_name}, api_key={config.api_key[:10] if config.api_key else 'None'}..., base_url={config.base_url}")
# 创建智能体实例 # 创建智能体实例
agent = AgentCore(config) agent = AgentCore(config)
@@ -129,10 +250,15 @@ async def chat(request: ChatRequest):
try: try:
result = await agent.run(request.message, request.user_id, session_id) result = await agent.run(request.message, request.user_id, session_id)
except Exception as e: except Exception as e:
FailureLogger.log(f"Agent execution failed: agent_id={request.agent_id}, message={request.message[:30]}", str(e))
chat_logger.error(f"Agent execution error: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
duration_ms = int((time.time() - start_time) * 1000) duration_ms = int((time.time() - start_time) * 1000)
# 记录成功日志
SuccessLogger.log(f"Chat success: agent_id={request.agent_id}, duration={duration_ms}ms, message={request.message[:30]}...")
return ChatResponse( return ChatResponse(
agent_id=request.agent_id, agent_id=request.agent_id,
response=result.content, response=result.content,

View File

@@ -296,7 +296,7 @@ func main() {
toolService := service.NewToolService(toolRepo) toolService := service.NewToolService(toolRepo)
mcpService := service.NewMCPService(mcpRepo) mcpService := service.NewMCPService(mcpRepo)
skillService := service.NewSkillService(skillRepo) skillService := service.NewSkillService(skillRepo)
agentService := service.NewAgentService(cfg.PythonServiceURL) agentService := service.NewAgentService(cfg.PythonServiceURL, modelRepo)
memoryService := service.NewMemoryService(agentRepo) memoryService := service.NewMemoryService(agentRepo)
// 4.2 初始化默认工具 // 4.2 初始化默认工具

View File

@@ -25,6 +25,7 @@ type ChatRequest struct {
AgentID int `json:"agent_id" binding:"required"` AgentID int `json:"agent_id" binding:"required"`
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"`
} }
// ChatResponse 对话响应 // ChatResponse 对话响应
@@ -54,6 +55,7 @@ func (h *AgentHandler) Chat(c *gin.Context) {
Message: req.Message, Message: req.Message,
UserID: userID, UserID: userID,
SessionID: req.SessionID, SessionID: req.SessionID,
ModelID: req.ModelID,
} }
result, err := h.agentService.Chat(pythonReq) result, err := h.agentService.Chat(pythonReq)

View File

@@ -105,6 +105,7 @@ type AgentRequest struct {
AgentID string `json:"agent_id" binding:"required"` AgentID string `json:"agent_id" binding:"required"`
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"`
Context map[string]interface{} `json:"context"` Context map[string]interface{} `json:"context"`
} }

View File

@@ -5,16 +5,24 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"log"
"net/http" "net/http"
"time" "time"
"x-agents/server/internal/repository"
) )
// AgentChatRequest Python Agent 对话请求 // AgentChatRequest Python Agent 对话请求
type AgentChatRequest struct { type AgentChatRequest struct {
AgentID int `json:"agent_id"` AgentID int `json:"agent_id"`
Message string `json:"message"` Message string `json:"message"`
UserID int `json:"user_id"` UserID int `json:"user_id"`
SessionID string `json:"session_id,omitempty"` SessionID string `json:"session_id,omitempty"`
ModelID string `json:"model_id,omitempty"`
ModelName string `json:"model_name,omitempty"`
ModelProvider string `json:"model_provider,omitempty"`
APIKey string `json:"api_key,omitempty"`
BaseURL string `json:"base_url,omitempty"`
} }
// AgentChatResponse Python Agent 对话响应 // AgentChatResponse Python Agent 对话响应
@@ -51,20 +59,55 @@ type TeamChatResponse struct {
type AgentService struct { type AgentService struct {
pythonURL string pythonURL string
client *http.Client client *http.Client
modelRepo *repository.ModelRepository
} }
// NewAgentService 创建 Agent 服务 // NewAgentService 创建 Agent 服务
func NewAgentService(pythonURL string) *AgentService { func NewAgentService(pythonURL string, modelRepo *repository.ModelRepository) *AgentService {
return &AgentService{ return &AgentService{
pythonURL: pythonURL, pythonURL: pythonURL,
client: &http.Client{ client: &http.Client{
Timeout: 120 * time.Second, // Agent 可能需要较长时间 Timeout: 120 * time.Second, // Agent 可能需要较长时间
}, },
modelRepo: modelRepo,
} }
} }
// Chat 单智能体对话 // Chat 单智能体对话
func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) { func (s *AgentService) Chat(req AgentChatRequest) (*AgentChatResponse, error) {
// 如果传入了 model_id查询模型配置获取 api_key 和 base_url
log.Printf("[AgentService] Chat called, model_id: %s, modelRepo: %v", req.ModelID, s.modelRepo != nil)
if req.ModelID != "" && s.modelRepo != nil {
model, err := s.modelRepo.FindByID(req.ModelID)
if err != nil {
log.Printf("[AgentService] Error finding model: %v", err)
} else if model != nil {
log.Printf("[AgentService] Found model: id=%s, provider=%s, model=%s, base_url=%s, api_key_len=%d",
model.ID, model.Provider, model.Model, model.BaseURL, len(model.APIKey))
req.APIKey = model.APIKey
req.BaseURL = model.BaseURL
req.ModelProvider = model.Provider
req.ModelName = model.Model
log.Printf("[AgentService] Set req.APIKey=%s, req.BaseURL=%s", req.APIKey[:10]+"...", req.BaseURL)
} else {
log.Printf("[AgentService] Model not found for id: %s", req.ModelID)
}
} else if s.modelRepo == nil {
log.Printf("[AgentService] WARNING: modelRepo is nil!")
}
// 打印传给 Python 的请求内容
apiKeyPreview := ""
if req.APIKey != "" {
apiKeyPreview = req.APIKey
if len(apiKeyPreview) > 10 {
apiKeyPreview = apiKeyPreview[:10] + "..."
}
}
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)
url := fmt.Sprintf("%s/agent/chat", s.pythonURL) url := fmt.Sprintf("%s/agent/chat", s.pythonURL)
jsonData, err := json.Marshal(req) jsonData, err := json.Marshal(req)

View File

@@ -16,13 +16,15 @@ import (
type ChatService struct { type ChatService struct {
pythonURL string pythonURL string
agentRepo *repository.AgentRepository agentRepo *repository.AgentRepository
modelRepo *repository.ModelRepository
} }
func NewChatService(pythonURL string, agentRepo *repository.AgentRepository) *ChatService { func NewChatService(pythonURL string, agentRepo *repository.AgentRepository, modelRepo *repository.ModelRepository) *ChatService {
return &ChatService{ return &ChatService{
pythonURL: pythonURL, pythonURL: pythonURL,
agentRepo: agentRepo, agentRepo: agentRepo,
modelRepo: modelRepo,
} }
} }
@@ -30,9 +32,19 @@ type ChatRequest struct {
AgentID string `json:"agent_id"` AgentID string `json:"agent_id"`
Message string `json:"message"` Message string `json:"message"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
ModelID string `json:"model_id"`
Context map[string]interface{} `json:"context"` Context map[string]interface{} `json:"context"`
} }
// ModelConfig 模型配置,用于传递给 Python 服务
type ModelConfig struct {
Provider string `json:"provider"`
Model string `json:"model"`
APIKey string `json:"api_key"`
BaseURL string `json:"base_url"`
APIEndpoint string `json:"api_endpoint"`
}
type ChatResponse struct { type ChatResponse struct {
Reply string `json:"reply"` Reply string `json:"reply"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
@@ -59,14 +71,40 @@ func (s *ChatService) Chat(ctx context.Context, userID string, req model.AgentRe
sessionID = uuid.New().String() sessionID = uuid.New().String()
} }
// 4. 调用 Python 服务 // 4. 如果提供了 ModelID获取模型配置
var modelConfig *ModelConfig
if req.ModelID != "" {
modelInfo, err := s.modelRepo.FindByID(req.ModelID)
if err != nil {
return nil, fmt.Errorf("model not found: %w", err)
}
modelConfig = &ModelConfig{
Provider: modelInfo.Provider,
Model: modelInfo.Model,
APIKey: modelInfo.APIKey,
BaseURL: modelInfo.BaseURL,
APIEndpoint: modelInfo.APIEndpoint,
}
}
// 5. 调用 Python 服务
pythonReq := ChatRequest{ pythonReq := ChatRequest{
AgentID: req.AgentID, AgentID: req.AgentID,
Message: req.Message, Message: req.Message,
SessionID: sessionID, SessionID: sessionID,
ModelID: req.ModelID,
Context: req.Context, Context: req.Context,
} }
// 将模型配置放入 Context 中传递给 Python 服务
if modelConfig != nil {
pythonReq.Context = make(map[string]interface{})
for k, v := range req.Context {
pythonReq.Context[k] = v
}
pythonReq.Context["model_config"] = modelConfig
}
pythonResp, err := s.callPythonChat(ctx, pythonReq) pythonResp, err := s.callPythonChat(ctx, pythonReq)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to call python service: %w", err) return nil, fmt.Errorf("failed to call python service: %w", err)

View File

@@ -1,8 +1,85 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, nextTick } from 'vue' import { ref, nextTick, onMounted, onUnmounted } from 'vue'
const API_BASE = 'http://localhost:8082' const API_BASE = 'http://localhost:8082'
// 模型列表
interface ChatModel {
id: string
name: string
model_type: string
provider: string
model: string
status: string
}
const chatModels = ref<ChatModel[]>([])
const selectedModel = ref<ChatModel | null>(null)
const modelsLoading = ref(false)
const showModelDropdown = ref(false)
// 根据 provider 获取图标
const getModelIcon = (provider: string) => {
const icons: Record<string, string> = {
'OpenAI': '🤖',
'Claude': '🧠',
'Google': '✨',
'Gemini': '✨',
'Ollama': '🦙',
'DeepSeek': '🔮',
'Moonshot': '🌙',
'Kimi': '🌙',
'Baidu': '🐉',
'文心一言': '🐉',
'Aliyun': '☁️',
'Ali': '☁️',
'通义千问': '☁️',
'Azure': '⬛',
'Anthropic': '🧠',
}
return icons[provider] || '💬'
}
// 获取模型列表
const fetchModels = async () => {
modelsLoading.value = true
try {
const response = await fetch(`${API_BASE}/model/list`)
const data = await response.json()
if (data.list) {
// 过滤出 chat 类型且 active 状态的模型
chatModels.value = data.list.filter((m: ChatModel) => m.model_type === 'chat' && m.status === 'active')
console.log('Chat models:', chatModels.value)
// 默认选择第一个
if (chatModels.value.length > 0 && !selectedModel.value) {
selectedModel.value = chatModels.value[0]
}
}
} catch (error) {
console.error('Failed to fetch models:', error)
} finally {
modelsLoading.value = false
}
}
onMounted(() => {
fetchModels()
// 点击外部关闭下拉框
document.addEventListener('click', handleClickOutside)
})
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside)
})
// 点击外部关闭下拉框
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement
if (!target.closest('.model-dropdown')) {
showModelDropdown.value = false
}
}
interface ChatMessage { interface ChatMessage {
id: number id: number
role: 'user' | 'assistant' role: 'user' | 'assistant'
@@ -162,16 +239,24 @@ const sendMessage = async () => {
isLoading.value = true isLoading.value = true
try { try {
// 构建请求体,包含模型信息
const requestBody: any = {
agent_id: selectedAgent.value?.id || 1,
message: userContent,
}
// 如果选择了模型,只传递 model_id后端会根据 id 查询 api_key 和 base_url
if (selectedModel.value) {
requestBody.model_id = selectedModel.value.id
}
// 调用后端 API // 调用后端 API
const response = await fetch(`${API_BASE}/api/agent/chat`, { const response = await fetch(`${API_BASE}/api/agent/chat`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify(requestBody),
agent_id: selectedAgent.value?.id || 1,
message: userContent,
}),
}) })
const data = await response.json() const data = await response.json()
@@ -294,6 +379,18 @@ const toggleSidebar = () => {
background: rgba(255, 255, 255, 0.15); background: rgba(255, 255, 255, 0.15);
} }
/* 下拉框动画 */
.dropdown-enter-active,
.dropdown-leave-active {
transition: all 0.2s ease;
}
.dropdown-enter-from,
.dropdown-leave-to {
opacity: 0;
transform: translateY(-8px);
}
@keyframes messageSlideIn { @keyframes messageSlideIn {
from { from {
opacity: 0; opacity: 0;
@@ -356,8 +453,59 @@ const toggleSidebar = () => {
<!-- 中间空白 --> <!-- 中间空白 -->
<div class="flex-1"></div> <div class="flex-1"></div>
<!-- 右侧折叠按钮 --> <!-- 右侧模型选择和折叠按钮 -->
<div class="flex items-center"> <div class="flex items-center gap-3">
<!-- 模型选择下拉框 -->
<div class="relative model-dropdown" v-if="chatModels.length > 0">
<button
@click="showModelDropdown = !showModelDropdown"
class="flex items-center gap-2 px-3 py-1.5 rounded-lg border border-white/[0.08] bg-[#1a1a24] hover:border-orange-500/30 transition-all duration-200"
>
<div class="flex items-center gap-2">
<!-- 动态获取模型图标 -->
<span class="text-base">{{ getModelIcon(selectedModel?.provider || '') }}</span>
<span class="text-sm text-white">{{ selectedModel?.name || 'Select Model' }}</span>
</div>
<span class="w-1.5 h-1.5 rounded-full bg-emerald-400"></span>
<svg class="w-3.5 h-3.5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
</svg>
</button>
<!-- 下拉菜单 -->
<Transition name="dropdown">
<div
v-if="showModelDropdown"
class="absolute right-0 top-full mt-2 w-64 bg-[#1a1a24] border border-white/[0.08] rounded-xl shadow-2xl shadow-black/50 overflow-hidden z-50"
>
<div class="p-2">
<div class="text-xs text-white/40 px-2 py-1 mb-1">Select Chat Model</div>
<button
v-for="model in chatModels"
:key="model.id"
@click="selectedModel = model; showModelDropdown = false"
class="w-full flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-150"
:class="selectedModel?.id === model.id
? 'bg-orange-500/15 border border-orange-500/30'
: 'hover:bg-white/5'"
>
<span class="text-lg">{{ getModelIcon(model.provider) }}</span>
<div class="flex-1 text-left">
<div class="text-sm text-white">{{ model.name }}</div>
<div class="text-xs text-white/40">{{ model.provider }} · {{ model.model }}</div>
</div>
<span class="w-2 h-2 rounded-full bg-emerald-400"></span>
</button>
</div>
</div>
</Transition>
</div>
<!-- 没有模型时的提示 -->
<div v-else class="text-xs text-white/30 px-2 flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-white/20"></span>
No models
</div>
<button <button
@click="toggleSidebar" @click="toggleSidebar"
class="p-2.5 rounded-xl hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200" class="p-2.5 rounded-xl hover:bg-white/[0.06] text-white/35 hover:text-white/80 transition-all duration-200"