From a8eb5ab6fe2a1a0180fbd27f6a4771287e510a47 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Sun, 26 Apr 2026 20:59:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=20=E5=BC=95=E5=85=A5=20is=5Fv2=5Fs?= =?UTF-8?q?pec=5Fformat=20=E5=87=BD=E6=95=B0=E4=BB=A5=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E8=A7=84=E6=A0=BC=E5=88=A4=E6=96=AD=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package/yuxi/agents/models.py | 5 +++-- backend/package/yuxi/models/chat.py | 8 ++++++-- backend/package/yuxi/models/embed.py | 9 +++++---- backend/package/yuxi/services/model_cache.py | 20 +++++++++++++++++-- .../yuxi/services/model_provider_service.py | 3 ++- backend/server/routers/knowledge_router.py | 9 ++++++--- web/src/components/AgentConfigSidebar.vue | 6 +++--- web/src/components/ConversationNavSection.vue | 2 +- web/src/layouts/AppLayout.vue | 6 +++--- web/src/views/ModelConfigView.vue | 10 +--------- 10 files changed, 48 insertions(+), 30 deletions(-) diff --git a/backend/package/yuxi/agents/models.py b/backend/package/yuxi/agents/models.py index 3f82efce..fb77a1a7 100644 --- a/backend/package/yuxi/agents/models.py +++ b/backend/package/yuxi/agents/models.py @@ -5,6 +5,7 @@ from langchain.chat_models import BaseChatModel, init_chat_model from pydantic import SecretStr from yuxi import config +from yuxi.services.model_cache import is_v2_spec_format from yuxi.utils import get_docker_safe_url from yuxi.utils.logging_config import logger @@ -64,8 +65,8 @@ def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel: """ Load a chat model from a fully specified name. """ - # v2 判断:如果 spec 包含冒号,尝试走 v2 路径 - if ":" in fully_specified_name: + # v2 判断:第一个特殊字符为冒号则走 v2 路径 + if is_v2_spec_format(fully_specified_name): from yuxi.services.model_cache import model_cache info = model_cache.get_model_info(fully_specified_name) diff --git a/backend/package/yuxi/models/chat.py b/backend/package/yuxi/models/chat.py index 272ee263..22d19f62 100644 --- a/backend/package/yuxi/models/chat.py +++ b/backend/package/yuxi/models/chat.py @@ -5,6 +5,7 @@ from openai import AsyncOpenAI from tenacity import before_sleep_log, retry, retry_if_exception_type, stop_after_attempt, wait_exponential from yuxi import config +from yuxi.services.model_cache import is_v2_spec_format from yuxi.utils import logger @@ -126,13 +127,16 @@ def select_model_v2(spec: str) -> OpenAIBase: def select_model(model_provider=None, model_name=None, model_spec=None): """根据模型提供者选择模型""" - # v2 判断:如果 spec 包含冒号且在缓存中存在,走 v2 路径 - if model_spec and ":" in model_spec: + if model_spec and is_v2_spec_format(model_spec): from yuxi.services.model_cache import model_cache if model_cache.is_v2_spec(model_spec): return select_model_v2(model_spec) + available = model_cache.get_all_specs("chat") + available_ids = [s.spec for s in available[:10]] + raise ValueError(f"未找到 V2 模型: '{model_spec}'。可用聊天模型 ({len(available)}): {available_ids}") + logger.warning( f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;" f"当前模型选择参数: provider={model_provider}, model_name={model_name}, spec={model_spec}" diff --git a/backend/package/yuxi/models/embed.py b/backend/package/yuxi/models/embed.py index c5a8137c..4e34c68f 100644 --- a/backend/package/yuxi/models/embed.py +++ b/backend/package/yuxi/models/embed.py @@ -7,6 +7,7 @@ import httpx import requests from yuxi import config +from yuxi.services.model_cache import is_v2_spec_format from yuxi.utils import get_docker_safe_url, hashstr, logger @@ -288,7 +289,7 @@ def get_embedding_model_info_by_id(model_id: str) -> dict: api_key 已从环境变量解析为实际值。 """ # V2 spec 检测 - if isinstance(model_id, str) and ":" in model_id: + if isinstance(model_id, str) and is_v2_spec_format(model_id): from yuxi.services.model_cache import model_cache info = model_cache.get_model_info(model_id) @@ -321,8 +322,8 @@ def select_embedding_model(model_id): V1 格式: provider/model_name(斜杠分隔) V2 格式: provider_id:model_id(冒号分隔) """ - # V2 spec 检测:包含 ":" 且存在于缓存中 - if isinstance(model_id, str) and ":" in model_id: + # V2 spec 检测:第一个特殊字符为冒号且存在于缓存中 + if isinstance(model_id, str) and is_v2_spec_format(model_id): from yuxi.services.model_cache import model_cache if model_cache.is_v2_spec(model_id): @@ -385,7 +386,7 @@ async def test_embedding_model_status_by_spec(spec: str) -> dict: V2 spec 格式: provider_id:model_id(冒号分隔) """ try: - if ":" in spec: + if is_v2_spec_format(spec): from yuxi.services.model_cache import model_cache if model_cache.is_v2_spec(spec): diff --git a/backend/package/yuxi/services/model_cache.py b/backend/package/yuxi/services/model_cache.py index eca2020b..73478345 100644 --- a/backend/package/yuxi/services/model_cache.py +++ b/backend/package/yuxi/services/model_cache.py @@ -22,6 +22,22 @@ _DEFAULT_REDIS_URL = "redis://redis:6379/0" _CACHE_TTL_SECONDS = 5 +def is_v2_spec_format(spec: str) -> bool: + """判断 spec 是否为 V2 格式(基于格式特征而非缓存查找)。 + + V2 格式的特征是第一个特殊字符为冒号(provider_id:model_id), + V1 格式的特征是第一个特殊字符为斜杠(provider/model_name)。 + Provider ID 中不包含斜杠,因此第一个特殊字符决定了格式类型。 + """ + colon_pos = spec.find(":") + slash_pos = spec.find("/") + if colon_pos == -1: + return False + if slash_pos == -1: + return True + return colon_pos < slash_pos + + @dataclass(frozen=True) class ModelInfo: """不可变的模型信息,供运行时使用。""" @@ -245,8 +261,8 @@ def resolve_model_spec(spec: str) -> ModelInfo: if not spec: raise ValueError("spec 不能为空") - # V2: 优先查找缓存 - if ":" in spec: + # V2: 第一个特殊字符为冒号则走 v2 路径 + if is_v2_spec_format(spec): info = model_cache.get_model_info(spec) if info: return info diff --git a/backend/package/yuxi/services/model_provider_service.py b/backend/package/yuxi/services/model_provider_service.py index 0e77a150..f4125108 100644 --- a/backend/package/yuxi/services/model_provider_service.py +++ b/backend/package/yuxi/services/model_provider_service.py @@ -15,6 +15,7 @@ from yuxi.repositories.model_provider_repository import ( update_model_provider, ) from yuxi.config.builtin_providers import BUILTIN_PROVIDERS +from yuxi.services.model_cache import is_v2_spec_format from yuxi.storage.postgres.models_business import ModelProvider VALID_MODEL_TYPES = {"chat", "embedding", "rerank"} @@ -375,7 +376,7 @@ async def test_model_status_by_spec(spec: str) -> dict: from yuxi.services.model_cache import model_cache # V2: 从缓存识别模型类型并分派 - if ":" in spec: + if is_v2_spec_format(spec): info = model_cache.get_model_info(spec) if info: if info.model_type == "embedding": diff --git a/backend/server/routers/knowledge_router.py b/backend/server/routers/knowledge_router.py index 10e891da..c3ade1c4 100644 --- a/backend/server/routers/knowledge_router.py +++ b/backend/server/routers/knowledge_router.py @@ -18,6 +18,7 @@ from yuxi.plugins.parser import Parser, SUPPORTED_FILE_EXTENSIONS, is_supported_ from yuxi.knowledge.utils import calculate_content_hash from yuxi.knowledge.utils.kb_utils import parse_minio_url from yuxi.models.embed import test_all_embedding_models_status, test_embedding_model_status +from yuxi.services.model_cache import is_v2_spec_format from yuxi.storage.postgres.models_business import User from yuxi.storage.minio.client import MinIOClient, StorageError, aupload_file_to_minio, get_minio_client from yuxi.utils import logger @@ -149,8 +150,8 @@ async def create_database( if not embed_model_name: raise HTTPException(status_code=400, detail="embed_model_name 不能为空") - # V2 embedding model (spec 格式: provider_id:model_id,使用冒号分隔) - if ":" in embed_model_name: + # V2 embedding model (spec 格式: provider_id:model_id,第一个特殊字符为冒号) + if is_v2_spec_format(embed_model_name): from yuxi.services.model_cache import model_cache info = model_cache.get_model_info(embed_model_name) @@ -443,7 +444,9 @@ async def add_documents( db_id, file_id, indexing_params, operator_id=current_user.user_id ) # 2. 执行入库(传入 indexing_params 确保使用的参数与用户设置一致) - result = await knowledge_base.index_file(db_id, file_id, operator_id=current_user.user_id, params=indexing_params) + result = await knowledge_base.index_file( + db_id, file_id, operator_id=current_user.user_id, params=indexing_params + ) processed_items.append(result) except Exception as index_error: logger.error(f"自动入库失败 {item} (file_id={file_id}): {index_error}") diff --git a/web/src/components/AgentConfigSidebar.vue b/web/src/components/AgentConfigSidebar.vue index f822b5b9..fcf8f6df 100644 --- a/web/src/components/AgentConfigSidebar.vue +++ b/web/src/components/AgentConfigSidebar.vue @@ -1063,7 +1063,7 @@ const confirmDeleteConfig = async () => { flex-shrink: 0; &.open { - width: 400px; + width: 360px; } .sidebar-header { @@ -1074,7 +1074,7 @@ const confirmDeleteConfig = async () => { border-bottom: 1px solid var(--gray-150); background: var(--gray-0); flex-shrink: 0; - min-width: 400px; + min-width: 360px; z-index: 10; .header-top-row { @@ -1150,7 +1150,7 @@ const confirmDeleteConfig = async () => { flex: 1; overflow-y: auto; padding: 10px 12px 8px; - min-width: 400px; + min-width: 360px; .agent-info { .agent-basic-info { diff --git a/web/src/components/ConversationNavSection.vue b/web/src/components/ConversationNavSection.vue index 3d70525d..d17a22c5 100644 --- a/web/src/components/ConversationNavSection.vue +++ b/web/src/components/ConversationNavSection.vue @@ -293,7 +293,7 @@ const renameChat = async (chatId) => { .conversation-actions { position: absolute; top: 50%; - right: 8px; + right: 4px; display: flex; align-items: center; opacity: 0; diff --git a/web/src/layouts/AppLayout.vue b/web/src/layouts/AppLayout.vue index cefbed1f..f3c29865 100644 --- a/web/src/layouts/AppLayout.vue +++ b/web/src/layouts/AppLayout.vue @@ -7,7 +7,7 @@ import { BarChart3, ClipboardList, Blocks, - Cpu, + Box, PanelLeftClose, PanelLeftOpen, MessageCirclePlus @@ -154,8 +154,8 @@ const mainList = computed(() => { items.push({ name: '模型配置', path: '/model-config', - icon: Cpu, - activeIcon: Cpu + icon: Box, + activeIcon: Box }) items.push({ diff --git a/web/src/views/ModelConfigView.vue b/web/src/views/ModelConfigView.vue index ea3f3a62..1449dbe9 100644 --- a/web/src/views/ModelConfigView.vue +++ b/web/src/views/ModelConfigView.vue @@ -661,14 +661,6 @@ onMounted(loadProviders) - - - - - - 新增供应商 - - @@ -1182,7 +1174,7 @@ onMounted(loadProviders) width: 40px; height: 40px; border-radius: 8px; - background: var(--main-50); + background: #e1f6fb; // 固定使用浅色背景,确保图标清晰可见 overflow: hidden; img {