feat(model): 重构嵌入模型选择器,新增 Rerank 模型选择器,优化模型提供者管理
This commit is contained in:
parent
f8acc48184
commit
e3d312059a
@ -80,10 +80,7 @@ def load_chat_model(fully_specified_name: str, **kwargs) -> BaseChatModel:
|
|||||||
f"Available chat models ({len(available_specs)}): {available_ids}"
|
f"Available chat models ({len(available_specs)}): {available_ids}"
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.warning(
|
logger.warning(f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;当前模型选择参数: {fully_specified_name=}")
|
||||||
f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;"
|
|
||||||
f"当前模型选择参数: {fully_specified_name=}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# v1 逻辑:spec 必须包含 /
|
# v1 逻辑:spec 必须包含 /
|
||||||
if "/" not in fully_specified_name:
|
if "/" not in fully_specified_name:
|
||||||
|
|||||||
@ -14,7 +14,7 @@ from yuxi import config
|
|||||||
from yuxi.knowledge.base import FileStatus, KnowledgeBase
|
from yuxi.knowledge.base import FileStatus, KnowledgeBase
|
||||||
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown
|
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown
|
||||||
from yuxi.knowledge.chunking.ragflow_like.presets import resolve_chunk_processing_params
|
from yuxi.knowledge.chunking.ragflow_like.presets import resolve_chunk_processing_params
|
||||||
from yuxi.knowledge.utils.kb_utils import get_embedding_config
|
from yuxi.models.embed import get_embedding_model_info_by_id
|
||||||
from yuxi.plugins.parser.unified import Parser
|
from yuxi.plugins.parser.unified import Parser
|
||||||
from yuxi.utils import hashstr, logger
|
from yuxi.utils import hashstr, logger
|
||||||
from yuxi.utils.datetime_utils import utc_isoformat
|
from yuxi.utils.datetime_utils import utc_isoformat
|
||||||
@ -262,7 +262,7 @@ class LightRagKB(KnowledgeBase):
|
|||||||
|
|
||||||
def _get_embedding_func(self, embed_info: dict):
|
def _get_embedding_func(self, embed_info: dict):
|
||||||
"""获取 embedding 函数"""
|
"""获取 embedding 函数"""
|
||||||
config_dict = get_embedding_config(embed_info)
|
config_dict = get_embedding_model_info_by_id(embed_info["model_id"])
|
||||||
logger.debug(f"Embedding config dict: {config_dict}")
|
logger.debug(f"Embedding config dict: {config_dict}")
|
||||||
|
|
||||||
if config_dict.get("model_id") and config_dict["model_id"].startswith("ollama"):
|
if config_dict.get("model_id") and config_dict["model_id"].startswith("ollama"):
|
||||||
|
|||||||
@ -355,7 +355,7 @@ def merge_processing_params(metadata_params: dict | None, request_params: dict |
|
|||||||
|
|
||||||
def get_embedding_config(embed_info: dict) -> dict:
|
def get_embedding_config(embed_info: dict) -> dict:
|
||||||
"""
|
"""
|
||||||
获取嵌入模型配置
|
获取嵌入模型配置(兼容性入口,新代码请直接使用 get_embedding_model_info_by_id)。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
embed_info: 嵌入信息字典
|
embed_info: 嵌入信息字典
|
||||||
@ -364,13 +364,12 @@ def get_embedding_config(embed_info: dict) -> dict:
|
|||||||
dict: 标准化的嵌入配置
|
dict: 标准化的嵌入配置
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 使用最新配置
|
|
||||||
assert isinstance(embed_info, dict), f"embed_info must be a dict, got {type(embed_info)}"
|
assert isinstance(embed_info, dict), f"embed_info must be a dict, got {type(embed_info)}"
|
||||||
assert "model_id" in embed_info, f"embed_info must contain 'model_id', got {embed_info}"
|
assert "model_id" in embed_info, f"embed_info must contain 'model_id', got {embed_info}"
|
||||||
logger.warning(f"Using model_id: {embed_info['model_id']}")
|
logger.warning(f"Using model_id: {embed_info['model_id']}")
|
||||||
config_dict = config.embed_model_names[embed_info["model_id"]].model_dump()
|
from yuxi.models.embed import get_embedding_model_info_by_id
|
||||||
config_dict["api_key"] = os.getenv(config_dict["api_key"]) or config_dict["api_key"]
|
|
||||||
return config_dict
|
return get_embedding_model_info_by_id(embed_info["model_id"])
|
||||||
|
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
logger.error(f"AssertionError in get_embedding_config: {e}, embed_info={embed_info}")
|
logger.error(f"AssertionError in get_embedding_config: {e}, embed_info={embed_info}")
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from yuxi.models.chat import select_model
|
from yuxi.models.chat import select_model
|
||||||
from yuxi.models.embed import select_embedding_model
|
from yuxi.models.embed import get_embedding_model_info_by_id, select_embedding_model
|
||||||
|
|
||||||
__all__ = ["select_model", "select_embedding_model"]
|
__all__ = ["select_model", "select_embedding_model", "get_embedding_model_info_by_id"]
|
||||||
|
|||||||
@ -276,16 +276,64 @@ async def test_all_embedding_models_status() -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def select_embedding_model(model_id):
|
def get_embedding_model_info_by_id(model_id: str) -> dict:
|
||||||
provider, model_name = model_id.split("/", 1) if model_id else ("", "")
|
"""
|
||||||
|
通过模型ID获取Embedding模型的标准化配置信息(统一入口,V1/V2 自动识别)。
|
||||||
|
|
||||||
|
V1 格式: provider/model_name(如 "siliconflow/BAAI/bge-m3"),从 config.embed_model_names 查找
|
||||||
|
V2 格式: provider_id:model_id(如 "siliconflow:BAAI/bge-m3"),从 model_cache 查找
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含 base_url、api_key、name、dimension、model_id、batch_size 等字段的配置字典。
|
||||||
|
api_key 已从环境变量解析为实际值。
|
||||||
|
"""
|
||||||
|
# V2 spec 检测
|
||||||
|
if isinstance(model_id, str) and ":" in model_id:
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
info = model_cache.get_model_info(model_id)
|
||||||
|
if info:
|
||||||
|
logger.info(f"Loaded v2 embedding model info for {model_id}")
|
||||||
|
return {
|
||||||
|
"name": info.display_name,
|
||||||
|
"dimension": info.dimension,
|
||||||
|
"base_url": info.base_url,
|
||||||
|
"api_key": info.api_key,
|
||||||
|
"model_id": info.spec,
|
||||||
|
"batch_size": info.batch_size,
|
||||||
|
}
|
||||||
|
|
||||||
support_embed_models = config.embed_model_names.keys()
|
support_embed_models = config.embed_model_names.keys()
|
||||||
assert model_id in support_embed_models, f"Unsupported embed model: {model_id}, only support {support_embed_models}"
|
assert model_id in support_embed_models, f"Unsupported embed model: {model_id}, only support {support_embed_models}"
|
||||||
|
|
||||||
|
embed_config = config.embed_model_names[model_id].model_dump()
|
||||||
|
|
||||||
|
# 解析 api_key:如果值本身是环境变量名,则从环境变量获取实际值
|
||||||
|
embed_config["api_key"] = os.getenv(embed_config["api_key"]) or embed_config["api_key"]
|
||||||
|
|
||||||
|
logger.info(f"Loaded embedding model info for {model_id}")
|
||||||
|
return embed_config
|
||||||
|
|
||||||
|
|
||||||
|
def select_embedding_model(model_id):
|
||||||
|
"""选择 Embedding 模型(V1/V2 自动识别)。
|
||||||
|
|
||||||
|
V1 格式: provider/model_name(斜杠分隔)
|
||||||
|
V2 格式: provider_id:model_id(冒号分隔)
|
||||||
|
"""
|
||||||
|
# V2 spec 检测:包含 ":" 且存在于缓存中
|
||||||
|
if isinstance(model_id, str) and ":" in model_id:
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
if model_cache.is_v2_spec(model_id):
|
||||||
|
return select_embedding_model_v2(model_id)
|
||||||
|
|
||||||
|
provider, model_name = model_id.split("/", 1) if model_id else ("", "")
|
||||||
logger.info(f"Loading embedding model {model_id}")
|
logger.info(f"Loading embedding model {model_id}")
|
||||||
if provider == "local":
|
if provider == "local":
|
||||||
raise ValueError("Local embedding model is not supported, please use other embedding models")
|
raise ValueError("Local embedding model is not supported, please use other embedding models")
|
||||||
|
|
||||||
# 获取嵌入模型配置并转换为字典
|
embed_config = get_embedding_model_info_by_id(model_id)
|
||||||
embed_config = config.embed_model_names[model_id].model_dump()
|
|
||||||
|
|
||||||
if provider == "ollama":
|
if provider == "ollama":
|
||||||
model = OllamaEmbedding(**embed_config)
|
model = OllamaEmbedding(**embed_config)
|
||||||
@ -293,3 +341,66 @@ def select_embedding_model(model_id):
|
|||||||
model = OtherEmbedding(**embed_config)
|
model = OtherEmbedding(**embed_config)
|
||||||
|
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
|
||||||
|
def select_embedding_model_v2(spec: str):
|
||||||
|
"""根据 v2 spec(provider_id:model_id)选择 Embedding 模型。
|
||||||
|
|
||||||
|
v2 spec 格式使用冒号分隔,如: siliconflow:BAAI/bge-m3
|
||||||
|
数据来源为数据库中的 model_providers 表,通过全局缓存访问。
|
||||||
|
"""
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
info = model_cache.get_model_info(spec)
|
||||||
|
if not info:
|
||||||
|
raise ValueError(f"Unknown v2 embedding model spec: {spec}")
|
||||||
|
|
||||||
|
if info.model_type != "embedding":
|
||||||
|
raise ValueError(f"Model {spec} is not an embedding model (type={info.model_type})")
|
||||||
|
|
||||||
|
logger.info(f"Selecting v2 embedding model: {spec} (provider_type={info.provider_type})")
|
||||||
|
|
||||||
|
if info.provider_type == "ollama":
|
||||||
|
return OllamaEmbedding(
|
||||||
|
model=info.model_id,
|
||||||
|
base_url=info.base_url,
|
||||||
|
api_key=info.api_key,
|
||||||
|
dimension=info.dimension,
|
||||||
|
batch_size=info.batch_size,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return OtherEmbedding(
|
||||||
|
model=info.model_id,
|
||||||
|
base_url=info.base_url,
|
||||||
|
api_key=info.api_key,
|
||||||
|
dimension=info.dimension,
|
||||||
|
batch_size=info.batch_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_embedding_model_status_by_spec(spec: str) -> dict:
|
||||||
|
"""根据 full spec 测试 Embedding 模型状态(自动识别 V1/V2)。
|
||||||
|
|
||||||
|
V1 spec 格式: provider/model_name(斜杠分隔)
|
||||||
|
V2 spec 格式: provider_id:model_id(冒号分隔)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if ":" in spec:
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
if model_cache.is_v2_spec(spec):
|
||||||
|
model = select_embedding_model_v2(spec)
|
||||||
|
else:
|
||||||
|
return {"spec": spec, "status": "unsupported", "message": f"不支持的 V2 模型: {spec}"}
|
||||||
|
else:
|
||||||
|
model = select_embedding_model(spec)
|
||||||
|
|
||||||
|
success, message = await model.test_connection()
|
||||||
|
return {
|
||||||
|
"spec": spec,
|
||||||
|
"status": "available" if success else "unavailable",
|
||||||
|
"message": "连接正常" if success else message,
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"测试 Embedding 模型状态失败 {spec}: {e}")
|
||||||
|
return {"spec": spec, "status": "error", "message": str(e)}
|
||||||
|
|||||||
@ -324,9 +324,7 @@ def _validate_models_capabilities(enabled_models: list[dict], capabilities: set[
|
|||||||
"""校验 enabled_models 中所有模型的 type 都在 provider capabilities 范围内。"""
|
"""校验 enabled_models 中所有模型的 type 都在 provider capabilities 范围内。"""
|
||||||
for model in enabled_models or []:
|
for model in enabled_models or []:
|
||||||
if model["type"] not in capabilities:
|
if model["type"] not in capabilities:
|
||||||
raise ValueError(
|
raise ValueError(f"模型 {model['id']} 的 type={model['type']} 不在 provider 能力 {sorted(capabilities)} 内")
|
||||||
f"模型 {model['id']} 的 type={model['type']} 不在 provider 能力 {sorted(capabilities)} 内"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_payload(data: dict[str, Any], *, partial: bool = False) -> dict[str, Any]:
|
def _normalize_payload(data: dict[str, Any], *, partial: bool = False) -> dict[str, Any]:
|
||||||
@ -519,7 +517,7 @@ async def ensure_builtin_model_providers_in_db(db: AsyncSession) -> None:
|
|||||||
payload["enabled_models"] = provider_def.get("enabled_models", [])
|
payload["enabled_models"] = provider_def.get("enabled_models", [])
|
||||||
payload["headers_json"] = payload.get("headers_json") or {}
|
payload["headers_json"] = payload.get("headers_json") or {}
|
||||||
payload["extra_json"] = payload.get("extra_json") or {}
|
payload["extra_json"] = payload.get("extra_json") or {}
|
||||||
payload["is_enabled"] = False
|
payload["is_enabled"] = provider_id == "siliconflow-cn"
|
||||||
payload["is_builtin"] = True
|
payload["is_builtin"] = True
|
||||||
payload["created_by"] = "system"
|
payload["created_by"] = "system"
|
||||||
payload["updated_by"] = "system"
|
payload["updated_by"] = "system"
|
||||||
|
|||||||
@ -148,10 +148,26 @@ async def create_database(
|
|||||||
else:
|
else:
|
||||||
if not embed_model_name:
|
if not embed_model_name:
|
||||||
raise HTTPException(status_code=400, detail="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:
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
info = model_cache.get_model_info(embed_model_name)
|
||||||
|
if not info or info.model_type != "embedding":
|
||||||
|
raise HTTPException(status_code=400, detail=f"不支持的 embedding 模型: {embed_model_name}")
|
||||||
|
embed_info_dict = {
|
||||||
|
"name": info.display_name,
|
||||||
|
"dimension": info.dimension,
|
||||||
|
"base_url": info.base_url,
|
||||||
|
"api_key": info.api_key,
|
||||||
|
"model_id": info.spec,
|
||||||
|
"batch_size": info.batch_size,
|
||||||
|
}
|
||||||
|
else:
|
||||||
if embed_model_name not in config.embed_model_names:
|
if embed_model_name not in config.embed_model_names:
|
||||||
raise HTTPException(status_code=400, detail=f"不支持的 embedding 模型: {embed_model_name}")
|
raise HTTPException(status_code=400, detail=f"不支持的 embedding 模型: {embed_model_name}")
|
||||||
embed_info = config.embed_model_names[embed_model_name]
|
embed_info = config.embed_model_names[embed_model_name]
|
||||||
# 将Pydantic模型转换为字典以便JSON序列化
|
|
||||||
embed_info_dict = embed_info.model_dump() if hasattr(embed_info, "model_dump") else embed_info.dict()
|
embed_info_dict = embed_info.model_dump() if hasattr(embed_info, "model_dump") else embed_info.dict()
|
||||||
|
|
||||||
database_info = await knowledge_base.create_database(
|
database_info = await knowledge_base.create_database(
|
||||||
|
|||||||
@ -234,8 +234,23 @@ async def get_model_status_by_spec(
|
|||||||
spec: str,
|
spec: str,
|
||||||
current_user: User = Depends(get_admin_user),
|
current_user: User = Depends(get_admin_user),
|
||||||
):
|
):
|
||||||
"""根据 full spec 检查模型状态(自动识别 V1/V2)。"""
|
"""根据 full spec 检查模型状态(自动识别 V1/V2、Chat/Embedding)。
|
||||||
|
|
||||||
|
V2 spec 格式: provider_id:model_id(冒号分隔)
|
||||||
|
优先从缓存获取模型类型,根据类型分派到对应的测试函数。
|
||||||
|
"""
|
||||||
from yuxi.models.chat import test_chat_model_status_by_spec
|
from yuxi.models.chat import test_chat_model_status_by_spec
|
||||||
|
|
||||||
|
# 尝试从缓存获取模型类型,区分 Chat 和 Embedding
|
||||||
|
if ":" in spec:
|
||||||
|
from yuxi.services.model_cache import model_cache
|
||||||
|
|
||||||
|
info = model_cache.get_model_info(spec)
|
||||||
|
if info and info.model_type == "embedding":
|
||||||
|
from yuxi.models.embed import test_embedding_model_status_by_spec
|
||||||
|
|
||||||
|
result = await test_embedding_model_status_by_spec(spec)
|
||||||
|
return {"success": True, "data": result}
|
||||||
|
|
||||||
result = await test_chat_model_status_by_spec(spec)
|
result = await test_chat_model_status_by_spec(spec)
|
||||||
return {"success": True, "data": result}
|
return {"success": True, "data": result}
|
||||||
|
|||||||
@ -96,8 +96,7 @@ export const ocrApi = {
|
|||||||
// === 聊天模型状态检查分组 ===
|
// === 聊天模型状态检查分组 ===
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
export const chatModelApi = {
|
export const chatModelApi = {}
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// === 独立模型供应商配置分组 ===
|
// === 独立模型供应商配置分组 ===
|
||||||
|
|||||||
@ -199,11 +199,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
:root {
|
:root {
|
||||||
--page-padding: 12px;
|
--page-padding: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -30,7 +30,6 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
thead th,
|
thead th,
|
||||||
tbody th {
|
tbody th {
|
||||||
border: none;
|
border: none;
|
||||||
@ -161,7 +160,6 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td {
|
td {
|
||||||
padding: 0.5rem 0rem;
|
padding: 0.5rem 0rem;
|
||||||
@ -186,6 +184,5 @@
|
|||||||
// tbody tr:last-child td {
|
// tbody tr:last-child td {
|
||||||
// border-bottom: none;
|
// border-bottom: none;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
135
web/src/assets/css/model-selector-common.less
Normal file
135
web/src/assets/css/model-selector-common.less
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// 模型选择器公共样式 - 供 Chat/Embedding/Rerank 三个模型选择器共用
|
||||||
|
@scrollbar-width: 6px;
|
||||||
|
@border-radius: 8px;
|
||||||
|
|
||||||
|
// 下拉触发器样式
|
||||||
|
.model-select {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 4px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: @border-radius;
|
||||||
|
background-color: var(--gray-0);
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
|
||||||
|
&.borderless {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.max-width {
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.model-select--middle {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.model-select--large {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容布局
|
||||||
|
.model-select-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模型名称区域
|
||||||
|
.model-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--gray-1000);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态控制区域(检查按钮、刷新按钮等)
|
||||||
|
.model-status-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状态指示器
|
||||||
|
.model-status-indicator {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&.available {
|
||||||
|
color: var(--color-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unavailable {
|
||||||
|
color: var(--color-error-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--color-warning-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Provider 标签样式
|
||||||
|
.provider-tag {
|
||||||
|
padding: 2px 4px;
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新图标旋转动画
|
||||||
|
.spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动菜单样式
|
||||||
|
.scrollable-menu {
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: @scrollbar-width;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--gray-400);
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--gray-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -120,8 +120,13 @@
|
|||||||
size="16"
|
size="16"
|
||||||
class="agent-switcher-menu-icon"
|
class="agent-switcher-menu-icon"
|
||||||
/>
|
/>
|
||||||
<span class="agent-switcher-menu-text">{{ agent.name || 'Unknown' }}</span>
|
<span class="agent-switcher-menu-text">{{
|
||||||
<span v-if="agent.id === currentAgentId" class="agent-switcher-menu-badge">
|
agent.name || 'Unknown'
|
||||||
|
}}</span>
|
||||||
|
<span
|
||||||
|
v-if="agent.id === currentAgentId"
|
||||||
|
class="agent-switcher-menu-badge"
|
||||||
|
>
|
||||||
当前
|
当前
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -528,7 +533,10 @@ const conversationRows = computed(() => {
|
|||||||
if (currentThreadConfigNotice.value) {
|
if (currentThreadConfigNotice.value) {
|
||||||
const insertAfterCount = Math.max(
|
const insertAfterCount = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(Number(currentThreadConfigNotice.value.insertAfterConversationCount) || 0, rows.length)
|
Math.min(
|
||||||
|
Number(currentThreadConfigNotice.value.insertAfterConversationCount) || 0,
|
||||||
|
rows.length
|
||||||
|
)
|
||||||
)
|
)
|
||||||
rows.splice(insertAfterCount, 0, {
|
rows.splice(insertAfterCount, 0, {
|
||||||
type: 'notice',
|
type: 'notice',
|
||||||
@ -569,7 +577,10 @@ const showStartAgentSelector = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const showStartAgentDropdown = computed(() => {
|
const showStartAgentDropdown = computed(() => {
|
||||||
return showStartAgentSelector.value && (startAgents.value.length >= 4 || localUIState.chatMainWidth < 380)
|
return (
|
||||||
|
showStartAgentSelector.value &&
|
||||||
|
(startAgents.value.length >= 4 || localUIState.chatMainWidth < 380)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
const showStartAgentSegment = computed(() => {
|
const showStartAgentSegment = computed(() => {
|
||||||
@ -766,7 +777,11 @@ const queuePendingThreadConfigNotice = (threadId) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const flushPendingThreadConfigNotice = (threadId) => {
|
const flushPendingThreadConfigNotice = (threadId) => {
|
||||||
if (!threadId || !currentThreadHasHistory.value || !threadPendingConfigNoticeMap.value[threadId]) {
|
if (
|
||||||
|
!threadId ||
|
||||||
|
!currentThreadHasHistory.value ||
|
||||||
|
!threadPendingConfigNoticeMap.value[threadId]
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1184,7 +1199,11 @@ const selectChat = async (chatId) => {
|
|||||||
// 先更新当前线程,确保底部智能体名称与选中项即时同步。
|
// 先更新当前线程,确保底部智能体名称与选中项即时同步。
|
||||||
setCurrentThreadId(chatId)
|
setCurrentThreadId(chatId)
|
||||||
|
|
||||||
if (!props.singleMode && targetChat?.agent_id && targetChat.agent_id !== currentAgentId.value) {
|
if (
|
||||||
|
!props.singleMode &&
|
||||||
|
targetChat?.agent_id &&
|
||||||
|
targetChat.agent_id !== currentAgentId.value
|
||||||
|
) {
|
||||||
await agentStore.selectAgent(targetChat.agent_id)
|
await agentStore.selectAgent(targetChat.agent_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1698,9 +1717,7 @@ const isDisplayMessageProcessing = (conv, displayItem) => {
|
|||||||
|
|
||||||
const isToolGroupActive = (conv, itemIndex, displayItems) => {
|
const isToolGroupActive = (conv, itemIndex, displayItems) => {
|
||||||
return (
|
return (
|
||||||
isReplyLoading.value &&
|
isReplyLoading.value && conv?.status === 'streaming' && itemIndex === displayItems.length - 1
|
||||||
conv?.status === 'streaming' &&
|
|
||||||
itemIndex === displayItems.length - 1
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,11 +57,7 @@
|
|||||||
/>
|
/>
|
||||||
<!-- 统一显示所有配置项 -->
|
<!-- 统一显示所有配置项 -->
|
||||||
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
|
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
|
||||||
<a-form-item
|
<a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item">
|
||||||
:label="getConfigLabel(key, value)"
|
|
||||||
:name="key"
|
|
||||||
class="config-item"
|
|
||||||
>
|
|
||||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
||||||
|
|
||||||
<!-- <div>{{ value }}</div> -->
|
<!-- <div>{{ value }}</div> -->
|
||||||
|
|||||||
@ -40,16 +40,11 @@
|
|||||||
<div class="col-item">
|
<div class="col-item">
|
||||||
<div class="setting-label">{{ items?.reranker?.des }}</div>
|
<div class="setting-label">{{ items?.reranker?.des }}</div>
|
||||||
<div class="setting-content">
|
<div class="setting-content">
|
||||||
<a-select
|
<RerankModelSelector
|
||||||
class="full-width"
|
|
||||||
:value="configStore.config?.reranker"
|
:value="configStore.config?.reranker"
|
||||||
@change="handleChange('reranker', $event)"
|
@change="handleChange('reranker', $event)"
|
||||||
placeholder="请选择重排序模型"
|
style="width: 100%"
|
||||||
>
|
/>
|
||||||
<a-select-option v-for="(name, idx) in rerankerChoices" :key="idx" :value="name"
|
|
||||||
>{{ name }}
|
|
||||||
</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -187,17 +182,13 @@ import { useUserStore } from '@/stores/user'
|
|||||||
import { Globe } from 'lucide-vue-next'
|
import { Globe } from 'lucide-vue-next'
|
||||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||||
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
||||||
|
import RerankModelSelector from '@/components/RerankModelSelector.vue'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
const agentStore = useAgentStore()
|
const agentStore = useAgentStore()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const items = computed(() => configStore.config?._config_items || {})
|
const items = computed(() => configStore.config?._config_items || {})
|
||||||
const isSettingDefaultAgent = ref(false)
|
const isSettingDefaultAgent = ref(false)
|
||||||
|
|
||||||
const rerankerChoices = computed(() => {
|
|
||||||
return Object.keys(configStore?.config?.reranker_names || {}) || []
|
|
||||||
})
|
|
||||||
|
|
||||||
const agentOptions = computed(() =>
|
const agentOptions = computed(() =>
|
||||||
(agentStore.agents || []).map((agent) => ({
|
(agentStore.agents || []).map((agent) => ({
|
||||||
label: agent.name || 'Unknown',
|
label: agent.name || 'Unknown',
|
||||||
|
|||||||
@ -343,5 +343,4 @@ const renameChat = async (chatId) => {
|
|||||||
color: var(--main-color);
|
color: var(--main-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,68 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-select
|
<a-dropdown trigger="click" @open-change="handleOpenChange">
|
||||||
:style="style"
|
<div class="model-select" :class="modelSelectClasses" @click.prevent>
|
||||||
:size="size"
|
<div class="model-select-content">
|
||||||
:value="value"
|
<div class="model-info">
|
||||||
@change="handleSelect"
|
<a-tooltip :title="displayText" placement="right">
|
||||||
@dropdownVisibleChange="checkAllModelStatus"
|
<span class="model-text">{{ displayText }}</span>
|
||||||
:placeholder="placeholder"
|
</a-tooltip>
|
||||||
:disabled="disabled"
|
|
||||||
>
|
|
||||||
<a-select-option v-for="(name, idx) in embedModelChoices" :key="idx" :value="name">
|
|
||||||
<div style="display: flex; align-items: center; gap: 8px; min-width: 0">
|
|
||||||
<span
|
|
||||||
style="
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ name }} ({{ configStore.config?.embed_model_names[name]?.dimension }})
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
:style="{
|
|
||||||
color: getModelStatusColor(name),
|
|
||||||
fontSize: '11px',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
flexShrink: 0,
|
|
||||||
borderRadius: '3px'
|
|
||||||
}"
|
|
||||||
:title="getModelStatusTooltip(name)"
|
|
||||||
>
|
|
||||||
{{ getModelStatusIcon(name) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</a-select-option>
|
</div>
|
||||||
</a-select>
|
</div>
|
||||||
|
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu class="scrollable-menu">
|
||||||
|
<!-- V2 模型列表(按 provider 分组) -->
|
||||||
|
<a-menu-item-group v-for="(providerData, providerId) in v2Models" :key="`v2-${providerId}`">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ providerId }}</span>
|
||||||
|
<a-tag color="success" size="small" class="provider-tag">新</a-tag>
|
||||||
|
</template>
|
||||||
|
<a-menu-item
|
||||||
|
v-for="model in providerData.models"
|
||||||
|
:key="model.spec"
|
||||||
|
@click="handleSelect(model.spec)"
|
||||||
|
>
|
||||||
|
<div class="model-option">
|
||||||
|
<span class="model-option-name">
|
||||||
|
{{ model.display_name }}
|
||||||
|
<span class="model-dimension">({{ model.dimension }})</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="model-status-icon"
|
||||||
|
:class="getV2StatusClass(model.spec)"
|
||||||
|
:title="getV2StatusTooltip(model.spec)"
|
||||||
|
>{{ getV2StatusIcon(model.spec) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu-item-group>
|
||||||
|
|
||||||
|
<!-- V1 模型列表(过时,仅保留兼容) -->
|
||||||
|
<a-menu-item-group v-if="v1EmbedModels.length" key="v1" title="V1 版本(过时)">
|
||||||
|
<a-menu-item v-for="name in v1EmbedModels" :key="name" @click="handleSelect(name)">
|
||||||
|
<div class="model-option">
|
||||||
|
<span class="model-option-name">
|
||||||
|
{{ name }}
|
||||||
|
<span class="model-dimension"
|
||||||
|
>({{ configStore.config?.embed_model_names[name]?.dimension }})</span
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<a-tag color="default" size="small" class="provider-tag">过时</a-tag>
|
||||||
|
<span
|
||||||
|
class="model-status-icon"
|
||||||
|
:class="getV1StatusClass(name)"
|
||||||
|
:title="getV1StatusTooltip(name)"
|
||||||
|
>{{ getV1StatusIcon(name) }}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu-item-group>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive } from 'vue'
|
import { computed, reactive, ref } from 'vue'
|
||||||
import { useConfigStore } from '@/stores/config'
|
import { useConfigStore } from '@/stores/config'
|
||||||
import { embeddingApi } from '@/apis/knowledge_api'
|
import { embeddingApi } from '@/apis/knowledge_api'
|
||||||
|
import { modelProviderApi } from '@/apis/system_api'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const configStore = useConfigStore()
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
value: {
|
value: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
size: {
|
|
||||||
type: String,
|
|
||||||
default: 'default',
|
|
||||||
validator: (value) => ['default', 'large', 'small'].includes(value)
|
|
||||||
},
|
|
||||||
placeholder: {
|
placeholder: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '请选择嵌入模型'
|
default: '请选择嵌入模型'
|
||||||
},
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'small',
|
||||||
|
validator: (value) => ['default', 'small', 'middle', 'large'].includes(value)
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({ width: '320px' })
|
default: () => ({ width: '100%' })
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
@ -72,34 +98,85 @@ defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['update:value', 'change'])
|
const emit = defineEmits(['update:value', 'change'])
|
||||||
|
|
||||||
|
const v2Models = ref({})
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
modelStatuses: {},
|
v1ModelStatuses: {},
|
||||||
|
v2ModelStatuses: {},
|
||||||
checkingStatus: false
|
checkingStatus: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const embedModelChoices = computed(() => {
|
const displayText = computed(() => props.value || props.placeholder)
|
||||||
return Object.keys(configStore?.config?.embed_model_names || {}) || []
|
|
||||||
|
const resolvedSize = computed(() => props.size || 'small')
|
||||||
|
const modelSelectClasses = computed(() => ({
|
||||||
|
'model-select--middle': resolvedSize.value === 'middle',
|
||||||
|
'model-select--large': resolvedSize.value === 'large'
|
||||||
|
}))
|
||||||
|
|
||||||
|
const v1EmbedModels = computed(() => {
|
||||||
|
return Object.keys(configStore?.config?.embed_model_names || {})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 检查所有embedding模型状态
|
const handleOpenChange = async (open) => {
|
||||||
const checkAllModelStatus = async () => {
|
if (!open) return
|
||||||
|
fetchV2Models()
|
||||||
|
checkV1ModelStatuses()
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchV2Models = async () => {
|
||||||
try {
|
try {
|
||||||
state.checkingStatus = true
|
const response = await modelProviderApi.getV2Models('embedding')
|
||||||
const response = await embeddingApi.getAllModelsStatus()
|
if (response.success) {
|
||||||
if (response.status.models) {
|
v2Models.value = response.data || {}
|
||||||
state.modelStatuses = response.status.models
|
// 拉取到 V2 模型列表后,逐一检查状态
|
||||||
|
await checkV2ModelStatuses()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('检查所有模型状态失败:', error)
|
console.error('获取 V2 embedding 模型失败:', error)
|
||||||
message.error('获取模型状态失败')
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkV2ModelStatuses = async () => {
|
||||||
|
state.checkingStatus = true
|
||||||
|
try {
|
||||||
|
for (const providerData of Object.values(v2Models.value)) {
|
||||||
|
for (const model of providerData.models || []) {
|
||||||
|
try {
|
||||||
|
const response = await modelProviderApi.getModelStatusBySpec(model.spec)
|
||||||
|
if (response.data) {
|
||||||
|
state.v2ModelStatuses[model.spec] = response.data
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
state.v2ModelStatuses[model.spec] = {
|
||||||
|
spec: model.spec,
|
||||||
|
status: 'error',
|
||||||
|
message: '检查失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查 V2 模型状态失败:', error)
|
||||||
} finally {
|
} finally {
|
||||||
state.checkingStatus = false
|
state.checkingStatus = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型状态图标
|
const checkV1ModelStatuses = async () => {
|
||||||
const getModelStatusIcon = (modelId) => {
|
try {
|
||||||
const status = state.modelStatuses[modelId]
|
const response = await embeddingApi.getAllModelsStatus()
|
||||||
|
if (response.status.models) {
|
||||||
|
state.v1ModelStatuses = response.status.models
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查 V1 模型状态失败:', error)
|
||||||
|
message.error('获取模型状态失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// V1 模型状态辅助函数
|
||||||
|
const getV1StatusIcon = (modelId) => {
|
||||||
|
const status = state.v1ModelStatuses[modelId]
|
||||||
if (!status) return '○'
|
if (!status) return '○'
|
||||||
if (status.status === 'available') return '✓'
|
if (status.status === 'available') return '✓'
|
||||||
if (status.status === 'unavailable') return '✗'
|
if (status.status === 'unavailable') return '✗'
|
||||||
@ -107,28 +184,40 @@ const getModelStatusIcon = (modelId) => {
|
|||||||
return '○'
|
return '○'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型状态颜色
|
const getV1StatusClass = (modelId) => {
|
||||||
const getModelStatusColor = (modelId) => {
|
const status = state.v1ModelStatuses[modelId]
|
||||||
const status = state.modelStatuses[modelId]
|
return status?.status || ''
|
||||||
if (!status) return 'var(--gray-500)'
|
|
||||||
if (status.status === 'available') return 'var(--color-success-500)'
|
|
||||||
if (status.status === 'unavailable') return 'var(--color-error-500)'
|
|
||||||
if (status.status === 'error') return 'var(--color-warning-500)'
|
|
||||||
return 'var(--gray-500)'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取模型状态提示文本
|
const getV1StatusTooltip = (modelId) => {
|
||||||
const getModelStatusTooltip = (modelId) => {
|
const status = state.v1ModelStatuses[modelId]
|
||||||
const status = state.modelStatuses[modelId]
|
|
||||||
if (!status) return '状态未知'
|
if (!status) return '状态未知'
|
||||||
|
let statusText =
|
||||||
|
{ available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知'
|
||||||
|
return `${statusText}: ${status.message || '无详细信息'}`
|
||||||
|
}
|
||||||
|
|
||||||
let statusText = ''
|
// V2 模型状态辅助函数
|
||||||
if (status.status === 'available') statusText = '可用'
|
const getV2StatusIcon = (spec) => {
|
||||||
else if (status.status === 'unavailable') statusText = '不可用'
|
const status = state.v2ModelStatuses[spec]
|
||||||
else if (status.status === 'error') statusText = '错误'
|
if (!status) return '○'
|
||||||
|
if (status.status === 'available') return '✓'
|
||||||
|
if (status.status === 'unavailable') return '✗'
|
||||||
|
if (status.status === 'error') return '⚠'
|
||||||
|
return '○'
|
||||||
|
}
|
||||||
|
|
||||||
const message = status.message || '无详细信息'
|
const getV2StatusClass = (spec) => {
|
||||||
return `${statusText}: ${message}`
|
const status = state.v2ModelStatuses[spec]
|
||||||
|
return status?.status || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const getV2StatusTooltip = (spec) => {
|
||||||
|
const status = state.v2ModelStatuses[spec]
|
||||||
|
if (!status) return '状态未知'
|
||||||
|
let statusText =
|
||||||
|
{ available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知'
|
||||||
|
return `${statusText}: ${status.message || '无详细信息'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelect = (value) => {
|
const handleSelect = (value) => {
|
||||||
@ -138,5 +227,46 @@ const handleSelect = (value) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 如果需要添加特定样式,可以在这里添加
|
@import '@/assets/css/model-selector-common.less';
|
||||||
|
|
||||||
|
.model-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-option-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-dimension {
|
||||||
|
color: var(--gray-500);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.model-status-icon {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--gray-500);
|
||||||
|
|
||||||
|
&.available {
|
||||||
|
color: var(--color-success-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.unavailable {
|
||||||
|
color: var(--color-error-500);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
color: var(--color-warning-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -14,8 +14,8 @@
|
|||||||
<template #description>
|
<template #description>
|
||||||
项目已启用新版的模型配置,自定义程度更高,且支持 Embedding / Rerank 模型,您可以
|
项目已启用新版的模型配置,自定义程度更高,且支持 Embedding / Rerank 模型,您可以
|
||||||
<a @click="goToNewModelConfig">点击此处</a>
|
<a @click="goToNewModelConfig">点击此处</a>
|
||||||
跳转到新版模型配置页面。
|
跳转到新版模型配置页面。 已配置模型将会在 0.6.x 期间继续使用,0.7
|
||||||
已配置模型将会在 0.6.x 期间继续使用,0.7 版本将不再支持老版的模型调用。
|
版本将不再支持老版的模型调用。
|
||||||
为了更加稳定的模型调用和更好的用户体验,建议尽快迁移到新的模型配置。
|
为了更加稳定的模型调用和更好的用户体验,建议尽快迁移到新的模型配置。
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
@ -25,7 +25,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="item in tableData" :key="item.id" class="provider-block">
|
<div v-for="item in tableData" :key="item.id" class="provider-block">
|
||||||
<h4 class="provider-name">{{ item.name }} <span class="provider-id">({{ item.id }})</span></h4>
|
<h4 class="provider-name">
|
||||||
|
{{ item.name }} <span class="provider-id">({{ item.id }})</span>
|
||||||
|
</h4>
|
||||||
<a-descriptions bordered :column="1" size="small">
|
<a-descriptions bordered :column="1" size="small">
|
||||||
<a-descriptions-item label="API地址">{{ item.base_url }}</a-descriptions-item>
|
<a-descriptions-item label="API地址">{{ item.base_url }}</a-descriptions-item>
|
||||||
<a-descriptions-item label="默认模型">{{ item.default }}</a-descriptions-item>
|
<a-descriptions-item label="默认模型">{{ item.default }}</a-descriptions-item>
|
||||||
@ -34,7 +36,9 @@
|
|||||||
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a>
|
<a v-if="item.url" :href="item.url" target="_blank">{{ item.url }}</a>
|
||||||
<span v-else>无</span>
|
<span v-else>无</span>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
<a-descriptions-item label="可用模型">{{ item.models?.join(', ') || '无' }}</a-descriptions-item>
|
<a-descriptions-item label="可用模型">{{
|
||||||
|
item.models?.join(', ') || '无'
|
||||||
|
}}</a-descriptions-item>
|
||||||
</a-descriptions>
|
</a-descriptions>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,17 +17,14 @@
|
|||||||
{{ modelStatusIcon }}
|
{{ modelStatusIcon }}
|
||||||
</span>
|
</span>
|
||||||
<a-tooltip title="刷新缓存">
|
<a-tooltip title="刷新缓存">
|
||||||
<a-button
|
<button
|
||||||
type="text"
|
|
||||||
:loading="state.refreshingCache"
|
:loading="state.refreshingCache"
|
||||||
@click.stop="refreshCache"
|
@click.stop="refreshCache"
|
||||||
:disabled="state.refreshingCache"
|
:disabled="state.refreshingCache"
|
||||||
class="cache-refresh-button"
|
class="cache-refresh-button"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<RefreshCw :size="13" :class="{ spin: state.refreshingCache }" />
|
||||||
<RefreshCw :size="13" :class="{ 'spin': state.refreshingCache }" />
|
</button>
|
||||||
</template>
|
|
||||||
</a-button>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-button
|
<a-button
|
||||||
:size="buttonSize"
|
:size="buttonSize"
|
||||||
@ -48,7 +45,7 @@
|
|||||||
<a-menu-item-group v-for="(providerData, providerId) in v2Models" :key="`v2-${providerId}`">
|
<a-menu-item-group v-for="(providerData, providerId) in v2Models" :key="`v2-${providerId}`">
|
||||||
<template #title>
|
<template #title>
|
||||||
<span>{{ providerId }}</span>
|
<span>{{ providerId }}</span>
|
||||||
<a-tag color="success" size="small" class="provider-tag">v2</a-tag>
|
<a-tag color="success" size="small" class="provider-tag">新</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<a-menu-item
|
<a-menu-item
|
||||||
v-for="model in providerData.models"
|
v-for="model in providerData.models"
|
||||||
@ -199,170 +196,28 @@ const handleSelectV2Model = (spec) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
// 变量定义
|
@import '@/assets/css/model-selector-common.less';
|
||||||
@status-success: var(--color-success-500);
|
|
||||||
@status-error: var(--color-error-500);
|
|
||||||
@status-warning: var(--color-warning-500);
|
|
||||||
@status-default: var(--gray-500);
|
|
||||||
@border-radius: 8px;
|
|
||||||
@scrollbar-width: 6px;
|
|
||||||
@status-indicator-padding: 2px 4px;
|
|
||||||
@status-check-button-padding: 0 4px;
|
|
||||||
@status-check-button-font-size: 12px;
|
|
||||||
@status-indicator-font-size: 11px;
|
|
||||||
|
|
||||||
// 主选择器样式
|
|
||||||
.model-select {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
padding: 4px 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
border: 1px solid var(--gray-200);
|
|
||||||
border-radius: @border-radius;
|
|
||||||
background-color: var(--gray-0);
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
|
|
||||||
// 修饰符类
|
// 状态检查按钮
|
||||||
&.borderless {
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.max-width {
|
|
||||||
max-width: 380px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.model-select--middle {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.model-select--large {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 内容区域
|
|
||||||
.model-select-content {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
min-width: 0;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
// 模型信息区域
|
|
||||||
.model-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.model-text {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
color: var(--gray-1000);
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态控制区域
|
|
||||||
.model-status-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
flex: 0;
|
|
||||||
margin-left: auto;
|
|
||||||
|
|
||||||
// 状态指示器
|
|
||||||
.model-status-indicator {
|
|
||||||
font-size: @status-indicator-font-size;
|
|
||||||
font-weight: bold;
|
|
||||||
padding: @status-indicator-padding;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
// 状态样式修饰符
|
|
||||||
&.available {
|
|
||||||
color: @status-success;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.unavailable {
|
|
||||||
color: @status-error;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: @status-warning;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查按钮
|
|
||||||
.status-check-button {
|
.status-check-button {
|
||||||
font-size: @status-check-button-font-size;
|
font-size: 12px;
|
||||||
padding: @status-check-button-padding;
|
padding: 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存刷新按钮
|
// 缓存刷新按钮
|
||||||
.cache-refresh-button {
|
.cache-refresh-button {
|
||||||
font-size: @status-check-button-font-size;
|
font-size: 12px;
|
||||||
padding: @status-check-button-padding;
|
padding: 0 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
border: none;
|
||||||
}
|
outline: none;
|
||||||
}
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 刷新图标旋转动画
|
|
||||||
.spin {
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Provider 标签样式
|
|
||||||
.provider-tag {
|
|
||||||
margin-left: 6px;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 1;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 滚动菜单样式
|
|
||||||
.scrollable-menu {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
// 自定义滚动条样式
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
width: @scrollbar-width;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::-webkit-scrollbar-thumb {
|
|
||||||
background: var(--gray-400);
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--gray-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
// 将全局样式移到scoped中以避免样式污染
|
|
||||||
:deep(.ant-dropdown-menu) {
|
:deep(.ant-dropdown-menu) {
|
||||||
&.scrollable-menu {
|
&.scrollable-menu {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
|
|||||||
101
web/src/components/RerankModelSelector.vue
Normal file
101
web/src/components/RerankModelSelector.vue
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<template>
|
||||||
|
<a-dropdown trigger="click" @open-change="handleOpenChange">
|
||||||
|
<div class="model-select" @click.prevent>
|
||||||
|
<div class="model-select-content">
|
||||||
|
<div class="model-info">
|
||||||
|
<a-tooltip :title="displayText" placement="right">
|
||||||
|
<span class="model-text">{{ displayText }}</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu class="scrollable-menu">
|
||||||
|
<a-menu-item-group v-for="(providerData, providerId) in v2Models" :key="`v2-${providerId}`">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ providerId }}</span>
|
||||||
|
<a-tag color="success" size="small" class="provider-tag">新</a-tag>
|
||||||
|
</template>
|
||||||
|
<a-menu-item
|
||||||
|
v-for="model in providerData.models"
|
||||||
|
:key="model.spec"
|
||||||
|
@click="handleSelect(model.spec)"
|
||||||
|
>
|
||||||
|
{{ model.display_name }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu-item-group>
|
||||||
|
|
||||||
|
<a-menu-item-group v-if="v1Models.length" key="v1" title="V1 版本(过时)">
|
||||||
|
<a-menu-item v-for="name in v1Models" :key="name" @click="handleSelect(name)">
|
||||||
|
<span>{{ name }}</span>
|
||||||
|
<a-tag color="default" size="small" class="provider-tag">过时</a-tag>
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu-item-group>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { useConfigStore } from '@/stores/config'
|
||||||
|
import { modelProviderApi } from '@/apis/system_api'
|
||||||
|
|
||||||
|
const configStore = useConfigStore()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
value: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
type: String,
|
||||||
|
default: '请选择重排序模型'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: String,
|
||||||
|
default: 'small',
|
||||||
|
validator: (value) => ['small', 'middle', 'large'].includes(value)
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ width: '100%' })
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:value', 'change'])
|
||||||
|
|
||||||
|
const v2Models = ref({})
|
||||||
|
|
||||||
|
const displayText = computed(() => props.value || props.placeholder)
|
||||||
|
|
||||||
|
const v1Models = computed(() => {
|
||||||
|
return Object.keys(configStore?.config?.reranker_names || {})
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOpenChange = async (open) => {
|
||||||
|
if (!open) return
|
||||||
|
try {
|
||||||
|
const response = await modelProviderApi.getV2Models('rerank')
|
||||||
|
if (response.success) {
|
||||||
|
v2Models.value = response.data || {}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 V2 rerank 模型失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSelect = (spec) => {
|
||||||
|
emit('update:value', spec)
|
||||||
|
emit('change', spec)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
@import '@/assets/css/model-selector-common.less';
|
||||||
|
</style>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Result Slot -->
|
<!-- Result Slot -->
|
||||||
<div class="tool-result" style="opacity: 0.8;" v-if="hasResult">
|
<div class="tool-result" style="opacity: 0.8" v-if="hasResult">
|
||||||
<slot name="result" :tool-call="toolCall" :result-content="resultContent">
|
<slot name="result" :tool-call="toolCall" :result-content="resultContent">
|
||||||
<div class="tool-result-content" :data-tool-call-id="toolCall.id">
|
<div class="tool-result-content" :data-tool-call-id="toolCall.id">
|
||||||
<!-- Default rendering -->
|
<!-- Default rendering -->
|
||||||
|
|||||||
@ -13,8 +13,12 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="summary-content">
|
<span class="summary-content">
|
||||||
<span class="summary-title">{{ toolCallsSummaryTitle }}</span>
|
<span class="summary-title">{{ toolCallsSummaryTitle }}</span>
|
||||||
<span class="summary-separator" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">·</span>
|
<span class="summary-separator" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta"
|
||||||
<span class="summary-meta" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">{{ toolCallsNamesMeta }}</span>
|
>·</span
|
||||||
|
>
|
||||||
|
<span class="summary-meta" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">{{
|
||||||
|
toolCallsNamesMeta
|
||||||
|
}}</span>
|
||||||
<span class="summary-status-tag" v-if="statusSummary">{{ statusSummary }}</span>
|
<span class="summary-status-tag" v-if="statusSummary">{{ statusSummary }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="summary-trailing">
|
<span class="summary-trailing">
|
||||||
@ -123,8 +127,9 @@ const statusSummary = computed(() => {
|
|||||||
(toolCall) =>
|
(toolCall) =>
|
||||||
toolCall.status !== 'success' && toolCall.status !== 'error' && !toolCall.tool_call_result
|
toolCall.status !== 'success' && toolCall.status !== 'error' && !toolCall.tool_call_result
|
||||||
).length
|
).length
|
||||||
const errorCount = normalizedToolCalls.value.filter((toolCall) => toolCall.status === 'error')
|
const errorCount = normalizedToolCalls.value.filter(
|
||||||
.length
|
(toolCall) => toolCall.status === 'error'
|
||||||
|
).length
|
||||||
|
|
||||||
const parts = []
|
const parts = []
|
||||||
if (successCount > 0 && successCount === normalizedToolCalls.value.length) {
|
if (successCount > 0 && successCount === normalizedToolCalls.value.length) {
|
||||||
|
|||||||
@ -208,10 +208,7 @@
|
|||||||
|
|
||||||
<!-- 部门选择器(仅超级管理员可见) -->
|
<!-- 部门选择器(仅超级管理员可见) -->
|
||||||
<a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item">
|
<a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item">
|
||||||
<a-select
|
<a-select v-model:value="userManagement.form.departmentId" placeholder="请选择部门">
|
||||||
v-model:value="userManagement.form.departmentId"
|
|
||||||
placeholder="请选择部门"
|
|
||||||
>
|
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="dept in departmentManagement.departments"
|
v-for="dept in departmentManagement.departments"
|
||||||
:key="dept.id"
|
:key="dept.id"
|
||||||
|
|||||||
@ -36,7 +36,10 @@ export const useChatThreadsStore = defineStore('chatThreads', () => {
|
|||||||
const fetchedThreads = await threadApi.getThreads(agentId, PAGE_SIZE, 0)
|
const fetchedThreads = await threadApi.getThreads(agentId, PAGE_SIZE, 0)
|
||||||
threads.value = fetchedThreads || []
|
threads.value = fetchedThreads || []
|
||||||
hasMoreThreads.value = Boolean(fetchedThreads && fetchedThreads.length >= PAGE_SIZE)
|
hasMoreThreads.value = Boolean(fetchedThreads && fetchedThreads.length >= PAGE_SIZE)
|
||||||
if (currentThreadId.value && !threads.value.find((thread) => thread.id === currentThreadId.value)) {
|
if (
|
||||||
|
currentThreadId.value &&
|
||||||
|
!threads.value.find((thread) => thread.id === currentThreadId.value)
|
||||||
|
) {
|
||||||
currentThreadId.value = null
|
currentThreadId.value = null
|
||||||
}
|
}
|
||||||
return threads.value
|
return threads.value
|
||||||
|
|||||||
@ -1,32 +1,32 @@
|
|||||||
const ICON_BASE = 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons'
|
const ICON_BASE = 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons'
|
||||||
|
|
||||||
export const modelIcons = {
|
export const modelIcons = {
|
||||||
'default': `${ICON_BASE}/default.svg`,
|
default: `${ICON_BASE}/default.svg`,
|
||||||
'alibaba': `${ICON_BASE}/bailian-color.svg`,
|
alibaba: `${ICON_BASE}/bailian-color.svg`,
|
||||||
'alibaba-coding-plan': `${ICON_BASE}/alibabacloud-color.svg`,
|
'alibaba-coding-plan': `${ICON_BASE}/alibabacloud-color.svg`,
|
||||||
'alibaba-coding-plan-cn': `${ICON_BASE}/alibabacloud-color.svg`,
|
'alibaba-coding-plan-cn': `${ICON_BASE}/alibabacloud-color.svg`,
|
||||||
'anthropic': `${ICON_BASE}/anthropic.svg`,
|
anthropic: `${ICON_BASE}/anthropic.svg`,
|
||||||
'ark': `${ICON_BASE}/volcengine-color.svg`,
|
ark: `${ICON_BASE}/volcengine-color.svg`,
|
||||||
'dashscope': `${ICON_BASE}/bailian-color.svg`,
|
dashscope: `${ICON_BASE}/bailian-color.svg`,
|
||||||
'deepseek': `${ICON_BASE}/deepseek-color.svg`,
|
deepseek: `${ICON_BASE}/deepseek-color.svg`,
|
||||||
'google': `${ICON_BASE}/google-color.svg`,
|
google: `${ICON_BASE}/google-color.svg`,
|
||||||
'lmstudio': `${ICON_BASE}/lmstudio.svg`,
|
lmstudio: `${ICON_BASE}/lmstudio.svg`,
|
||||||
'minimax': `${ICON_BASE}/minimax-color.svg`,
|
minimax: `${ICON_BASE}/minimax-color.svg`,
|
||||||
'minimax-cn': `${ICON_BASE}/minimax-color.svg`,
|
'minimax-cn': `${ICON_BASE}/minimax-color.svg`,
|
||||||
'modelscope': `${ICON_BASE}/modelscope-color.svg`,
|
modelscope: `${ICON_BASE}/modelscope-color.svg`,
|
||||||
'moonshotai': `${ICON_BASE}/moonshot.svg`,
|
moonshotai: `${ICON_BASE}/moonshot.svg`,
|
||||||
'moonshotai-cn': `${ICON_BASE}/moonshot.svg`,
|
'moonshotai-cn': `${ICON_BASE}/moonshot.svg`,
|
||||||
'ollama': `${ICON_BASE}/ollama.svg`,
|
ollama: `${ICON_BASE}/ollama.svg`,
|
||||||
'ollama-cloud': `${ICON_BASE}/ollama.svg`,
|
'ollama-cloud': `${ICON_BASE}/ollama.svg`,
|
||||||
'opencode': `${ICON_BASE}/opencode.svg`,
|
opencode: `${ICON_BASE}/opencode.svg`,
|
||||||
'openai': `${ICON_BASE}/openai.svg`,
|
openai: `${ICON_BASE}/openai.svg`,
|
||||||
'openrouter': `${ICON_BASE}/openrouter.svg`,
|
openrouter: `${ICON_BASE}/openrouter.svg`,
|
||||||
'siliconflow': `${ICON_BASE}/siliconcloud-color.svg`,
|
siliconflow: `${ICON_BASE}/siliconcloud-color.svg`,
|
||||||
'siliconflow-cn': `${ICON_BASE}/siliconcloud-color.svg`,
|
'siliconflow-cn': `${ICON_BASE}/siliconcloud-color.svg`,
|
||||||
'together': `${ICON_BASE}/together-color.svg`,
|
together: `${ICON_BASE}/together-color.svg`,
|
||||||
'zai': `${ICON_BASE}/zai.svg`,
|
zai: `${ICON_BASE}/zai.svg`,
|
||||||
'zai-coding-plan': `${ICON_BASE}/zai.svg`,
|
'zai-coding-plan': `${ICON_BASE}/zai.svg`,
|
||||||
'zhipu': `${ICON_BASE}/zhipu-color.svg`,
|
zhipu: `${ICON_BASE}/zhipu-color.svg`,
|
||||||
'zhipuai': `${ICON_BASE}/zhipu-color.svg`,
|
zhipuai: `${ICON_BASE}/zhipu-color.svg`,
|
||||||
'zhipuai-coding-plan': `${ICON_BASE}/zhipu-color.svg`
|
'zhipuai-coding-plan': `${ICON_BASE}/zhipu-color.svg`
|
||||||
} // ESLint-disable-line
|
} // ESLint-disable-line
|
||||||
|
|||||||
@ -167,13 +167,8 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
// 从 agentStore 中获取响应式状态
|
// 从 agentStore 中获取响应式状态
|
||||||
const {
|
const { selectedAgentId, defaultAgentId, selectedAgentConfigId, agentConfigs, isLoadingConfig } =
|
||||||
selectedAgentId,
|
storeToRefs(agentStore)
|
||||||
defaultAgentId,
|
|
||||||
selectedAgentConfigId,
|
|
||||||
agentConfigs,
|
|
||||||
isLoadingConfig
|
|
||||||
} = storeToRefs(agentStore)
|
|
||||||
|
|
||||||
const syncingRouteThread = ref(false)
|
const syncingRouteThread = ref(false)
|
||||||
|
|
||||||
|
|||||||
@ -95,7 +95,9 @@ const filteredProviders = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const providerStats = computed(() => {
|
const providerStats = computed(() => {
|
||||||
let enabled = 0, warning = 0, models = 0
|
let enabled = 0,
|
||||||
|
warning = 0,
|
||||||
|
models = 0
|
||||||
for (const p of providers.value) {
|
for (const p of providers.value) {
|
||||||
if (p.is_enabled) {
|
if (p.is_enabled) {
|
||||||
enabled++
|
enabled++
|
||||||
@ -989,8 +991,7 @@ onMounted(loadProviders)
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="remote-fetch-actions">
|
<div class="remote-fetch-actions"></div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user