From 2862db35345ef491e1ebfddebee25098df9fdef9 Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Wed, 28 Jan 2026 10:15:51 +0800 Subject: [PATCH 1/3] feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics --- api/app/controllers/app_controller.py | 41 ++ api/app/controllers/model_controller.py | 294 +++++++++++-- api/app/models/__init__.py | 4 +- api/app/models/models_model.py | 102 ++++- api/app/repositories/model_repository.py | 268 ++++++++++-- api/app/schemas/model_schema.py | 171 +++++++- api/app/services/app_statistics_service.py | 193 +++++++++ api/app/services/draft_run_service.py | 27 +- api/app/services/llm_router.py | 17 +- api/app/services/memory_config_service.py | 4 +- api/app/services/model_service.py | 413 ++++++++++++++++--- api/app/services/multi_agent_orchestrator.py | 25 +- api/app/services/shared_chat_service.py | 50 ++- api/app/version_info.json | 82 ++-- 14 files changed, 1458 insertions(+), 233 deletions(-) create mode 100644 api/app/services/app_statistics_service.py diff --git a/api/app/controllers/app_controller.py b/api/app/controllers/app_controller.py index 3b4e5a25..d57ee69d 100644 --- a/api/app/controllers/app_controller.py +++ b/api/app/controllers/app_controller.py @@ -872,3 +872,44 @@ async def update_workflow_config( workspace_id = current_user.current_workspace_id cfg = app_service.update_workflow_config(db, app_id=app_id, data=payload, workspace_id=workspace_id) return success(data=WorkflowConfigSchema.model_validate(cfg)) + + +@router.get("/{app_id}/statistics", summary="应用统计数据") +@cur_workspace_access_guard() +def get_app_statistics( + app_id: uuid.UUID, + start_date: int, + end_date: int, + db: Session = Depends(get_db), + current_user=Depends(get_current_user), +): + """获取应用统计数据 + + Args: + app_id: 应用ID + start_date: 开始时间戳(毫秒) + end_date: 结束时间戳(毫秒) + + Returns: + - daily_conversations: 每日会话数统计 + - total_conversations: 总会话数 + - daily_new_users: 每日新增用户数 + - total_new_users: 总新增用户数 + - daily_api_calls: 每日API调用次数 + - total_api_calls: 总API调用次数 + - daily_tokens: 每日token消耗 + - total_tokens: 总token消耗 + """ + workspace_id = current_user.current_workspace_id + + from app.services.app_statistics_service import AppStatisticsService + stats_service = AppStatisticsService(db) + + result = stats_service.get_app_statistics( + app_id=app_id, + workspace_id=workspace_id, + start_date=start_date, + end_date=end_date + ) + + return success(data=result) diff --git a/api/app/controllers/model_controller.py b/api/app/controllers/model_controller.py index 42d59664..481c520e 100644 --- a/api/app/controllers/model_controller.py +++ b/api/app/controllers/model_controller.py @@ -3,15 +3,17 @@ from sqlalchemy.orm import Session from typing import Optional import uuid - +from app.core.error_codes import BizCode +from app.core.exceptions import BusinessException from app.db import get_db from app.dependencies import get_current_user from app.models.models_model import ModelProvider, ModelType from app.models.user_model import User +from app.repositories.model_repository import ModelConfigRepository from app.schemas import model_schema from app.core.response_utils import success from app.schemas.response_schema import ApiResponse, PageData -from app.services.model_service import ModelConfigService, ModelApiKeyService +from app.services.model_service import ModelConfigService, ModelApiKeyService, ModelBaseService from app.core.logging_config import get_api_logger # 获取API专用日志器 @@ -24,7 +26,6 @@ router = APIRouter( @router.get("/type", response_model=ApiResponse) def get_model_types(): - return success(msg="获取模型类型成功", data=list(ModelType)) @@ -35,13 +36,65 @@ def get_model_providers(): @router.get("", response_model=ApiResponse) def get_model_list( - type: Optional[str] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), - provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于API Key)"), + type: Optional[str | list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), + provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于API Key)"), + is_active: Optional[bool] = Query(None, description="激活状态筛选"), + is_public: Optional[bool] = Query(None, description="公开状态筛选"), + search: Optional[str] = Query(None, description="搜索关键词"), + page: int = Query(1, ge=1, description="页码"), + pagesize: int = Query(10, ge=1, le=100, description="每页数量"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 获取模型配置列表 + + 支持多个 type 参数: + - 单个:?type=LLM + - 多个(逗号分隔):?type=LLM,EMBEDDING + - 多个(重复参数):?type=LLM&type=EMBEDDING + """ + api_logger.info( + f"获取模型配置列表请求: type={type}, provider={provider}, page={page}, pagesize={pagesize}, tenant_id={current_user.tenant_id}") + + try: + # 解析 type 参数(支持逗号分隔) + type_list = [] + if isinstance(type, str): + type_values = [t.strip() for t in type.split(',')] + type_list = [model_schema.ModelType(t.lower()) for t in type_values if t] + elif isinstance(type, list): + type_list = type + + api_logger.error(f"获取模型type_list: {type_list}") + query = model_schema.ModelConfigQuery( + type=type_list, + provider=provider, + is_active=is_active, + is_public=is_public, + search=search, + page=page, + pagesize=pagesize + ) + + api_logger.debug(f"开始获取模型配置列表: {query.dict()}") + result_orm = ModelConfigService.get_model_list(db=db, query=query, tenant_id=current_user.tenant_id) + result = PageData.model_validate(result_orm) + api_logger.info(f"模型配置列表获取成功: 总数={result.page.total}, 当前页={len(result.items)}") + return success(data=result, msg="模型配置列表获取成功") + except Exception as e: + api_logger.error(f"获取模型配置列表失败: {str(e)}") + raise + + +@router.get("/new", response_model=ApiResponse) +def get_model_list( + type: Optional[str | list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), + provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于ModelConfig)"), is_active: Optional[bool] = Query(None, description="激活状态筛选"), is_public: Optional[bool] = Query(None, description="公开状态筛选"), search: Optional[str] = Query(None, description="搜索关键词"), - page: int = Query(1, ge=1, description="页码"), - pagesize: int = Query(10, ge=1, le=100, description="每页数量"), + is_composite: Optional[bool] = Query(None, description="组合模型筛选"), db: Session = Depends(get_db), current_user: User = Depends(get_current_user) ): @@ -53,36 +106,120 @@ def get_model_list( - 多个(逗号分隔):?type=LLM,EMBEDDING - 多个(重复参数):?type=LLM&type=EMBEDDING """ - api_logger.info(f"获取模型配置列表请求: type={type}, provider={provider}, page={page}, pagesize={pagesize}, tenant_id={current_user.tenant_id}") + api_logger.info(f"获取模型配置列表请求: type={type}, provider={provider}, tenant_id={current_user.tenant_id}") try: # 解析 type 参数(支持逗号分隔) - type_list = None - if type: + type_list = [] + if isinstance(type, str): type_values = [t.strip() for t in type.split(',')] type_list = [model_schema.ModelType(t.lower()) for t in type_values if t] + elif isinstance(type, list): + type_list = type - api_logger.error(f"获取模型type_list: {type_list}") - query = model_schema.ModelConfigQuery( + api_logger.info(f"获取模型type_list: {type_list}") + query = model_schema.ModelConfigQueryNew( type=type_list, provider=provider, is_active=is_active, is_public=is_public, - search=search, - page=page, - pagesize=pagesize + is_composite=is_composite, + search=search ) - api_logger.debug(f"开始获取模型配置列表: {query.dict()}") - result_orm = ModelConfigService.get_model_list(db=db, query=query, tenant_id=current_user.tenant_id) - result = PageData.model_validate(result_orm) - api_logger.info(f"模型配置列表获取成功: 总数={result.page.total}, 当前页={len(result.items)}") + api_logger.debug(f"开始获取模型配置列表: {query.model_dump()}") + result = ModelConfigService.get_model_list_new(db=db, query=query, tenant_id=current_user.tenant_id) + api_logger.info(f"模型配置列表获取成功: 分组数={len(result)}, 总模型数={sum(len(item['models']) for item in result)}") return success(data=result, msg="模型配置列表获取成功") except Exception as e: api_logger.error(f"获取模型配置列表失败: {str(e)}") raise +@router.get("/model_plaza", response_model=ApiResponse) +def get_model_plaza_list( + type: Optional[ModelType] = Query(None, description="模型类型"), + provider: Optional[ModelProvider] = Query(None, description="供应商"), + is_official: Optional[bool] = Query(None, description="是否官方模型"), + is_deprecated: Optional[bool] = Query(False, description="是否弃用"), + search: Optional[str] = Query(None, description="搜索关键词"), + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """模型广场查询接口(按供应商分组)""" + + query = model_schema.ModelBaseQuery( + type=type, + provider=provider, + is_official=is_official, + is_deprecated=is_deprecated, + search=search + ) + result = ModelBaseService.get_model_base_list(db=db, query=query, tenant_id=current_user.tenant_id) + return success(data=result, msg="模型广场列表获取成功") + + +@router.get("/model_plaza/{model_base_id}", response_model=ApiResponse) +def get_model_base_by_id( + model_base_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """获取基础模型详情""" + + result = ModelBaseService.get_model_base_by_id(db=db, model_base_id=model_base_id) + return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型获取成功") + + +@router.post("/model_plaza", response_model=ApiResponse) +def create_model_base( + data: model_schema.ModelBaseCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """创建基础模型""" + + result = ModelBaseService.create_model_base(db=db, data=data) + return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型创建成功") + + +@router.put("/model_plaza/{model_base_id}", response_model=ApiResponse) +def update_model_base( + model_base_id: uuid.UUID, + data: model_schema.ModelBaseUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """更新基础模型""" + + result = ModelBaseService.update_model_base(db=db, model_base_id=model_base_id, data=data) + return success(data=model_schema.ModelBase.model_validate(result), msg="基础模型更新成功") + + +@router.delete("/model_plaza/{model_base_id}", response_model=ApiResponse) +def delete_model_base( + model_base_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """删除基础模型""" + + ModelBaseService.delete_model_base(db=db, model_base_id=model_base_id) + return success(msg="基础模型删除成功") + + +@router.post("/model_plaza/{model_base_id}/add", response_model=ApiResponse) +def add_model_from_plaza( + model_base_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """从模型广场添加模型到模型列表""" + + result = ModelBaseService.add_model_from_plaza(db=db, model_base_id=model_base_id, tenant_id=current_user.tenant_id) + return success(data=model_schema.ModelConfig.model_validate(result), msg="模型添加成功") + + @router.get("/{model_id}", response_model=ApiResponse) def get_model_by_id( model_id: uuid.UUID, @@ -138,6 +275,71 @@ async def create_model( raise +@router.post("/composite", response_model=ApiResponse) +async def create_composite_model( + model_data: model_schema.CompositeModelCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 创建组合模型 + + - 绑定一个或多个现有的 API Key + - 所有 API Key 必须来自非组合模型 + - 所有 API Key 关联的模型类型必须与组合模型类型一致 + """ + api_logger.info(f"创建组合模型请求: {model_data.name}, 用户: {current_user.username}, tenant_id={current_user.tenant_id}") + + try: + result_orm = await ModelConfigService.create_composite_model(db=db, model_data=model_data, tenant_id=current_user.tenant_id) + api_logger.info(f"组合模型创建成功: {result_orm.name} (ID: {result_orm.id})") + + result = model_schema.ModelConfig.model_validate(result_orm) + return success(data=result, msg="组合模型创建成功") + except Exception as e: + api_logger.error(f"创建组合模型失败: {model_data.name} - {str(e)}") + raise + + +@router.put("/composite/{model_id}", response_model=ApiResponse) +async def update_composite_model( + model_id: uuid.UUID, + model_data: model_schema.CompositeModelCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """更新组合模型""" + api_logger.info(f"更新组合模型请求: model_id={model_id}, 用户: {current_user.username}") + + try: + result_orm = await ModelConfigService.update_composite_model(db=db, model_id=model_id, model_data=model_data, tenant_id=current_user.tenant_id) + api_logger.info(f"组合模型更新成功: {result_orm.name} (ID: {model_id})") + + result = model_schema.ModelConfig.model_validate(result_orm) + return success(data=result, msg="组合模型更新成功") + except Exception as e: + api_logger.error(f"更新组合模型失败: model_id={model_id} - {str(e)}") + raise + + +@router.delete("/composite/{model_id}", response_model=ApiResponse) +def delete_composite_model( + model_id: uuid.UUID, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """删除组合模型""" + api_logger.info(f"删除组合模型请求: model_id={model_id}, 用户: {current_user.username}") + + try: + ModelConfigService.delete_model(db=db, model_id=model_id, tenant_id=current_user.tenant_id) + api_logger.info(f"组合模型删除成功: model_id={model_id}") + return success(msg="组合模型删除成功") + except Exception as e: + api_logger.error(f"删除组合模型失败: model_id={model_id} - {str(e)}") + raise + + @router.put("/{model_id}", response_model=ApiResponse) def update_model( model_id: uuid.UUID, @@ -214,6 +416,51 @@ def get_model_api_keys( raise +@router.post("/provider/apikeys", response_model=ApiResponse) +async def create_model_api_key_by_provider( + api_key_data: model_schema.ModelApiKeyCreateByProvider, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) +): + """ + 根据供应商为所有匹配的模型创建API Key + """ + api_logger.info(f"创建API Key请求: provider={api_key_data.provider}, 用户: {current_user.username}") + + try: + # 根据tenant_id和provider筛选model_config_id列表 + model_config_ids = api_key_data.model_config_ids + if not model_config_ids: + model_config_ids = ModelConfigRepository.get_model_config_ids_by_provider( + db=db, + tenant_id=current_user.tenant_id, + provider=api_key_data.provider + ) + + if not model_config_ids: + raise BusinessException(f"未找到供应商 {api_key_data.provider} 的模型配置", BizCode.MODEL_NOT_FOUND) + + # 构造schema并调用service + create_data = model_schema.ModelApiKeyCreateByProvider( + provider=api_key_data.provider, + api_key=api_key_data.api_key, + api_base=api_key_data.api_base, + description=api_key_data.description, + config=api_key_data.config, + is_active=api_key_data.is_active, + priority=api_key_data.priority, + model_config_ids=model_config_ids + ) + created_keys = await ModelApiKeyService.create_api_key_by_provider(db=db, data=create_data) + + api_logger.info(f"API Key创建成功: 关联{len(created_keys)}个模型") + result_list = [model_schema.ModelApiKey.model_validate(key) for key in created_keys] + return success(data=result_list, msg=f"成功为 {len(created_keys)} 个模型创建API Key") + except Exception as e: + api_logger.error(f"创建API Key失败: {str(e)}") + raise + + @router.post("/{model_id}/apikeys", response_model=ApiResponse, status_code=status.HTTP_201_CREATED) async def create_model_api_key( model_id: uuid.UUID, @@ -228,11 +475,12 @@ async def create_model_api_key( try: # 设置模型配置ID - api_key_data.model_config_id = model_id + api_key_data.model_config_ids = [model_id] api_logger.debug(f"开始创建模型API Key: {api_key_data.model_name}") - result = await ModelApiKeyService.create_api_key(db=db, api_key_data=api_key_data) - api_logger.info(f"模型API Key创建成功: {result.model_name} (ID: {result.id})") + result_orm = await ModelApiKeyService.create_api_key(db=db, api_key_data=api_key_data) + api_logger.info(f"模型API Key创建成功: {result_orm.model_name} (ID: {result_orm.id})") + result = model_schema.ModelApiKey.model_validate(result_orm) return success(data=result, msg="模型API Key创建成功") except Exception as e: api_logger.error(f"创建模型API Key失败: {api_key_data.model_name} - {str(e)}") @@ -334,5 +582,3 @@ async def validate_model_config( return success(data=model_schema.ModelValidateResponse(**result), msg="验证完成") - - diff --git a/api/app/models/__init__.py b/api/app/models/__init__.py index e069b40d..a429dd8e 100644 --- a/api/app/models/__init__.py +++ b/api/app/models/__init__.py @@ -6,7 +6,7 @@ from .document_model import Document from .file_model import File from .file_metadata_model import FileMetadata from .generic_file_model import GenericFile -from .models_model import ModelConfig, ModelProvider, ModelType, ModelApiKey +from .models_model import ModelConfig, ModelProvider, ModelType, ModelApiKey, ModelBase, LoadBalanceStrategy from .memory_short_model import ShortTermMemory, LongTermMemory from .knowledgeshare_model import KnowledgeShare from .app_model import App @@ -79,4 +79,6 @@ __all__ = [ "AuthType", "ExecutionStatus", "MemoryPerceptualModel", + "ModelBase", + "LoadBalanceStrategy" ] diff --git a/api/app/models/models_model.py b/api/app/models/models_model.py index 2e60ef1c..a2bfa284 100644 --- a/api/app/models/models_model.py +++ b/api/app/models/models_model.py @@ -1,19 +1,31 @@ import datetime import uuid from enum import StrEnum -from typing import Optional, List -from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum + +from sqlalchemy import Column, String, Boolean, DateTime, Text, ForeignKey, Enum as SQLEnum, UniqueConstraint, Integer, ARRAY, Table from sqlalchemy.dialects.postgresql import UUID, JSON from sqlalchemy.orm import relationship from app.db import Base +class BaseModel(Base): + """基础模型(抽象类,提取公共字段)""" + __abstract__ = True # 标记为抽象类,不生成表 + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) + created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间") + updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间") + is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") + + class ModelType(StrEnum): """模型类型枚举""" LLM = "llm" CHAT = "chat" EMBEDDING = "embedding" RERANK = "rerank" + # IMAGE = "image" + # AUDIO = "audio" + # VISION = "vision" class ModelProvider(StrEnum): @@ -30,16 +42,37 @@ class ModelProvider(StrEnum): XINFERENCE = "xinference" GPUSTACK = "gpustack" BEDROCK = "bedrock" + COMPOSITE = "composite" -class ModelConfig(Base): +class LoadBalanceStrategy(StrEnum): + """API Key负载均衡策略枚举""" + ROUND_ROBIN = "round_robin" # 轮询 + WEIGHTED_ROUND_ROBIN = "weighted_round_robin" # 加权轮询 + RANDOM = "random" # 随机 + + +# 多对多关联表 +model_config_api_key_association = Table( + 'model_config_api_key_association', + Base.metadata, + Column('model_config_id', UUID(as_uuid=True), ForeignKey('model_configs.id'), primary_key=True), + Column('api_key_id', UUID(as_uuid=True), ForeignKey('model_api_keys.id'), primary_key=True), + Column('created_at', DateTime, default=datetime.datetime.now) +) + + +class ModelConfig(BaseModel): """模型配置表""" __tablename__ = "model_configs" - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) + model_id = Column(UUID(as_uuid=True), ForeignKey("model_bases.id"), nullable=True, index=True, comment="基础模型ID") tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True, comment="租户ID") + logo = Column(String(255), nullable=True, comment="模型logo图片URL") name = Column(String, nullable=False, comment="模型显示名称") + provider = Column(String, nullable=False, comment="供应商", server_default=ModelProvider.COMPOSITE) type = Column(String, nullable=False, index=True, comment="模型类型") + is_composite = Column(Boolean, default=False, server_default="true", nullable=False, comment="是否为组合模型") description = Column(String, comment="模型描述") # 模型配置参数 @@ -56,29 +89,28 @@ class ModelConfig(Base): # context_length = Column(String, comment="上下文长度") # 状态管理 - is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") is_public = Column(Boolean, default=False, nullable=False, comment="是否公开") - - # 时间戳 - created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间") - updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间") + load_balance_strategy = Column(String, nullable=True, comment="负载均衡策略") # 关联关系 - api_keys = relationship("ModelApiKey", back_populates="model_config", cascade="all, delete-orphan") + model_base = relationship("ModelBase", back_populates="configs") + api_keys = relationship( + "ModelApiKey", + secondary=model_config_api_key_association, + back_populates="model_configs" + ) def __repr__(self): return f"" -class ModelApiKey(Base): +class ModelApiKey(BaseModel): """模型API密钥表""" __tablename__ = "model_api_keys" - - id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) - model_config_id = Column(UUID(as_uuid=True), ForeignKey("model_configs.id"), nullable=False, comment="模型配置ID") # API Key 信息 model_name = Column(String, nullable=False, comment="模型实际名称") + description = Column(String, comment="备注") provider = Column(String, nullable=False, comment="API Key提供商") api_key = Column(String, nullable=False, comment="API密钥") api_base = Column(String, comment="API基础URL") @@ -91,15 +123,41 @@ class ModelApiKey(Base): last_used_at = Column(DateTime, comment="最后使用时间") # 状态管理 - is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") priority = Column(String, default="1", comment="优先级") - - # 时间戳 - created_at = Column(DateTime, default=datetime.datetime.now, comment="创建时间") - updated_at = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now, comment="更新时间") - + # 关联关系 - model_config = relationship("ModelConfig", back_populates="api_keys") + model_configs = relationship( + "ModelConfig", + secondary=model_config_api_key_association, + back_populates="api_keys" + ) + def __repr__(self): - return f"" + return f"" + + +class ModelBase(Base): + """基础模型信息表(模型广场)""" + __tablename__ = "model_bases" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True) + logo = Column(String(255), nullable=True, comment="模型logo图片URL") + name = Column(String, nullable=False, comment="模型唯一标识(如gpt-3.5-turbo)") + type = Column(String, nullable=False, index=True, comment="模型类型") + provider = Column(String, nullable=False, index=True) + description = Column(Text, comment="模型描述") + is_deprecated = Column(Boolean, default=False, nullable=False, comment="是否弃用") + is_official = Column(Boolean, default=True, comment="是否供应商官方模型(区分自定义)") + tags = Column(ARRAY(String), default=[], nullable=False, comment="模型标签(如['聊天', '创作'])") + add_count = Column(Integer, default=0, nullable=False, comment="模型被用户添加的次数") + + # 关联关系 + configs = relationship("ModelConfig", back_populates="model_base", cascade="all, delete-orphan") + + __table_args__ = ( + UniqueConstraint("name", "provider", name="uk_model_name_provider"), + ) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/api/app/repositories/model_repository.py b/api/app/repositories/model_repository.py index 1fe29d66..8e4632cc 100644 --- a/api/app/repositories/model_repository.py +++ b/api/app/repositories/model_repository.py @@ -1,12 +1,12 @@ -from sqlalchemy.orm import Session, joinedload -from sqlalchemy import and_, or_, func, desc +from sqlalchemy.orm import Session, joinedload, selectinload +from sqlalchemy import and_, or_, func, desc, select from typing import List, Optional, Dict, Any, Tuple import uuid -from app.models.models_model import ModelConfig, ModelApiKey, ModelType +from app.models.models_model import ModelConfig, ModelApiKey, ModelType, ModelBase, model_config_api_key_association from app.schemas.model_schema import ( ModelConfigUpdate, ModelApiKeyCreate, ModelApiKeyUpdate, - ModelConfigQuery + ModelConfigQuery, ModelConfigQueryNew ) from app.core.logging_config import get_db_logger @@ -107,6 +107,80 @@ class ModelConfigRepository: def get_list(db: Session, query: ModelConfigQuery, tenant_id: uuid.UUID | None = None) -> Tuple[List[ModelConfig], int]: """获取模型配置列表""" db_logger.debug(f"查询模型配置列表: {query.dict()}, tenant_id={tenant_id}") + + try: + # 构建查询条件 + filters = [] + + # 添加租户过滤(查询本租户的模型或公开模型) + if tenant_id: + filters.append( + or_( + ModelConfig.tenant_id == tenant_id, + ModelConfig.is_public + ) + ) + + # 支持多个 type 值(使用 IN 查询) + # 兼容 chat 和 llm 类型:如果查询包含其中一个,则同时匹配两者 + if query.type: + type_values = list(query.type) + # 如果包含 chat 或 llm,则同时包含两者 + if ModelType.CHAT in type_values or ModelType.LLM in type_values: + if ModelType.CHAT not in type_values: + type_values.append(ModelType.CHAT) + if ModelType.LLM not in type_values: + type_values.append(ModelType.LLM) + filters.append(ModelConfig.type.in_(type_values)) + + if query.is_active is not None: + filters.append(ModelConfig.is_active == query.is_active) + + if query.is_public is not None: + filters.append(ModelConfig.is_public == query.is_public) + + if query.search: + # 搜索逻辑需要join ModelApiKey表来搜索model_name + search_filter = or_( + ModelConfig.name.ilike(f"%{query.search}%"), + # ModelConfig.description.ilike(f"%{query.search}%") + ) + filters.append(search_filter) + + # 构建基础查询 + base_query = db.query(ModelConfig).options( + joinedload(ModelConfig.api_keys) + ) + + # 如果需要按provider筛选,需要join ModelApiKey表 + if query.provider: + base_query = base_query.join(ModelApiKey).filter( + ModelApiKey.provider == query.provider + ).distinct() + + if filters: + base_query = base_query.filter(and_(*filters)) + + # 获取总数 + total = base_query.count() + + # 分页查询 + models = base_query.order_by(desc(ModelConfig.updated_at)).offset( + (query.page - 1) * query.pagesize + ).limit(query.pagesize).all() + + db_logger.debug(f"模型配置列表查询成功: 总数={total}, 当前页={len(models)}, type筛选={query.type}") + return models, total + + except Exception as e: + db_logger.error(f"查询模型配置列表失败: {str(e)}") + raise + + @staticmethod + def get_list_new(db: Session, query: ModelConfigQueryNew, tenant_id: uuid.UUID | None = None) -> tuple[ + dict[str, list[ModelConfig]], Any]: + """获取模型配置列表""" + db_logger.debug(f"查询模型配置列表: {query.model_dump()}, tenant_id={tenant_id}") try: # 构建查询条件 @@ -138,13 +212,15 @@ class ModelConfigRepository: if query.is_public is not None: filters.append(ModelConfig.is_public == query.is_public) + + if query.is_composite is not None: + filters.append(ModelConfig.is_composite == query.is_composite) + + if query.provider: + filters.append(ModelConfig.provider == query.provider) if query.search: - # 搜索逻辑需要join ModelApiKey表来搜索model_name - search_filter = or_( - ModelConfig.name.ilike(f"%{query.search}%"), - # ModelConfig.description.ilike(f"%{query.search}%") - ) + search_filter = ModelConfig.name.ilike(f"%{query.search}%") filters.append(search_filter) # 构建基础查询 @@ -152,28 +228,30 @@ class ModelConfigRepository: joinedload(ModelConfig.api_keys) ) - # 如果需要按provider筛选,需要join ModelApiKey表 - if query.provider: - base_query = base_query.join(ModelApiKey).filter( - ModelApiKey.provider == query.provider - ).distinct() - if filters: base_query = base_query.filter(and_(*filters)) # 获取总数 total = base_query.count() + + query_results = base_query.order_by(desc(ModelConfig.updated_at)).all() + + provider_groups: Dict[str, List[ModelConfig]] = {} + for model_config in query_results: + provider = model_config.provider + if provider not in provider_groups: + provider_groups[provider] = [] + provider_groups[provider].append(model_config) - # 分页查询 - models = base_query.order_by(desc(ModelConfig.updated_at)).offset( - (query.page - 1) * query.pagesize - ).limit(query.pagesize).all() - - db_logger.debug(f"模型配置列表查询成功: 总数={total}, 当前页={len(models)}, type筛选={query.type}") - return models, total + db_logger.debug( + f"模型配置列表查询成功: 总数={total}, " + f"分组数={len(provider_groups)}, " + f"各分组模型数={[len(v) for v in provider_groups.values()]}, " + f"type筛选={query.type}") + return provider_groups, total except Exception as e: - db_logger.error(f"查询模型配置列表失败: {str(e)}") + db_logger.error(f"查询模型配置列表失败(按provider分组/无分页): {str(e)}") raise @staticmethod @@ -241,7 +319,7 @@ class ModelConfigRepository: return None # 更新字段 - update_data = model_data.dict(exclude_unset=True) + update_data = model_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_model, field, value) @@ -303,8 +381,18 @@ class ModelConfigRepository: # 按提供商统计 - 现在从ModelApiKey表获取 provider_stats = {} provider_results = db.query( - ModelApiKey.provider, func.count(func.distinct(ModelApiKey.model_config_id)) - ).group_by(ModelApiKey.provider).all() + # 保留 provider 字段 + ModelApiKey.provider, + # 统计中间表中 唯一的 model_config_id 数量(替换原 ModelApiKey.model_config_id) + func.count(func.distinct(model_config_api_key_association.c.model_config_id)) + ).join( + # 联表:ModelApiKey <-> 中间表(多对多关联) + model_config_api_key_association, + ModelApiKey.id == model_config_api_key_association.c.api_key_id + ).group_by( + # 按 provider 分组(保留原有逻辑) + ModelApiKey.provider + ).all() for provider, count in provider_results: provider_stats[provider.value] = count @@ -325,6 +413,37 @@ class ModelConfigRepository: db_logger.error(f"获取模型统计信息失败: {str(e)}") raise + @staticmethod + def get_model_config_ids_by_provider( + db: Session, + tenant_id: uuid.UUID, + provider: Any + ) -> List[uuid.UUID]: + """根据tenant_id和provider获取model_config_id列表""" + db_logger.debug(f"查询model_config_id列表: tenant_id={tenant_id}, provider={provider}") + + try: + # 查询ModelConfig关联的ModelApiKey,筛选出匹配的model_config_id + model_config_ids = db.query(ModelConfig.id).join( + ModelBase, ModelConfig.model_id == ModelBase.id + ).filter( + and_( + or_( + ModelConfig.tenant_id == tenant_id, + ModelConfig.is_public + ), + ModelBase.provider == provider, + ~ModelConfig.is_composite + ) + ).distinct().all() + + db_logger.debug(f"查询成功: 数量={len(model_config_ids)}") + return [row[0] for row in model_config_ids] + + except Exception as e: + db_logger.error(f"查询model_config_id列表失败: {str(e)}") + raise + class ModelApiKeyRepository: """模型API Key Repository""" @@ -349,7 +468,14 @@ class ModelApiKeyRepository: db_logger.debug(f"根据模型配置ID查询API Key: model_config_id={model_config_id}") try: - query = db.query(ModelApiKey).filter(ModelApiKey.model_config_id == model_config_id) + from app.models.models_model import ModelConfig, model_config_api_key_association + + query = db.query(ModelApiKey).join( + model_config_api_key_association, + ModelApiKey.id == model_config_api_key_association.c.api_key_id + ).filter( + model_config_api_key_association.c.model_config_id == model_config_id + ) if is_active: query = query.filter(ModelApiKey.is_active) @@ -368,8 +494,20 @@ class ModelApiKeyRepository: db_logger.debug(f"创建API Key: {api_key_data.provider}") try: - db_api_key = ModelApiKey(**api_key_data.dict()) + from app.models.models_model import ModelConfig + + # 创建API Key,不包含model_config_ids + api_key_dict = api_key_data.model_dump(exclude={"model_config_ids"}) + db_api_key = ModelApiKey(**api_key_dict) db.add(db_api_key) + db.flush() # 获取生成的ID + + # 关联ModelConfig + if api_key_data.model_config_ids: + for model_config_id in api_key_data.model_config_ids: + model_config = db.query(ModelConfig).filter(ModelConfig.id == model_config_id).first() + if model_config: + db_api_key.model_configs.append(model_config) db_logger.info(f"API Key已添加到会话: {db_api_key.provider}") return db_api_key @@ -391,7 +529,7 @@ class ModelApiKeyRepository: return None # 更新字段 - update_data = api_key_data.dict(exclude_unset=True) + update_data = api_key_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(db_api_key, field, value) @@ -451,4 +589,74 @@ class ModelApiKeyRepository: except Exception as e: db.rollback() db_logger.error(f"更新API Key使用统计失败: api_key_id={api_key_id} - {str(e)}") - raise \ No newline at end of file + raise + + +class ModelBaseRepository: + """基础模型Repository""" + + @staticmethod + def get_by_id(db: Session, model_base_id: uuid.UUID) -> Optional['ModelBase']: + return db.query(ModelBase).filter(ModelBase.id == model_base_id).first() + + @staticmethod + def get_list(db: Session, query: 'ModelBaseQuery') -> List['ModelBase']: + + filters = [] + if query.type: + filters.append(ModelBase.type == query.type) + if query.provider: + filters.append(ModelBase.provider == query.provider) + if query.is_official is not None: + filters.append(ModelBase.is_official == query.is_official) + if query.is_deprecated is not None: + filters.append(ModelBase.is_deprecated == query.is_deprecated) + if query.search: + filters.append(or_( + ModelBase.name.ilike(f"%{query.search}%"), + # ModelBase.description.ilike(f"%{query.search}%") + )) + + q = db.query(ModelBase) + if filters: + q = q.filter(and_(*filters)) + + return q.order_by(ModelBase.add_count.desc()).all() + + @staticmethod + def create(db: Session, data: dict) -> 'ModelBase': + model_base = ModelBase(**data) + db.add(model_base) + return model_base + + @staticmethod + def update(db: Session, model_base_id: uuid.UUID, data: dict) -> Optional['ModelBase']: + model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first() + if not model_base: + return None + for key, value in data.items(): + setattr(model_base, key, value) + return model_base + + @staticmethod + def delete(db: Session, model_base_id: uuid.UUID) -> bool: + model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first() + if not model_base: + return False + db.delete(model_base) + return True + + @staticmethod + def increment_add_count(db: Session, model_base_id: uuid.UUID) -> bool: + model_base = db.query(ModelBase).filter(ModelBase.id == model_base_id).first() + if not model_base: + return False + model_base.add_count += 1 + return True + + @staticmethod + def check_added_by_tenant(db: Session, model_base_id: uuid.UUID, tenant_id: uuid.UUID) -> bool: + return db.query(ModelConfig).filter( + ModelConfig.model_id == model_base_id, + ModelConfig.tenant_id == tenant_id + ).first() is not None diff --git a/api/app/schemas/model_schema.py b/api/app/schemas/model_schema.py index 68f15115..b83107ef 100644 --- a/api/app/schemas/model_schema.py +++ b/api/app/schemas/model_schema.py @@ -12,7 +12,9 @@ class ModelConfigBase(BaseModel): """模型配置基础Schema""" name: str = Field(..., description="模型显示名称", max_length=255) type: ModelType = Field(..., description="模型类型") + logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255) description: Optional[str] = Field(None, description="模型描述") + provider: str = Field(..., description="供应商") config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数") is_active: bool = Field(True, description="是否激活") is_public: bool = Field(False, description="是否公开") @@ -21,6 +23,7 @@ class ModelConfigBase(BaseModel): class ApiKeyCreateNested(BaseModel): """用于在创建模型时内嵌创建API Key的Schema""" model_name: str = Field(..., description="模型实际名称", max_length=255) + description: Optional[str] = Field(None, description="备注") provider: ModelProvider = Field(..., description="API Key提供商") api_key: str = Field(..., description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) @@ -30,10 +33,22 @@ class ApiKeyCreateNested(BaseModel): class ModelConfigCreate(ModelConfigBase): """创建模型配置Schema""" - api_keys: Optional[ApiKeyCreateNested] = Field(None, description="同时创建的API Key配置") + api_keys: Optional[List[ApiKeyCreateNested]] = Field(None, description="同时创建的API Key配置") skip_validation: Optional[bool] = Field(False, description="是否跳过配置验证") +class CompositeModelCreate(BaseModel): + """创建组合模型Schema""" + name: str = Field(..., description="组合模型名称", max_length=255) + type: ModelType = Field(..., description="模型类型") + logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255) + description: Optional[str] = Field(None, description="模型描述") + config: Optional[Dict[str, Any]] = Field({}, description="模型配置参数") + is_active: bool = Field(True, description="是否激活") + is_public: bool = Field(False, description="是否公开") + api_key_ids: List[uuid.UUID] = Field(..., description="绑定的API Key ID列表") + + class ModelConfigUpdate(BaseModel): """更新模型配置Schema""" name: Optional[str] = Field(None, description="模型显示名称", max_length=255) @@ -53,22 +68,48 @@ class ModelConfig(ModelConfigBase): updated_at: datetime.datetime api_keys: List["ModelApiKey"] = [] + @field_validator("api_keys", mode="after") + @classmethod + def filter_active_api_keys(cls, api_keys: List["ModelApiKey"]) -> List["ModelApiKey"]: + return [key for key in api_keys if key.is_active] + + @field_serializer("created_at", when_used="json") + def _serialize_created_at(self, dt: datetime.datetime | None): + return int(dt.timestamp() * 1000) if dt else None + + @field_serializer("updated_at", when_used="json") + def _serialize_updated_at(self, dt: datetime.datetime): + return int(dt.timestamp() * 1000) if dt else None + # ModelApiKey Schemas -class ModelApiKeyBase(BaseModel): - """API Key基础Schema""" - model_name: str = Field(..., description="模型实际名称", max_length=255) +class ModelApiKeyCreateByProvider(BaseModel): + """基于供应商创建API Key Schema""" provider: ModelProvider = Field(..., description="API Key提供商") api_key: str = Field(..., description="API密钥", max_length=500) api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) - config: Optional[Dict[str, Any]] = Field(None, description="API Key特定配置") + description: Optional[str] = Field(None, description="备注") + config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置") + is_active: bool = Field(True, description="是否激活") + priority: str = Field("1", description="优先级", max_length=10) + model_config_ids: Optional[List[uuid.UUID]] = Field(None, description="关联的模型配置ID列表") + + +class ModelApiKeyBase(BaseModel): + """API Key基础Schema""" + model_name: str = Field(..., description="模型实际名称", max_length=255) + description: Optional[str] = Field(None, description="备注") + provider: ModelProvider = Field(..., description="API Key提供商") + api_key: str = Field(..., description="API密钥", max_length=500) + api_base: Optional[str] = Field(None, description="API基础URL", max_length=500) + config: Optional[Dict[str, Any]] = Field({}, description="API Key特定配置") is_active: bool = Field(True, description="是否激活") priority: str = Field("1", description="优先级", max_length=10) class ModelApiKeyCreate(ModelApiKeyBase): """创建API Key Schema""" - model_config_id: uuid.UUID = Field(..., description="模型配置ID") + model_config_ids: Optional[List[uuid.UUID]] = Field(None, description="关联的模型配置ID列表") class ModelApiKeyUpdate(BaseModel): @@ -85,23 +126,54 @@ class ModelApiKeyUpdate(BaseModel): class ModelApiKey(ModelApiKeyBase): """API Key Schema""" id: uuid.UUID - model_config_id: uuid.UUID usage_count: str last_used_at: Optional[datetime.datetime] created_at: datetime.datetime updated_at: datetime.datetime + model_configs: Any = Field(default=None, exclude=True) + model_config_ids: List[uuid.UUID] = Field(default_factory=list, description="关联的模型配置ID列表") - @field_validator("config", mode="before") - @classmethod - def parse_config(cls, v): - """处理 config 字段,如果是字符串则解析为字典""" - if isinstance(v, str): - import json + def model_post_init(self, __context: Any) -> None: + """实例化后强制提取 model_configs 的ID到 model_config_ids""" + # 如果手动传入了 model_config_ids,不覆盖 + if self.model_config_ids and len(self.model_config_ids) > 0: + return + + # 从 model_configs 提取ID(只提取与 model_name 相同的非组合模型) + if self.model_configs is not None: try: - return json.loads(v) - except json.JSONDecodeError: - return {} - return v + # 情况1:ORM 对象列表(SQLAlchemy 关联) + if hasattr(self.model_configs, '__iter__') and not isinstance(self.model_configs, dict): + self.model_config_ids = [ + mc.id for mc in self.model_configs + if hasattr(mc, 'id') + and not getattr(mc, 'is_composite', False) + and getattr(mc, 'name', None) == self.model_name + ] + # 情况2:字典列表 + elif isinstance(self.model_configs, list): + self.model_config_ids = [ + mc['id'] if isinstance(mc, dict) else mc.id + for mc in self.model_configs + if ((isinstance(mc, dict) + and 'id' in mc + and not mc.get('is_composite', False) + and mc.get('name') == self.model_name) or + (hasattr(mc, 'id') + and not getattr(mc, 'is_composite', False) + and getattr(mc, 'name', None) == self.model_name)) + ] + except Exception as e: + print(f"提取 model_config_ids 失败:{e}") + self.model_config_ids = [] + + model_config = ConfigDict( + from_attributes=True, # 支持从 ORM 解析 + arbitrary_types_allowed=True, # 允许任意类型(ORM 对象) + populate_by_name=True, # 按属性名匹配字段 + validate_assignment=True # 确保赋值触发校验 + ) + @field_serializer("created_at", when_used="json") def _serialize_created_at(self, dt: datetime.datetime): @@ -110,15 +182,12 @@ class ModelApiKey(ModelApiKeyBase): @field_serializer("updated_at", when_used="json") def _serialize_updated_at(self, dt: datetime.datetime): return int(dt.timestamp() * 1000) if dt else None - - model_config = ConfigDict(from_attributes=True) @field_serializer("last_used_at", when_used="json") def _serialize_last_used_at(self, dt: datetime.datetime): return int(dt.timestamp() * 1000) if dt else None -# 查询和响应Schemas class ModelConfigQuery(BaseModel): """模型配置查询Schema""" type: Optional[List[ModelType]] = Field(None, description="模型类型筛选(支持多个)") @@ -129,6 +198,17 @@ class ModelConfigQuery(BaseModel): page: int = Field(1, description="页码", ge=1) pagesize: int = Field(10, description="每页数量", ge=1, le=100) + +# 查询和响应Schemas +class ModelConfigQueryNew(BaseModel): + """模型配置查询Schema""" + type: Optional[List[ModelType]] = Field(None, description="模型类型筛选(支持多个)") + provider: Optional[ModelProvider] = Field(None, description="提供商筛选(通过API Key)") + is_active: Optional[bool] = Field(None, description="激活状态筛选") + is_public: Optional[bool] = Field(None, description="公开状态筛选") + is_composite: Optional[bool] = Field(None, description="组合模型筛选") + search: Optional[str] = Field(None, description="搜索关键词", max_length=255) + class ModelMarketplace(BaseModel): """模型广场响应Schema""" llm_models: List[ModelConfig] = [] @@ -171,4 +251,53 @@ class ModelValidateResponse(BaseModel): # 更新前向引用 -ModelConfig.model_rebuild() \ No newline at end of file +ModelConfig.model_rebuild() + + +# ModelBase Schemas +class ModelBaseCreate(BaseModel): + """创建基础模型Schema""" + name: str = Field(..., description="模型唯一标识", max_length=255) + type: ModelType = Field(..., description="模型类型") + provider: ModelProvider = Field(..., description="提供商") + logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255) + description: Optional[str] = Field(None, description="模型描述") + is_official: bool = Field(True, description="是否供应商官方模型") + tags: List[str] = Field(default_factory=list, description="模型标签") + + +class ModelBaseUpdate(BaseModel): + """更新基础模型Schema""" + name: Optional[str] = Field(None, description="模型唯一标识", max_length=255) + type: Optional[ModelType] = Field(None, description="模型类型") + provider: Optional[ModelProvider] = Field(None, description="提供商") + logo: Optional[str] = Field(None, description="模型logo图片URL", max_length=255) + description: Optional[str] = Field(None, description="模型描述") + is_deprecated: Optional[bool] = Field(None, description="是否弃用") + is_official: Optional[bool] = Field(None, description="是否供应商官方模型") + tags: Optional[List[str]] = Field(None, description="模型标签") + + +class ModelBase(BaseModel): + """基础模型Schema""" + model_config = ConfigDict(from_attributes=True) + + id: uuid.UUID + name: str + type: str + provider: str + logo: Optional[str] + description: Optional[str] + is_deprecated: bool + is_official: bool + tags: List[str] + add_count: int + + +class ModelBaseQuery(BaseModel): + """基础模型查询Schema""" + type: Optional[ModelType] = Field(None, description="模型类型") + provider: Optional[ModelProvider] = Field(None, description="提供商") + is_official: Optional[bool] = Field(None, description="是否官方模型") + is_deprecated: Optional[bool] = Field(None, description="是否弃用") + search: Optional[str] = Field(None, description="搜索关键词", max_length=255) diff --git a/api/app/services/app_statistics_service.py b/api/app/services/app_statistics_service.py new file mode 100644 index 00000000..c164924a --- /dev/null +++ b/api/app/services/app_statistics_service.py @@ -0,0 +1,193 @@ +"""应用统计服务""" +from datetime import datetime, timedelta +from typing import Dict, Any, List +import uuid +from sqlalchemy import func, and_, cast, Date +from sqlalchemy.orm import Session + +from app.models.conversation_model import Conversation, Message +from app.models.end_user_model import EndUser +from app.models.api_key_model import ApiKey, ApiKeyLog +from app.core.exceptions import BusinessException +from app.core.error_codes import BizCode + + +class AppStatisticsService: + """应用统计服务""" + + def __init__(self, db: Session): + self.db = db + + def get_app_statistics( + self, + app_id: uuid.UUID, + workspace_id: uuid.UUID, + start_date: int, + end_date: int + ) -> Dict[str, Any]: + """获取应用统计数据 + + Args: + app_id: 应用ID + workspace_id: 工作空间ID + start_date: 开始时间戳(毫秒) + end_date: 结束时间戳(毫秒) + + Returns: + 统计数据字典 + """ + # 将毫秒时间戳转换为 datetime + start_dt = datetime.fromtimestamp(start_date / 1000) + end_dt = datetime.fromtimestamp(end_date / 1000) + timedelta(days=1) + + # 1. 会话统计 + conversations_stats = self._get_conversations_statistics(app_id, workspace_id, start_dt, end_dt) + + # 2. 新增用户统计 + users_stats = self._get_new_users_statistics(app_id, start_dt, end_dt) + + # 3. API调用统计 + api_stats = self._get_api_calls_statistics(app_id, start_dt, end_dt) + + # 4. Token消耗统计 + token_stats = self._get_token_statistics(app_id, start_dt, end_dt) + + return { + "daily_conversations": conversations_stats["daily"], + "total_conversations": conversations_stats["total"], + "daily_new_users": users_stats["daily"], + "total_new_users": users_stats["total"], + "daily_api_calls": api_stats["daily"], + "total_api_calls": api_stats["total"], + "daily_tokens": token_stats["daily"], + "total_tokens": token_stats["total"] + } + + def _get_conversations_statistics( + self, + app_id: uuid.UUID, + workspace_id: uuid.UUID, + start_dt: datetime, + end_dt: datetime + ) -> Dict[str, Any]: + """获取会话统计""" + # 每日会话数 + daily_query = self.db.query( + cast(Conversation.created_at, Date).label('date'), + func.count(Conversation.id).label('count') + ).filter( + and_( + Conversation.app_id == app_id, + Conversation.workspace_id == workspace_id, + Conversation.created_at >= start_dt, + Conversation.created_at < end_dt + ) + ).group_by(cast(Conversation.created_at, Date)).all() + + daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query] + total = sum(row["count"] for row in daily_data) + + return {"daily": daily_data, "total": total} + + def _get_new_users_statistics( + self, + app_id: uuid.UUID, + start_dt: datetime, + end_dt: datetime + ) -> Dict[str, Any]: + """获取新增用户统计""" + # 每日新增用户数 + daily_query = self.db.query( + cast(EndUser.created_at, Date).label('date'), + func.count(EndUser.id).label('count') + ).filter( + and_( + EndUser.app_id == app_id, + EndUser.created_at >= start_dt, + EndUser.created_at < end_dt + ) + ).group_by(cast(EndUser.created_at, Date)).all() + + daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query] + total = sum(row["count"] for row in daily_data) + + return {"daily": daily_data, "total": total} + + def _get_api_calls_statistics( + self, + app_id: uuid.UUID, + start_dt: datetime, + end_dt: datetime + ) -> Dict[str, Any]: + """获取API调用统计""" + # 每日API调用次数 + daily_query = self.db.query( + cast(ApiKeyLog.created_at, Date).label('date'), + func.count(ApiKeyLog.id).label('count') + ).join( + ApiKey, ApiKeyLog.api_key_id == ApiKey.id + ).filter( + and_( + ApiKey.resource_id == app_id, + ApiKeyLog.created_at >= start_dt, + ApiKeyLog.created_at < end_dt + ) + ).group_by(cast(ApiKeyLog.created_at, Date)).all() + + daily_data = [{"date": str(row.date), "count": row.count} for row in daily_query] + total = sum(row["count"] for row in daily_data) + + return {"daily": daily_data, "total": total} + + def _get_token_statistics( + self, + app_id: uuid.UUID, + start_dt: datetime, + end_dt: datetime + ) -> Dict[str, Any]: + """获取Token消耗统计(从Message的meta_data中提取)""" + from sqlalchemy import text + + # 查询所有相关消息的token使用情况 + # meta_data中可能包含: {"usage": {"total_tokens": 100}} 或 {"tokens": 100} + daily_query = self.db.query( + cast(Message.created_at, Date).label('date'), + Message.meta_data + ).join( + Conversation, Message.conversation_id == Conversation.id + ).filter( + and_( + Conversation.app_id == app_id, + Message.created_at >= start_dt, + Message.created_at < end_dt, + Message.meta_data.isnot(None) + ) + ).all() + + # 按日期聚合token + daily_tokens = {} + for row in daily_query: + date_str = str(row.date) + meta = row.meta_data or {} + + # 提取token数量(支持多种格式) + tokens = 0 + if isinstance(meta, dict): + # 格式1: {"usage": {"total_tokens": 100}} + if "usage" in meta and isinstance(meta["usage"], dict): + tokens = meta["usage"].get("total_tokens", 0) + # 格式2: {"tokens": 100} + elif "tokens" in meta: + tokens = meta.get("tokens", 0) + # 格式3: {"total_tokens": 100} + elif "total_tokens" in meta: + tokens = meta.get("total_tokens", 0) + + if date_str not in daily_tokens: + daily_tokens[date_str] = 0 + daily_tokens[date_str] += int(tokens) + + daily_data = [{"date": date, "tokens": tokens} for date, tokens in sorted(daily_tokens.items()) if tokens != 0] + total = sum(row["tokens"] for row in daily_data) + + return {"daily": daily_data, "total": total} diff --git a/api/app/services/draft_run_service.py b/api/app/services/draft_run_service.py index 9766eec0..14acfd52 100644 --- a/api/app/services/draft_run_service.py +++ b/api/app/services/draft_run_service.py @@ -16,6 +16,7 @@ from app.core.exceptions import BusinessException from app.core.logging_config import get_business_logger from app.core.rag.nlp.search import knowledge_retrieval from app.models import AgentConfig, ModelApiKey, ModelConfig +from app.repositories.model_repository import ModelApiKeyRepository from app.repositories.tool_repository import ToolRepository from app.schemas.prompt_schema import PromptMessageRole, render_prompt_message from app.services import task_service @@ -724,17 +725,21 @@ class DraftRunService: Raises: BusinessException: 当没有可用的 API Key 时 """ - stmt = ( - select(ModelApiKey) - .where( - ModelApiKey.model_config_id == model_config_id, - ModelApiKey.is_active.is_(True) - ) - .order_by(ModelApiKey.priority.desc()) - .limit(1) - ) - - api_key = self.db.scalars(stmt).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id) + # stmt = ( + # select(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ) + # .where( + # ModelConfig.id == model_config_id, + # ModelApiKey.is_active.is_(True) + # ) + # .order_by(ModelApiKey.priority.desc()) + # .limit(1) + # ) + # + # api_key = self.db.scalars(stmt).first() + api_key = api_keys[0] if api_keys else None if not api_key: raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING) diff --git a/api/app/services/llm_router.py b/api/app/services/llm_router.py index 9ef9dbb1..9e102ac3 100644 --- a/api/app/services/llm_router.py +++ b/api/app/services/llm_router.py @@ -5,6 +5,7 @@ import uuid from typing import Dict, Any, List, Optional, Tuple from sqlalchemy.orm import Session +from app.repositories.model_repository import ModelApiKeyRepository from app.services.conversation_state_manager import ConversationStateManager from app.models import ModelConfig, AgentConfig from app.core.logging_config import get_business_logger @@ -382,11 +383,14 @@ class LLMRouter: from app.core.models.base import RedBearModelConfig from app.models import ModelApiKey, ModelType - # 获取 API Key 配置 - api_key_config = self.db.query(ModelApiKey).filter( - ModelApiKey.model_config_id == self.routing_model_config.id, - ModelApiKey.is_active - ).first() + # 获取 API Key 配置(通过关联关系) + # api_key_config = self.db.query(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ).filter(ModelConfig.id == self.routing_model_config.id, + # ModelApiKey.is_active == True + # ).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, self.routing_model_config.id) + api_key_config = api_keys[0] if api_keys else None if not api_key_config: raise Exception("路由模型没有可用的 API Key") @@ -419,6 +423,9 @@ class LLMRouter: # 调用模型 response = await llm.ainvoke(prompt) + + from app.services.model_service import ModelApiKeyService + ModelApiKeyService.record_api_key_usage(self.db, api_key_config.id) # 提取响应内容 if hasattr(response, 'content'): diff --git a/api/app/services/memory_config_service.py b/api/app/services/memory_config_service.py index 26b86b71..e09cf67f 100644 --- a/api/app/services/memory_config_service.py +++ b/api/app/services/memory_config_service.py @@ -338,7 +338,7 @@ class MemoryConfigService: "provider": api_config.provider, "api_key": api_config.api_key, "base_url": api_config.api_base, - "model_config_id": api_config.model_config_id, + "model_config_id": str(config.id), "type": config.type, "timeout": settings.LLM_TIMEOUT, "max_retries": settings.LLM_MAX_RETRIES, @@ -370,7 +370,7 @@ class MemoryConfigService: "provider": api_config.provider, "api_key": api_config.api_key, "base_url": api_config.api_base, - "model_config_id": api_config.model_config_id, + "model_config_id": str(config.id), "type": config.type, "timeout": 120.0, "max_retries": 5, diff --git a/api/app/services/model_service.py b/api/app/services/model_service.py index e94a889b..5b2ab7e6 100644 --- a/api/app/services/model_service.py +++ b/api/app/services/model_service.py @@ -1,3 +1,4 @@ +from datetime import datetime from sqlalchemy.orm import Session from typing import List, Optional, Dict, Any import uuid @@ -6,11 +7,11 @@ import time import asyncio from app.models.models_model import ModelConfig, ModelApiKey, ModelType -from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository +from app.repositories.model_repository import ModelConfigRepository, ModelApiKeyRepository, ModelBaseRepository from app.schemas import model_schema from app.schemas.model_schema import ( ModelConfigCreate, ModelConfigUpdate, ModelApiKeyCreate, ModelApiKeyUpdate, - ModelConfigQuery, ModelStats + ModelConfigQuery, ModelStats, ModelConfigQueryNew ) from app.core.logging_config import get_business_logger from app.schemas.response_schema import PageData, PageMeta @@ -47,6 +48,26 @@ class ModelConfigService: items=[model_schema.ModelConfig.model_validate(model) for model in models] ) + @staticmethod + def get_model_list_new(db: Session, query: ModelConfigQueryNew, tenant_id: uuid.UUID | None = None) -> List[dict]: + """获取模型配置列表""" + provider_groups, total = ModelConfigRepository.get_list_new(db, query, tenant_id=tenant_id) + + items = [] + for provider, models in provider_groups.items(): + # 验证每个模型并封装分组信息 + validated_models = [model_schema.ModelConfig.model_validate(model) for model in models] + tags = list({model.type for model in validated_models}) + group_item = { + "provider": provider, # 服务商名称 + "logo": validated_models[0].logo, + "tags": tags, + "models": validated_models # 该服务商下的所有模型 + } + items.append(group_item) + + return items + @staticmethod def get_model_by_name(db: Session, name: str, tenant_id: uuid.UUID | None = None) -> ModelConfig: """根据名称获取模型配置""" @@ -228,37 +249,39 @@ class ModelConfigService: # 验证配置 if not model_data.skip_validation and model_data.api_keys: - api_key_data = model_data.api_keys - validation_result = await ModelConfigService.validate_model_config( - db=db, - model_name=api_key_data.model_name, - provider=api_key_data.provider, - api_key=api_key_data.api_key, - api_base=api_key_data.api_base, - model_type=model_data.type, # 传递模型类型 - test_message="Hello" - ) - if not validation_result["valid"]: - raise BusinessException( - f"模型配置验证失败: {validation_result['error']}", - BizCode.INVALID_PARAMETER + api_key_data_list = model_data.api_keys + for api_key_data in api_key_data_list: + validation_result = await ModelConfigService.validate_model_config( + db=db, + model_name=api_key_data.model_name, + provider=api_key_data.provider, + api_key=api_key_data.api_key, + api_base=api_key_data.api_base, + model_type=model_data.type, # 传递模型类型 + test_message="Hello" ) + if not validation_result["valid"]: + raise BusinessException( + f"模型配置验证失败: {validation_result['error']}", + BizCode.INVALID_PARAMETER + ) # 事务处理 - api_key_data = model_data.api_keys - model_config_data = model_data.dict(exclude={"api_keys", "skip_validation"}) + api_key_datas = model_data.api_keys + model_config_data = model_data.model_dump(exclude={"api_keys", "skip_validation"}) # 添加租户ID model_config_data["tenant_id"] = tenant_id model = ModelConfigRepository.create(db, model_config_data) db.flush() # 获取生成的 ID - if api_key_data: - api_key_create_schema = ModelApiKeyCreate( - model_config_id=model.id, - **api_key_data.dict() - ) - ModelApiKeyRepository.create(db, api_key_create_schema) + if api_key_datas: + for api_key_data in api_key_datas: + api_key_create_schema = ModelApiKeyCreate( + model_config_ids=[model.id], + **api_key_data.model_dump() + ) + ModelApiKeyRepository.create(db, api_key_create_schema) db.commit() db.refresh(model) @@ -280,6 +303,112 @@ class ModelConfigService: db.refresh(model) return model + @staticmethod + async def create_composite_model(db: Session, model_data: model_schema.CompositeModelCreate, tenant_id: uuid.UUID) -> ModelConfig: + """创建组合模型""" + if ModelConfigRepository.get_by_name(db, model_data.name, tenant_id=tenant_id): + raise BusinessException("模型名称已存在", BizCode.DUPLICATE_NAME) + + # 验证所有 API Key 存在且类型匹配 + for api_key_id in model_data.api_key_ids: + api_key = ModelApiKeyRepository.get_by_id(db, api_key_id) + if not api_key: + raise BusinessException(f"API Key {api_key_id} 不存在", BizCode.NOT_FOUND) + + # 检查 API Key 关联的模型配置类型 + for model_config in api_key.model_configs: + # chat 和 llm 类型可以兼容 + compatible_types = {ModelType.LLM, ModelType.CHAT} + config_type = model_config.type + request_type = model_data.type + + if not (config_type == request_type or + (config_type in compatible_types and request_type in compatible_types)): + raise BusinessException( + f"API Key {api_key_id} 关联的模型类型 ({model_config.type}) 与组合模型类型 ({model_data.type}) 不匹配", + BizCode.INVALID_PARAMETER + ) + # if model_config.is_composite: + # raise BusinessException( + # f"API Key {api_key_id} 关联的模型是组合模型,不能用于创建新的组合模型", + # BizCode.INVALID_PARAMETER + # ) + + # 创建组合模型 + model_config_data = { + "tenant_id": tenant_id, + "name": model_data.name, + "type": model_data.type, + "logo": model_data.logo, + "description": model_data.description, + "provider": "composite", + "config": model_data.config, + "is_active": model_data.is_active, + "is_public": model_data.is_public, + "is_composite": True + } + + model = ModelConfigRepository.create(db, model_config_data) + db.flush() + + # 关联 API Keys + for api_key_id in model_data.api_key_ids: + api_key = ModelApiKeyRepository.get_by_id(db, api_key_id) + if api_key: + model.api_keys.append(api_key) + + db.commit() + db.refresh(model) + return model + + @staticmethod + async def update_composite_model(db: Session, model_id: uuid.UUID, model_data: model_schema.CompositeModelCreate, tenant_id: uuid.UUID) -> ModelConfig: + """更新组合模型""" + existing_model = ModelConfigRepository.get_by_id(db, model_id, tenant_id=tenant_id) + if not existing_model: + raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND) + + if not existing_model.is_composite: + raise BusinessException("该模型不是组合模型", BizCode.INVALID_PARAMETER) + + # 验证所有 API Key 存在且类型匹配 + for api_key_id in model_data.api_key_ids: + api_key = ModelApiKeyRepository.get_by_id(db, api_key_id) + if not api_key: + raise BusinessException(f"API Key {api_key_id} 不存在", BizCode.NOT_FOUND) + + for model_config in api_key.model_configs: + compatible_types = {ModelType.LLM, ModelType.CHAT} + config_type = model_config.type + request_type = model_data.type + + if not (config_type == request_type or + (config_type in compatible_types and request_type in compatible_types)): + raise BusinessException( + f"API Key {api_key_id} 关联的模型类型 ({model_config.type}) 与组合模型类型 ({model_data.type}) 不匹配", + BizCode.INVALID_PARAMETER + ) + + # 更新基本信息 + existing_model.name = model_data.name + existing_model.type = model_data.type + existing_model.logo = model_data.logo + existing_model.description = model_data.description + existing_model.config = model_data.config + existing_model.is_active = model_data.is_active + existing_model.is_public = model_data.is_public + + # 更新 API Keys 关联 + existing_model.api_keys.clear() + for api_key_id in model_data.api_key_ids: + api_key = ModelApiKeyRepository.get_by_id(db, api_key_id) + if api_key: + existing_model.api_keys.append(api_key) + + db.commit() + db.refresh(existing_model) + return existing_model + @staticmethod def delete_model(db: Session, model_id: uuid.UUID, tenant_id: uuid.UUID | None = None) -> bool: """删除模型配置""" @@ -324,27 +453,132 @@ class ModelApiKeyService: return ModelApiKeyRepository.get_by_model_config(db, model_config_id, is_active) @staticmethod - async def create_api_key(db: Session, api_key_data: ModelApiKeyCreate) -> ModelApiKey: - """创建API Key""" - model_config = ModelConfigRepository.get_by_id(db, api_key_data.model_config_id) - if not model_config: - raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND) - - validation_result = await ModelConfigService.validate_model_config( + async def create_api_key_by_provider(db: Session, data: model_schema.ModelApiKeyCreateByProvider) -> List[ModelApiKey]: + """根据provider为多个ModelConfig创建API Key""" + created_keys = [] + + for model_config_id in data.model_config_ids: + model_config = ModelConfigRepository.get_by_id(db, model_config_id) + if not model_config: + continue + + # 从ModelBase获取model_name + model_name = model_config.model_base.name if model_config.model_base else model_config.name + + # 检查是否存在API Key(包括软删除) + existing_key = db.query(ModelApiKey).filter( + ModelApiKey.api_key == data.api_key, + ModelApiKey.provider == data.provider, + ModelApiKey.model_name == model_name + ).first() + + if existing_key: + # 如果已存在,重新激活并更新 + if existing_key.is_active: + continue + existing_key.is_active = True + existing_key.api_base = data.api_base + existing_key.description = data.description + existing_key.config = data.config + existing_key.priority = data.priority + existing_key.model_name = model_name + + # 检查是否已关联该模型配置 + if model_config not in existing_key.model_configs: + existing_key.model_configs.append(model_config) + + created_keys.append(existing_key) + continue + + # 验证配置 + validation_result = await ModelConfigService.validate_model_config( db=db, - model_name=api_key_data.model_name, - provider=api_key_data.provider, - api_key=api_key_data.api_key, - api_base=api_key_data.api_base, - model_type=model_config.type, # 传递模型类型 + model_name=model_name, + provider=data.provider, + api_key=data.api_key, + api_base=data.api_base, + model_type=model_config.type, test_message="Hello" ) - print(validation_result) - if not validation_result["valid"]: + if not validation_result["valid"]: raise BusinessException( f"模型配置验证失败: {validation_result['error']}", BizCode.INVALID_PARAMETER ) + + # 创建API Key + api_key_data = ModelApiKeyCreate( + model_config_ids=[model_config_id], + model_name=model_name, + description=data.description, + provider=data.provider, + api_key=data.api_key, + api_base=data.api_base, + config=data.config, + is_active=data.is_active, + priority=data.priority + ) + api_key_obj = ModelApiKeyRepository.create(db, api_key_data) + created_keys.append(api_key_obj) + + if created_keys: + db.commit() + for key in created_keys: + db.refresh(key) + + return created_keys + + @staticmethod + async def create_api_key(db: Session, api_key_data: ModelApiKeyCreate) -> ModelApiKey: + # 验证所有关联的模型配置是否存在 + if api_key_data.model_config_ids: + for model_config_id in api_key_data.model_config_ids: + model_config = ModelConfigRepository.get_by_id(db, model_config_id) + if not model_config: + raise BusinessException("模型配置不存在", BizCode.MODEL_NOT_FOUND) + + # 检查API Key是否已存在(包括软删除) + existing_key = db.query(ModelApiKey).filter( + ModelApiKey.api_key == api_key_data.api_key, + ModelApiKey.provider == api_key_data.provider, + ModelApiKey.model_name == api_key_data.model_name + ).first() + + if existing_key: + if existing_key.is_active: + # 如果已激活,跳过 + raise BusinessException("该API Key已存在", BizCode.DUPLICATE_NAME) + # 如果已存在,重新激活并更新 + existing_key.is_active = True + existing_key.api_base = api_key_data.api_base + existing_key.description = api_key_data.description + existing_key.config = api_key_data.config + existing_key.priority = api_key_data.priority + existing_key.model_name = api_key_data.model_name + + # 检查是否已关联该模型配置 + if model_config not in existing_key.model_configs: + existing_key.model_configs.append(model_config) + + db.commit() + db.refresh(existing_key) + return existing_key + + # 验证配置 + validation_result = await ModelConfigService.validate_model_config( + db=db, + model_name=api_key_data.model_name, + provider=api_key_data.provider, + api_key=api_key_data.api_key, + api_base=api_key_data.api_base, + model_type=model_config.type, + test_message="Hello" + ) + if not validation_result["valid"]: + raise BusinessException( + f"模型配置验证失败: {validation_result['error']}", + BizCode.INVALID_PARAMETER + ) api_key = ModelApiKeyRepository.create(db, api_key_data) db.commit() @@ -359,21 +593,19 @@ class ModelApiKeyService: raise BusinessException("API Key不存在", BizCode.NOT_FOUND) # 获取关联的模型配置以获取模型类型 - model_config = ModelConfigRepository.get_by_id(db, existing_api_key.model_config_id) - if not model_config: - raise BusinessException("关联的模型配置不存在", BizCode.MODEL_NOT_FOUND) - - validation_result = await ModelConfigService.validate_model_config( + if existing_api_key.model_configs: + model_config = existing_api_key.model_configs[0] + + validation_result = await ModelConfigService.validate_model_config( db=db, - model_name=api_key_data.model_name, - provider=api_key_data.provider, - api_key=api_key_data.api_key, - api_base=api_key_data.api_base, - model_type=model_config.type, # 传递模型类型 + model_name=api_key_data.model_name or existing_api_key.model_name, + provider=api_key_data.provider or existing_api_key.provider, + api_key=api_key_data.api_key or existing_api_key.api_key, + api_base=api_key_data.api_base or existing_api_key.api_base, + model_type=model_config.type, test_message="Hello" ) - print(validation_result) - if not validation_result["valid"]: + if not validation_result["valid"]: raise BusinessException( f"模型配置验证失败: {validation_result['error']}", BizCode.INVALID_PARAMETER @@ -417,3 +649,84 @@ class ModelApiKeyService: if api_kes and len(api_kes) > 0: return api_kes[0] raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING) + + + +class ModelBaseService: + """基础模型服务""" + + @staticmethod + def get_model_base_list(db: Session, query: model_schema.ModelBaseQuery, tenant_id: uuid.UUID = None) -> List: + models = ModelBaseRepository.get_list(db, query) + + provider_groups = {} + for m in models: + model_dict = model_schema.ModelBase.model_validate(m).model_dump() + if tenant_id: + model_dict['is_added'] = ModelBaseRepository.check_added_by_tenant(db, m.id, tenant_id) + + provider = m.provider + if provider not in provider_groups: + provider_groups[provider] = { + "provider": provider, + "models": [] + } + provider_groups[provider]["models"].append(model_dict) + + return list(provider_groups.values()) + + @staticmethod + def get_model_base_by_id(db: Session, model_base_id: uuid.UUID): + model = ModelBaseRepository.get_by_id(db, model_base_id) + if not model: + raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND) + return model + + @staticmethod + def create_model_base(db: Session, data: model_schema.ModelBaseCreate): + model_base = ModelBaseRepository.create(db, data.model_dump()) + db.commit() + db.refresh(model_base) + return model_base + + @staticmethod + def update_model_base(db: Session, model_base_id: uuid.UUID, data: model_schema.ModelBaseUpdate): + model_base = ModelBaseRepository.update(db, model_base_id, data.model_dump(exclude_unset=True)) + if not model_base: + raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND) + db.commit() + db.refresh(model_base) + return model_base + + @staticmethod + def delete_model_base(db: Session, model_base_id: uuid.UUID) -> bool: + success = ModelBaseRepository.delete(db, model_base_id) + if not success: + raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND) + db.commit() + return success + + @staticmethod + def add_model_from_plaza(db: Session, model_base_id: uuid.UUID, tenant_id: uuid.UUID) -> ModelConfig: + model_base = ModelBaseRepository.get_by_id(db, model_base_id) + if not model_base: + raise BusinessException("基础模型不存在", BizCode.MODEL_NOT_FOUND) + + if ModelBaseRepository.check_added_by_tenant(db, model_base_id, tenant_id): + raise BusinessException("模型已添加", BizCode.DUPLICATE_NAME) + + model_config_data = { + "model_id": model_base_id, + "tenant_id": tenant_id, + "name": model_base.name, + "provider": model_base.provider, + "type": model_base.type, + "logo": model_base.logo, + "description": model_base.description, + "is_composite": False + } + model_config = ModelConfigRepository.create(db, model_config_data) + ModelBaseRepository.increment_add_count(db, model_base_id) + db.commit() + db.refresh(model_config) + return model_config diff --git a/api/app/services/multi_agent_orchestrator.py b/api/app/services/multi_agent_orchestrator.py index 4bcd28cd..d9062eaf 100644 --- a/api/app/services/multi_agent_orchestrator.py +++ b/api/app/services/multi_agent_orchestrator.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import Session from app.models import MultiAgentConfig, AgentConfig, ModelConfig from app.models.multi_agent_model import AggregationStrategy, OrchestrationMode +from app.repositories.model_repository import ModelApiKeyRepository from app.services.agent_registry import AgentRegistry from app.services.master_agent_router import MasterAgentRouter from app.services.conversation_state_manager import ConversationStateManager @@ -2546,10 +2547,14 @@ class MultiAgentOrchestrator: return self._smart_merge_results(results, strategy) # 获取 API Key 配置 - api_key_config = self.db.query(ModelApiKey).filter( - ModelApiKey.model_config_id == default_model_config_id, - ModelApiKey.is_active.is_(True) - ).first() + # api_key_config = self.db.query(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ).filter( + # ModelConfig.id == default_model_config_id, + # ModelApiKey.is_active.is_(True) + # ).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, default_model_config_id) + api_key_config = api_keys[0] if api_keys else None if not api_key_config: logger.warning("Master Agent 没有可用的 API Key,使用简单整合") @@ -2703,10 +2708,14 @@ class MultiAgentOrchestrator: return # 获取 API Key 配置 - api_key_config = self.db.query(ModelApiKey).filter( - ModelApiKey.model_config_id == default_model_config_id, - ModelApiKey.is_active.is_(True) - ).first() + # api_key_config = self.db.query(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ).filter( + # ModelConfig.id == default_model_config_id, + # ModelApiKey.is_active.is_(True) + # ).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, default_model_config_id) + api_key_config = api_keys[0] if api_keys else None if not api_key_config: logger.warning("Master Agent 没有可用的 API Key,使用简单整合") diff --git a/api/app/services/shared_chat_service.py b/api/app/services/shared_chat_service.py index 5eee5edc..1d012088 100644 --- a/api/app/services/shared_chat_service.py +++ b/api/app/services/shared_chat_service.py @@ -4,6 +4,8 @@ import time import asyncio from typing import Optional, Dict, Any, AsyncGenerator from sqlalchemy.orm import Session + +from app.repositories.model_repository import ModelApiKeyRepository from app.services.memory_konwledges_server import write_rag from app.models import ReleaseShare, AppRelease, Conversation from app.services.conversation_service import ConversationService @@ -164,16 +166,20 @@ class SharedChatService: raise ResourceNotFoundException("模型配置", str(model_config_id)) # 获取 API Key - stmt = ( - select(ModelApiKey) - .where( - ModelApiKey.model_config_id == model_config_id, - ModelApiKey.is_active.is_(True) - ) - .order_by(ModelApiKey.priority.desc()) - .limit(1) - ) - api_key_obj = self.db.scalars(stmt).first() + # stmt = ( + # select(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ) + # .where( + # ModelConfig.id == model_config_id, + # ModelApiKey.is_active.is_(True) + # ) + # .order_by(ModelApiKey.priority.desc()) + # .limit(1) + # ) + # api_key_obj = self.db.scalars(stmt).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id) + api_key_obj = api_keys[0] if api_keys else None if not api_key_obj: raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING) @@ -358,16 +364,20 @@ class SharedChatService: raise ResourceNotFoundException("模型配置", str(model_config_id)) # 获取 API Key - stmt = ( - select(ModelApiKey) - .where( - ModelApiKey.model_config_id == model_config_id, - ModelApiKey.is_active.is_(True) - ) - .order_by(ModelApiKey.priority.desc()) - .limit(1) - ) - api_key_obj = self.db.scalars(stmt).first() + # stmt = ( + # select(ModelApiKey).join( + # ModelConfig, ModelApiKey.model_configs + # ) + # .where( + # ModelConfig.id == model_config_id, + # ModelApiKey.is_active.is_(True) + # ) + # .order_by(ModelApiKey.priority.desc()) + # .limit(1) + # ) + # api_key_obj = self.db.scalars(stmt).first() + api_keys = ModelApiKeyRepository.get_by_model_config(self.db, model_config_id) + api_key_obj = api_keys[0] if api_keys else None if not api_key_obj: raise BusinessException("没有可用的 API Key", BizCode.AGENT_CONFIG_MISSING) diff --git a/api/app/version_info.json b/api/app/version_info.json index bee52989..86a5e33e 100644 --- a/api/app/version_info.json +++ b/api/app/version_info.json @@ -5,11 +5,12 @@ "releaseDate": "2026-1-23", "upgradePosition": "\uD83D\uDC3B 本次更新主要优化使用体验和修复已知问题,让系统更稳定、更好用。", "coreUpgrades": [ - "1. 工作流更好用了\n* 界面更清晰,一眼看懂怎么配置\n* 新增节点输出变量展示,方便其他节点引用\n* 修复了几个影响体验的bug", - "2. 智能体配置更简单\n* 提示词和变量联动更顺畅\n* 配置界面重新整理,找功能更方便", - "3. 记忆系统更稳定\n* 优化了情绪记忆和隐性记忆的缓存更新\n* 修复了记忆配置页面的报错问题\n* 现在能自动识别用户和AI的身份了", - "4. 知识库体验提升\n* 修复了文档解析异常的问题\n* 上传文档时能看到处理进度了\n* 取消了操作也不会报错了", - "5. 系统整体更可靠\n* 修复了新用户访问跳转问题\n* 流式接口更稳定,长对话不断线\n* 调整了菜单顺序,操作更顺手\n", + "1. 工作流更好用了
* 界面更清晰,一眼看懂怎么配置
* 新增节点输出变量展示,方便其他节点引用
* 修复了几个影响体验的bug", + "2. 智能体配置更简单
* 提示词和变量联动更顺畅
* 配置界面重新整理,找功能更方便", + "3. 记忆系统更稳定
* 优化了情绪记忆和隐性记忆的缓存更新
* 修复了记忆配置页面的报错问题
* 现在能自动识别用户和AI的身份了", + "4. 知识库体验提升
* 修复了文档解析异常的问题
* 上传文档时能看到处理进度了
* 取消了操作也不会报错了", + "5. 系统整体更可靠
* 修复了新用户访问跳转问题
* 流式接口更稳定,长对话不断线
* 调整了菜单顺序,操作更顺手", + "
", "这次更新虽然不大,但让记忆熊的基础更扎实、体验更流畅。我们继续努力,让AI记忆更好用!", "记忆熊,记得更牢,用得更好。\uD83D\uDC3B✨" ] @@ -19,12 +20,13 @@ "releaseDate": "2026-1-23", "upgradePosition": "\uD83D\uDC3B This update focuses on improving usability and fixing known issues, making the system more stable and easier to use overall.", "coreUpgrades": [ - "1. Improved Workflow Experience\nCleaner, more intuitive UI for easier configuration at a glance\nAdded visibility of node output variables, making them easier to reference in downstream nodes\nFixed several usability-related bugs that affected the workflow experience", - "2. Simpler Agent Configuration\nSmoother linkage between prompts and variables\nReorganized configuration layout for easier navigation and better clarity", - "3. More Stable Memory System\nOptimized cache refresh for emotional memory and implicit memory\nFixed error issues on the memory configuration page\nThe system can now automatically distinguish between user and AI roles", - "4. Enhanced Knowledge Base Experience\nFixed issues with document parsing failures\nUpload progress is now displayed during document processing\nCanceling an upload no longer triggers errors", - "5. Overall System Reliability Improvements\nFixed redirect issues affecting new users\nImproved stability of streaming APIs to prevent interruptions during long conversations\nAdjusted menu ordering for a smoother and more intuitive workflow\n", - "Although this is a relatively small update, it strengthens MemoryBear’s foundation and delivers a noticeably smoother experience.\nWe’ll keep refining the system to make AI memory more powerful and easier to use.", + "1. Improved Workflow Experience
* Cleaner, more intuitive UI for easier configuration at a glance
* Added visibility of node output variables, making them easier to reference in downstream nodes
* Fixed several usability-related bugs that affected the workflow experience", + "2. Simpler Agent Configuration
* Smoother linkage between prompts and variables
* Reorganized configuration layout for easier navigation and better clarity", + "3. More Stable Memory System
* Optimized cache refresh for emotional memory and implicit memory
* Fixed error issues on the memory configuration page
* The system can now automatically distinguish between user and AI roles", + "4. Enhanced Knowledge Base Experience
* Fixed issues with document parsing failures
* Upload progress is now displayed during document processing
* Canceling an upload no longer triggers errors", + "5. Overall System Reliability Improvements
* Fixed redirect issues affecting new users
* Improved stability of streaming APIs to prevent interruptions during long conversations
* Adjusted menu ordering for a smoother and more intuitive workflow", + "
", + "Although this is a relatively small update, it strengthens MemoryBear’s foundation and delivers a noticeably smoother experience. We’ll keep refining the system to make AI memory more powerful and easier to use.", "MemoryBear — remember better, work smarter. \uD83D\uDC3B✨" ] } @@ -35,10 +37,10 @@ "releaseDate": "2026-1-16", "upgradePosition": "本次为架构升级,核心目标是把\"被动存储\"升级为\"主动认知\",让系统具备情绪感知、情景理解与类人记忆机制,为后续多智能体协作与专业场景落地奠定底座。", "coreUpgrades": [ - "记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环", - "可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。", - "多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。", - "Agent集群内置\"记忆-知识-工具-审核\"四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。" + "1. 记忆详情:拟人记忆——情绪引擎、情景记忆、短期记忆、工作记忆、感知记忆、显性记忆、隐性记忆,并配套类脑遗忘机制,实现从感知→情绪→情景→长期沉淀的完整人类记忆闭环", + "2. 可视化工作流:拖拽式节点编排(LLM、知识库、逻辑、工具),业务落地周期由天缩至小时。", + "3. 多模态知识处理:PDF、PPT、MP3、MP4 一键解析,时间感知检索准确率 94.3%,问答对数据即插即用。", + "4. Agent集群内置\"记忆-知识-工具-审核\"四类角色模板,用户一键生成;主控Agent把复杂任务拆为子任务并行分发,再靠情景记忆统一消解冲突、校验一致性,输出完整报告。" ] }, "introduction_en": { @@ -46,10 +48,10 @@ "releaseDate": "2026-1-16", "upgradePosition": "This release marks a foundational upgrade to the system’s cognitive architecture. The core objective is to evolve the platform from passive information storage into active cognitive intelligence—enabling emotional awareness, situational understanding, and human-like memory mechanisms. This upgrade lays the groundwork for future multi-agent collaboration and domain-specific, production-grade AI applications.", "coreUpgrades": [ - "Human-Like Memory Architecture: A comprehensive, human-inspired memory system is introduced, encompassing emotional processing, situational memory, short-term and working memory, perceptual memory, as well as explicit and implicit memory. Combined with brain-inspired forgetting mechanisms, the system now supports a complete cognitive loop—from perception → emotion → context → long-term consolidation, closely mirroring human memory formation.", - "Visual Workflow Orchestration: A fully visual, drag-and-drop workflow enables modular composition of LLMs, knowledge bases, logic, and tools. This dramatically reduces the time required to move from experimentation to production—from days to hours.", - "Multimodal Knowledge Processing: The system now supports one-click parsing and ingestion of PDF, PPT, MP3, and MP4 content. With time-aware retrieval accuracy reaching 94.3%, structured Q&A data becomes instantly usable for downstream reasoning and generation.", - "Built-in Agent Clusters: Predefined role templates across four categories—Memory, Knowledge, Tools, and Review—can be generated with a single click. A Coordinator Agent decomposes complex tasks into parallel subtasks, while situational memory is used to resolve conflicts, validate consistency, and synthesize outputs into a coherent, end-to-end report." + "1. Human-Like Memory Architecture: A comprehensive, human-inspired memory system is introduced, encompassing emotional processing, situational memory, short-term and working memory, perceptual memory, as well as explicit and implicit memory. Combined with brain-inspired forgetting mechanisms, the system now supports a complete cognitive loop—from perception → emotion → context → long-term consolidation, closely mirroring human memory formation.", + "2. Visual Workflow Orchestration: A fully visual, drag-and-drop workflow enables modular composition of LLMs, knowledge bases, logic, and tools. This dramatically reduces the time required to move from experimentation to production—from days to hours.", + "3. Multimodal Knowledge Processing: The system now supports one-click parsing and ingestion of PDF, PPT, MP3, and MP4 content. With time-aware retrieval accuracy reaching 94.3%, structured Q&A data becomes instantly usable for downstream reasoning and generation.", + "4. Built-in Agent Clusters: Predefined role templates across four categories—Memory, Knowledge, Tools, and Review—can be generated with a single click. A Coordinator Agent decomposes complex tasks into parallel subtasks, while situational memory is used to resolve conflicts, validate consistency, and synthesize outputs into a coherent, end-to-end report." ] } }, @@ -59,16 +61,17 @@ "releaseDate": "2025-12-01", "upgradePosition": "这是一款专注于管理和利用AI记忆的工具,支持RAG和知识图谱两种主流存储方式,旨在为AI应用提供持久化、结构化的\"记忆\"能力。", "coreUpgrades": [ - "记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。", - "记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。", - "知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。", - "全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。", - "测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。", - "记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。", - "集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。", - "界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。", - "起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。", - "版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。", + "1. 记忆空间:用户可以创建独立的空间来隔离不同记忆,并灵活选择存储方式。", + "2. 记忆配置:简化了配置流程,内置自动提取关键信息的\"记忆萃取\"和管理生命周期的\"遗忘\"引擎。", + "3. 知识检索:提供语义、分词和混合三种检索模式,并支持多种参数微调和结果重排序,以提升召回效果。", + "4. 全局管理:支持统一设置默认检索参数,并可一键应用到所有知识库。", + "5. 测试与调试:内置\"召回测试\"功能,方便用户实时验证检索效果并调整参数,支持通过分享码与他人协作。", + "6. 记忆洞察:可查看详细的对话记录、用户画像和分析报告,帮助理解AI的\"记忆\"内容。", + "7. 集成与管理:提供API Key用于系统集成,并包含基本的用户管理功能。", + "8. 界面与体验:采用现代化的卡片式布局和渐变色设计,注重交互的流畅性和视觉美感。", + "9. 起步与使用:文档中提供了清晰的基础使用流程,引导用户从创建空间、配置记忆到测试检索快速上手。", + "10. 版本说明与限制: 记忆熊 v0.1.0 版本\"初心\"囊括智能记忆管理的核心思路和基础能力,为后续开发奠定了基础。", + "
", "文档资源:用户手册、API文档、FAQ", "问题反馈:GitHub Issues、邮件支持", "致谢:感谢所有参与测试和提供反馈的用户!" @@ -79,16 +82,17 @@ "releaseDate": "2025-12-01", "upgradePosition": "A tool focused on managing and utilizing AI memory, supporting both RAG and knowledge graph storage methods, aiming to provide persistent and structured 'memory' capabilities for AI applications.", "coreUpgrades": [ - "Memory Space: Users can create independent spaces to isolate different memories and flexibly choose storage methods.", - "Memory Configuration: Simplified configuration process with built-in 'memory extraction' for automatic key information extraction and 'forgetting' engine for lifecycle management.", - "Knowledge Retrieval: Provides semantic, tokenization, and hybrid retrieval modes with various parameter tuning and result reranking to improve recall.", - "Global Management: Supports unified default retrieval parameter settings with one-click application to all knowledge bases.", - "Testing & Debugging: Built-in 'recall testing' for real-time verification of retrieval effects and parameter adjustment, with sharing code support for collaboration.", - "Memory Insights: View detailed conversation records, user profiles, and analysis reports to understand AI 'memory' content.", - "Integration & Management: Provides API Key for system integration with basic user management features.", - "Interface & Experience: Modern card-based layout with gradient design, focusing on interaction fluidity and visual aesthetics.", - "Getting Started: Documentation provides clear basic usage flow, guiding users from creating spaces, configuring memory to testing retrieval.", - "Version Notes: MemoryBear v0.1.0 'Original Intent' encompasses core concepts and basic capabilities of intelligent memory management, laying foundation for future development.", + "1. Memory Space: Users can create independent spaces to isolate different memories and flexibly choose storage methods.", + "2. Memory Configuration: Simplified configuration process with built-in 'memory extraction' for automatic key information extraction and 'forgetting' engine for lifecycle management.", + "3. Knowledge Retrieval: Provides semantic, tokenization, and hybrid retrieval modes with various parameter tuning and result reranking to improve recall.", + "4. Global Management: Supports unified default retrieval parameter settings with one-click application to all knowledge bases.", + "5. Testing & Debugging: Built-in 'recall testing' for real-time verification of retrieval effects and parameter adjustment, with sharing code support for collaboration.", + "6. Memory Insights: View detailed conversation records, user profiles, and analysis reports to understand AI 'memory' content.", + "7. Integration & Management: Provides API Key for system integration with basic user management features.", + "8. Interface & Experience: Modern card-based layout with gradient design, focusing on interaction fluidity and visual aesthetics.", + "9. Getting Started: Documentation provides clear basic usage flow, guiding users from creating spaces, configuring memory to testing retrieval.", + "10. Version Notes: MemoryBear v0.1.0 'Original Intent' encompasses core concepts and basic capabilities of intelligent memory management, laying foundation for future development.", + "
", "Documentation: User Manual, API Documentation, FAQ", "Feedback: GitHub Issues, Email Support", "Acknowledgments: Thanks to all users who participated in testing and provided feedback!" From e5e914903cf0c6c5783b0f23d5d5dbb1c3f400ab Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Wed, 28 Jan 2026 11:04:46 +0800 Subject: [PATCH 2/3] feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics --- api/app/models/models_model.py | 2 +- api/app/schemas/model_schema.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/app/models/models_model.py b/api/app/models/models_model.py index a2bfa284..a8918c7c 100644 --- a/api/app/models/models_model.py +++ b/api/app/models/models_model.py @@ -149,7 +149,7 @@ class ModelBase(Base): description = Column(Text, comment="模型描述") is_deprecated = Column(Boolean, default=False, nullable=False, comment="是否弃用") is_official = Column(Boolean, default=True, comment="是否供应商官方模型(区分自定义)") - tags = Column(ARRAY(String), default=[], nullable=False, comment="模型标签(如['聊天', '创作'])") + tags = Column(ARRAY(String), default=list, nullable=False, comment="模型标签(如['聊天', '创作'])") add_count = Column(Integer, default=0, nullable=False, comment="模型被用户添加的次数") # 关联关系 diff --git a/api/app/schemas/model_schema.py b/api/app/schemas/model_schema.py index b83107ef..ce1b36bb 100644 --- a/api/app/schemas/model_schema.py +++ b/api/app/schemas/model_schema.py @@ -4,6 +4,10 @@ import datetime import uuid from app.models.models_model import ModelProvider, ModelType +from app.core.logging_config import get_business_logger + +schema_logger = get_business_logger() + @@ -164,7 +168,7 @@ class ModelApiKey(ModelApiKeyBase): and getattr(mc, 'name', None) == self.model_name)) ] except Exception as e: - print(f"提取 model_config_ids 失败:{e}") + schema_logger.warning(f"提取 model_config_ids 失败:{e}") self.model_config_ids = [] model_config = ConfigDict( From 9a4b1f093789e8fb26eae3e5cbe1dea2b3c91fce Mon Sep 17 00:00:00 2001 From: Timebomb2018 <18868801967@163.com> Date: Wed, 28 Jan 2026 11:42:45 +0800 Subject: [PATCH 3/3] feat(model and app statistic): 1. Optimize the model list; 2. Increase the model combination; 3. Add a model square; 4. Add application management statistics --- api/app/controllers/model_controller.py | 30 +++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/api/app/controllers/model_controller.py b/api/app/controllers/model_controller.py index 481c520e..509f7cad 100644 --- a/api/app/controllers/model_controller.py +++ b/api/app/controllers/model_controller.py @@ -36,7 +36,7 @@ def get_model_providers(): @router.get("", response_model=ApiResponse) def get_model_list( - type: Optional[str | list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), + type: Optional[list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于API Key)"), is_active: Optional[bool] = Query(None, description="激活状态筛选"), is_public: Optional[bool] = Query(None, description="公开状态筛选"), @@ -60,11 +60,14 @@ def get_model_list( try: # 解析 type 参数(支持逗号分隔) type_list = [] - if isinstance(type, str): - type_values = [t.strip() for t in type.split(',')] - type_list = [model_schema.ModelType(t.lower()) for t in type_values if t] - elif isinstance(type, list): - type_list = type + if type is not None: + flat_type = [] + for item in type: + split_items = [t.strip() for t in item.split(',') if t.strip()] + flat_type.extend(split_items) + + unique_flat_type = list(dict.fromkeys(flat_type)) + type_list = [ModelType(t.lower()) for t in unique_flat_type] api_logger.error(f"获取模型type_list: {type_list}") query = model_schema.ModelConfigQuery( @@ -89,7 +92,7 @@ def get_model_list( @router.get("/new", response_model=ApiResponse) def get_model_list( - type: Optional[str | list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), + type: Optional[list[str]] = Query(None, description="模型类型筛选(支持多个,如 ?type=LLM 或 ?type=LLM,EMBEDDING)"), provider: Optional[model_schema.ModelProvider] = Query(None, description="提供商筛选(基于ModelConfig)"), is_active: Optional[bool] = Query(None, description="激活状态筛选"), is_public: Optional[bool] = Query(None, description="公开状态筛选"), @@ -111,11 +114,14 @@ def get_model_list( try: # 解析 type 参数(支持逗号分隔) type_list = [] - if isinstance(type, str): - type_values = [t.strip() for t in type.split(',')] - type_list = [model_schema.ModelType(t.lower()) for t in type_values if t] - elif isinstance(type, list): - type_list = type + if type is not None: + flat_type = [] + for item in type: + split_items = [t.strip() for t in item.split(',') if t.strip()] + flat_type.extend(split_items) + + unique_flat_type = list(dict.fromkeys(flat_type)) + type_list = [ModelType(t.lower()) for t in unique_flat_type] api_logger.info(f"获取模型type_list: {type_list}") query = model_schema.ModelConfigQueryNew(