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}"
|
||||
)
|
||||
|
||||
logger.warning(
|
||||
f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;"
|
||||
f"当前模型选择参数: {fully_specified_name=}"
|
||||
)
|
||||
logger.warning(f"旧版本的模型选择逻辑已废弃,建议尽快迁移至新的模型配置;当前模型选择参数: {fully_specified_name=}")
|
||||
|
||||
# v1 逻辑:spec 必须包含 /
|
||||
if "/" not in fully_specified_name:
|
||||
|
||||
@ -14,7 +14,7 @@ from yuxi import config
|
||||
from yuxi.knowledge.base import FileStatus, KnowledgeBase
|
||||
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.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.utils import hashstr, logger
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
@ -262,7 +262,7 @@ class LightRagKB(KnowledgeBase):
|
||||
|
||||
def _get_embedding_func(self, embed_info: dict):
|
||||
"""获取 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}")
|
||||
|
||||
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:
|
||||
"""
|
||||
获取嵌入模型配置
|
||||
获取嵌入模型配置(兼容性入口,新代码请直接使用 get_embedding_model_info_by_id)。
|
||||
|
||||
Args:
|
||||
embed_info: 嵌入信息字典
|
||||
@ -364,13 +364,12 @@ def get_embedding_config(embed_info: dict) -> dict:
|
||||
dict: 标准化的嵌入配置
|
||||
"""
|
||||
try:
|
||||
# 使用最新配置
|
||||
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}"
|
||||
logger.warning(f"Using model_id: {embed_info['model_id']}")
|
||||
config_dict = config.embed_model_names[embed_info["model_id"]].model_dump()
|
||||
config_dict["api_key"] = os.getenv(config_dict["api_key"]) or config_dict["api_key"]
|
||||
return config_dict
|
||||
from yuxi.models.embed import get_embedding_model_info_by_id
|
||||
|
||||
return get_embedding_model_info_by_id(embed_info["model_id"])
|
||||
|
||||
except AssertionError as e:
|
||||
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.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):
|
||||
provider, model_name = model_id.split("/", 1) if model_id else ("", "")
|
||||
def get_embedding_model_info_by_id(model_id: str) -> dict:
|
||||
"""
|
||||
通过模型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()
|
||||
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}")
|
||||
if provider == "local":
|
||||
raise ValueError("Local embedding model is not supported, please use other embedding models")
|
||||
|
||||
# 获取嵌入模型配置并转换为字典
|
||||
embed_config = config.embed_model_names[model_id].model_dump()
|
||||
embed_config = get_embedding_model_info_by_id(model_id)
|
||||
|
||||
if provider == "ollama":
|
||||
model = OllamaEmbedding(**embed_config)
|
||||
@ -293,3 +341,66 @@ def select_embedding_model(model_id):
|
||||
model = OtherEmbedding(**embed_config)
|
||||
|
||||
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 范围内。"""
|
||||
for model in enabled_models or []:
|
||||
if model["type"] not in capabilities:
|
||||
raise ValueError(
|
||||
f"模型 {model['id']} 的 type={model['type']} 不在 provider 能力 {sorted(capabilities)} 内"
|
||||
)
|
||||
raise ValueError(f"模型 {model['id']} 的 type={model['type']} 不在 provider 能力 {sorted(capabilities)} 内")
|
||||
|
||||
|
||||
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["headers_json"] = payload.get("headers_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["created_by"] = "system"
|
||||
payload["updated_by"] = "system"
|
||||
|
||||
@ -148,11 +148,27 @@ async def create_database(
|
||||
else:
|
||||
if not embed_model_name:
|
||||
raise HTTPException(status_code=400, detail="embed_model_name 不能为空")
|
||||
if embed_model_name not in config.embed_model_names:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的 embedding 模型: {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()
|
||||
|
||||
# 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:
|
||||
raise HTTPException(status_code=400, detail=f"不支持的 embedding 模型: {embed_model_name}")
|
||||
embed_info = config.embed_model_names[embed_model_name]
|
||||
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_name,
|
||||
|
||||
@ -234,8 +234,23 @@ async def get_model_status_by_spec(
|
||||
spec: str,
|
||||
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
|
||||
|
||||
# 尝试从缓存获取模型类型,区分 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)
|
||||
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) {
|
||||
:root {
|
||||
--page-padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -30,7 +30,6 @@
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
|
||||
thead th,
|
||||
tbody th {
|
||||
border: none;
|
||||
@ -161,7 +160,6 @@
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.5rem 0rem;
|
||||
@ -186,6 +184,5 @@
|
||||
// tbody tr:last-child td {
|
||||
// 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"
|
||||
class="agent-switcher-menu-icon"
|
||||
/>
|
||||
<span class="agent-switcher-menu-text">{{ agent.name || 'Unknown' }}</span>
|
||||
<span v-if="agent.id === currentAgentId" class="agent-switcher-menu-badge">
|
||||
<span class="agent-switcher-menu-text">{{
|
||||
agent.name || 'Unknown'
|
||||
}}</span>
|
||||
<span
|
||||
v-if="agent.id === currentAgentId"
|
||||
class="agent-switcher-menu-badge"
|
||||
>
|
||||
当前
|
||||
</span>
|
||||
</div>
|
||||
@ -528,7 +533,10 @@ const conversationRows = computed(() => {
|
||||
if (currentThreadConfigNotice.value) {
|
||||
const insertAfterCount = Math.max(
|
||||
0,
|
||||
Math.min(Number(currentThreadConfigNotice.value.insertAfterConversationCount) || 0, rows.length)
|
||||
Math.min(
|
||||
Number(currentThreadConfigNotice.value.insertAfterConversationCount) || 0,
|
||||
rows.length
|
||||
)
|
||||
)
|
||||
rows.splice(insertAfterCount, 0, {
|
||||
type: 'notice',
|
||||
@ -569,7 +577,10 @@ const showStartAgentSelector = 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(() => {
|
||||
@ -766,7 +777,11 @@ const queuePendingThreadConfigNotice = (threadId) => {
|
||||
}
|
||||
|
||||
const flushPendingThreadConfigNotice = (threadId) => {
|
||||
if (!threadId || !currentThreadHasHistory.value || !threadPendingConfigNoticeMap.value[threadId]) {
|
||||
if (
|
||||
!threadId ||
|
||||
!currentThreadHasHistory.value ||
|
||||
!threadPendingConfigNoticeMap.value[threadId]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
@ -1184,7 +1199,11 @@ const selectChat = async (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)
|
||||
}
|
||||
|
||||
@ -1613,10 +1632,10 @@ const hasVisibleAssistantBody = (message) => {
|
||||
const { content, reasoningContent } = extractAssistantMessageBody(message)
|
||||
return Boolean(
|
||||
content ||
|
||||
reasoningContent ||
|
||||
message.error_type ||
|
||||
message.extra_metadata?.error_type ||
|
||||
message.isStoppedByUser
|
||||
reasoningContent ||
|
||||
message.error_type ||
|
||||
message.extra_metadata?.error_type ||
|
||||
message.isStoppedByUser
|
||||
)
|
||||
}
|
||||
|
||||
@ -1698,9 +1717,7 @@ const isDisplayMessageProcessing = (conv, displayItem) => {
|
||||
|
||||
const isToolGroupActive = (conv, itemIndex, displayItems) => {
|
||||
return (
|
||||
isReplyLoading.value &&
|
||||
conv?.status === 'streaming' &&
|
||||
itemIndex === displayItems.length - 1
|
||||
isReplyLoading.value && conv?.status === 'streaming' && itemIndex === displayItems.length - 1
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -57,11 +57,7 @@
|
||||
/>
|
||||
<!-- 统一显示所有配置项 -->
|
||||
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
|
||||
<a-form-item
|
||||
:label="getConfigLabel(key, value)"
|
||||
:name="key"
|
||||
class="config-item"
|
||||
>
|
||||
<a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item">
|
||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
||||
|
||||
<!-- <div>{{ value }}</div> -->
|
||||
|
||||
@ -40,16 +40,11 @@
|
||||
<div class="col-item">
|
||||
<div class="setting-label">{{ items?.reranker?.des }}</div>
|
||||
<div class="setting-content">
|
||||
<a-select
|
||||
class="full-width"
|
||||
<RerankModelSelector
|
||||
:value="configStore.config?.reranker"
|
||||
@change="handleChange('reranker', $event)"
|
||||
placeholder="请选择重排序模型"
|
||||
>
|
||||
<a-select-option v-for="(name, idx) in rerankerChoices" :key="idx" :value="name"
|
||||
>{{ name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
style="width: 100%"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,17 +182,13 @@ import { useUserStore } from '@/stores/user'
|
||||
import { Globe } from 'lucide-vue-next'
|
||||
import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'
|
||||
import EmbeddingModelSelector from '@/components/EmbeddingModelSelector.vue'
|
||||
import RerankModelSelector from '@/components/RerankModelSelector.vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const agentStore = useAgentStore()
|
||||
const userStore = useUserStore()
|
||||
const items = computed(() => configStore.config?._config_items || {})
|
||||
const isSettingDefaultAgent = ref(false)
|
||||
|
||||
const rerankerChoices = computed(() => {
|
||||
return Object.keys(configStore?.config?.reranker_names || {}) || []
|
||||
})
|
||||
|
||||
const agentOptions = computed(() =>
|
||||
(agentStore.agents || []).map((agent) => ({
|
||||
label: agent.name || 'Unknown',
|
||||
|
||||
@ -343,5 +343,4 @@ const renameChat = async (chatId) => {
|
||||
color: var(--main-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -1,68 +1,94 @@
|
||||
<template>
|
||||
<a-select
|
||||
:style="style"
|
||||
:size="size"
|
||||
:value="value"
|
||||
@change="handleSelect"
|
||||
@dropdownVisibleChange="checkAllModelStatus"
|
||||
:placeholder="placeholder"
|
||||
: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>
|
||||
<a-dropdown trigger="click" @open-change="handleOpenChange">
|
||||
<div class="model-select" :class="modelSelectClasses" @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>
|
||||
</a-select-option>
|
||||
</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>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { embeddingApi } from '@/apis/knowledge_api'
|
||||
import { modelProviderApi } from '@/apis/system_api'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const configStore = useConfigStore()
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'default',
|
||||
validator: (value) => ['default', 'large', 'small'].includes(value)
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择嵌入模型'
|
||||
},
|
||||
size: {
|
||||
type: String,
|
||||
default: 'small',
|
||||
validator: (value) => ['default', 'small', 'middle', 'large'].includes(value)
|
||||
},
|
||||
style: {
|
||||
type: Object,
|
||||
default: () => ({ width: '320px' })
|
||||
default: () => ({ width: '100%' })
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
@ -72,34 +98,85 @@ defineProps({
|
||||
|
||||
const emit = defineEmits(['update:value', 'change'])
|
||||
|
||||
const v2Models = ref({})
|
||||
const state = reactive({
|
||||
modelStatuses: {},
|
||||
v1ModelStatuses: {},
|
||||
v2ModelStatuses: {},
|
||||
checkingStatus: false
|
||||
})
|
||||
|
||||
const embedModelChoices = computed(() => {
|
||||
return Object.keys(configStore?.config?.embed_model_names || {}) || []
|
||||
const displayText = computed(() => props.value || props.placeholder)
|
||||
|
||||
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 checkAllModelStatus = async () => {
|
||||
const handleOpenChange = async (open) => {
|
||||
if (!open) return
|
||||
fetchV2Models()
|
||||
checkV1ModelStatuses()
|
||||
}
|
||||
|
||||
const fetchV2Models = async () => {
|
||||
try {
|
||||
state.checkingStatus = true
|
||||
const response = await embeddingApi.getAllModelsStatus()
|
||||
if (response.status.models) {
|
||||
state.modelStatuses = response.status.models
|
||||
const response = await modelProviderApi.getV2Models('embedding')
|
||||
if (response.success) {
|
||||
v2Models.value = response.data || {}
|
||||
// 拉取到 V2 模型列表后,逐一检查状态
|
||||
await checkV2ModelStatuses()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('检查所有模型状态失败:', error)
|
||||
message.error('获取模型状态失败')
|
||||
console.error('获取 V2 embedding 模型失败:', 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 {
|
||||
state.checkingStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取模型状态图标
|
||||
const getModelStatusIcon = (modelId) => {
|
||||
const status = state.modelStatuses[modelId]
|
||||
const checkV1ModelStatuses = async () => {
|
||||
try {
|
||||
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.status === 'available') return '✓'
|
||||
if (status.status === 'unavailable') return '✗'
|
||||
@ -107,28 +184,40 @@ const getModelStatusIcon = (modelId) => {
|
||||
return '○'
|
||||
}
|
||||
|
||||
// 获取模型状态颜色
|
||||
const getModelStatusColor = (modelId) => {
|
||||
const status = state.modelStatuses[modelId]
|
||||
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 getV1StatusClass = (modelId) => {
|
||||
const status = state.v1ModelStatuses[modelId]
|
||||
return status?.status || ''
|
||||
}
|
||||
|
||||
// 获取模型状态提示文本
|
||||
const getModelStatusTooltip = (modelId) => {
|
||||
const status = state.modelStatuses[modelId]
|
||||
const getV1StatusTooltip = (modelId) => {
|
||||
const status = state.v1ModelStatuses[modelId]
|
||||
if (!status) return '状态未知'
|
||||
let statusText =
|
||||
{ available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知'
|
||||
return `${statusText}: ${status.message || '无详细信息'}`
|
||||
}
|
||||
|
||||
let statusText = ''
|
||||
if (status.status === 'available') statusText = '可用'
|
||||
else if (status.status === 'unavailable') statusText = '不可用'
|
||||
else if (status.status === 'error') statusText = '错误'
|
||||
// V2 模型状态辅助函数
|
||||
const getV2StatusIcon = (spec) => {
|
||||
const status = state.v2ModelStatuses[spec]
|
||||
if (!status) return '○'
|
||||
if (status.status === 'available') return '✓'
|
||||
if (status.status === 'unavailable') return '✗'
|
||||
if (status.status === 'error') return '⚠'
|
||||
return '○'
|
||||
}
|
||||
|
||||
const message = status.message || '无详细信息'
|
||||
return `${statusText}: ${message}`
|
||||
const getV2StatusClass = (spec) => {
|
||||
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) => {
|
||||
@ -138,5 +227,46 @@ const handleSelect = (value) => {
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@ -14,8 +14,8 @@
|
||||
<template #description>
|
||||
项目已启用新版的模型配置,自定义程度更高,且支持 Embedding / Rerank 模型,您可以
|
||||
<a @click="goToNewModelConfig">点击此处</a>
|
||||
跳转到新版模型配置页面。
|
||||
已配置模型将会在 0.6.x 期间继续使用,0.7 版本将不再支持老版的模型调用。
|
||||
跳转到新版模型配置页面。 已配置模型将会在 0.6.x 期间继续使用,0.7
|
||||
版本将不再支持老版的模型调用。
|
||||
为了更加稳定的模型调用和更好的用户体验,建议尽快迁移到新的模型配置。
|
||||
</template>
|
||||
</a-alert>
|
||||
@ -25,7 +25,9 @@
|
||||
</div>
|
||||
|
||||
<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-item label="API地址">{{ item.base_url }}</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>
|
||||
<span v-else>无</span>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,17 +17,14 @@
|
||||
{{ modelStatusIcon }}
|
||||
</span>
|
||||
<a-tooltip title="刷新缓存">
|
||||
<a-button
|
||||
type="text"
|
||||
<button
|
||||
:loading="state.refreshingCache"
|
||||
@click.stop="refreshCache"
|
||||
:disabled="state.refreshingCache"
|
||||
class="cache-refresh-button"
|
||||
>
|
||||
<template #icon>
|
||||
<RefreshCw :size="13" :class="{ 'spin': state.refreshingCache }" />
|
||||
</template>
|
||||
</a-button>
|
||||
<RefreshCw :size="13" :class="{ spin: state.refreshingCache }" />
|
||||
</button>
|
||||
</a-tooltip>
|
||||
<a-button
|
||||
:size="buttonSize"
|
||||
@ -48,7 +45,7 @@
|
||||
<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">v2</a-tag>
|
||||
<a-tag color="success" size="small" class="provider-tag">新</a-tag>
|
||||
</template>
|
||||
<a-menu-item
|
||||
v-for="model in providerData.models"
|
||||
@ -199,170 +196,28 @@ const handleSelectV2Model = (spec) => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 变量定义
|
||||
@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;
|
||||
@import '@/assets/css/model-selector-common.less';
|
||||
|
||||
// 主选择器样式
|
||||
.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;
|
||||
|
||||
// 状态检查按钮
|
||||
.status-check-button {
|
||||
font-size: 12px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
// 缓存刷新按钮
|
||||
.cache-refresh-button {
|
||||
font-size: 12px;
|
||||
padding: 0 4px;
|
||||
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 {
|
||||
font-size: @status-check-button-font-size;
|
||||
padding: @status-check-button-padding;
|
||||
}
|
||||
|
||||
// 缓存刷新按钮
|
||||
.cache-refresh-button {
|
||||
font-size: @status-check-button-font-size;
|
||||
padding: @status-check-button-padding;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 24px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
width: 24px;
|
||||
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) {
|
||||
&.scrollable-menu {
|
||||
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>
|
||||
|
||||
<!-- 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">
|
||||
<div class="tool-result-content" :data-tool-call-id="toolCall.id">
|
||||
<!-- Default rendering -->
|
||||
@ -234,7 +234,7 @@ const formatResultData = (data) => {
|
||||
background-color: var(--gray-25);
|
||||
}
|
||||
|
||||
&>span {
|
||||
& > span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@ -13,8 +13,12 @@
|
||||
</span>
|
||||
<span class="summary-content">
|
||||
<span class="summary-title">{{ toolCallsSummaryTitle }}</span>
|
||||
<span class="summary-separator" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">·</span>
|
||||
<span class="summary-meta" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">{{ toolCallsNamesMeta }}</span>
|
||||
<span class="summary-separator" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta"
|
||||
>·</span
|
||||
>
|
||||
<span class="summary-meta" v-if="normalizedToolCalls.length > 1 && toolCallsNamesMeta">{{
|
||||
toolCallsNamesMeta
|
||||
}}</span>
|
||||
<span class="summary-status-tag" v-if="statusSummary">{{ statusSummary }}</span>
|
||||
</span>
|
||||
<span class="summary-trailing">
|
||||
@ -123,8 +127,9 @@ const statusSummary = computed(() => {
|
||||
(toolCall) =>
|
||||
toolCall.status !== 'success' && toolCall.status !== 'error' && !toolCall.tool_call_result
|
||||
).length
|
||||
const errorCount = normalizedToolCalls.value.filter((toolCall) => toolCall.status === 'error')
|
||||
.length
|
||||
const errorCount = normalizedToolCalls.value.filter(
|
||||
(toolCall) => toolCall.status === 'error'
|
||||
).length
|
||||
|
||||
const parts = []
|
||||
if (successCount > 0 && successCount === normalizedToolCalls.value.length) {
|
||||
|
||||
@ -208,10 +208,7 @@
|
||||
|
||||
<!-- 部门选择器(仅超级管理员可见) -->
|
||||
<a-form-item v-if="userStore.isSuperAdmin" label="部门" class="form-item">
|
||||
<a-select
|
||||
v-model:value="userManagement.form.departmentId"
|
||||
placeholder="请选择部门"
|
||||
>
|
||||
<a-select v-model:value="userManagement.form.departmentId" placeholder="请选择部门">
|
||||
<a-select-option
|
||||
v-for="dept in departmentManagement.departments"
|
||||
:key="dept.id"
|
||||
|
||||
@ -36,7 +36,10 @@ export const useChatThreadsStore = defineStore('chatThreads', () => {
|
||||
const fetchedThreads = await threadApi.getThreads(agentId, PAGE_SIZE, 0)
|
||||
threads.value = fetchedThreads || []
|
||||
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
|
||||
}
|
||||
return threads.value
|
||||
|
||||
@ -1,32 +1,32 @@
|
||||
const ICON_BASE = 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons'
|
||||
|
||||
export const modelIcons = {
|
||||
'default': `${ICON_BASE}/default.svg`,
|
||||
'alibaba': `${ICON_BASE}/bailian-color.svg`,
|
||||
default: `${ICON_BASE}/default.svg`,
|
||||
alibaba: `${ICON_BASE}/bailian-color.svg`,
|
||||
'alibaba-coding-plan': `${ICON_BASE}/alibabacloud-color.svg`,
|
||||
'alibaba-coding-plan-cn': `${ICON_BASE}/alibabacloud-color.svg`,
|
||||
'anthropic': `${ICON_BASE}/anthropic.svg`,
|
||||
'ark': `${ICON_BASE}/volcengine-color.svg`,
|
||||
'dashscope': `${ICON_BASE}/bailian-color.svg`,
|
||||
'deepseek': `${ICON_BASE}/deepseek-color.svg`,
|
||||
'google': `${ICON_BASE}/google-color.svg`,
|
||||
'lmstudio': `${ICON_BASE}/lmstudio.svg`,
|
||||
'minimax': `${ICON_BASE}/minimax-color.svg`,
|
||||
anthropic: `${ICON_BASE}/anthropic.svg`,
|
||||
ark: `${ICON_BASE}/volcengine-color.svg`,
|
||||
dashscope: `${ICON_BASE}/bailian-color.svg`,
|
||||
deepseek: `${ICON_BASE}/deepseek-color.svg`,
|
||||
google: `${ICON_BASE}/google-color.svg`,
|
||||
lmstudio: `${ICON_BASE}/lmstudio.svg`,
|
||||
minimax: `${ICON_BASE}/minimax-color.svg`,
|
||||
'minimax-cn': `${ICON_BASE}/minimax-color.svg`,
|
||||
'modelscope': `${ICON_BASE}/modelscope-color.svg`,
|
||||
'moonshotai': `${ICON_BASE}/moonshot.svg`,
|
||||
modelscope: `${ICON_BASE}/modelscope-color.svg`,
|
||||
moonshotai: `${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`,
|
||||
'opencode': `${ICON_BASE}/opencode.svg`,
|
||||
'openai': `${ICON_BASE}/openai.svg`,
|
||||
'openrouter': `${ICON_BASE}/openrouter.svg`,
|
||||
'siliconflow': `${ICON_BASE}/siliconcloud-color.svg`,
|
||||
opencode: `${ICON_BASE}/opencode.svg`,
|
||||
openai: `${ICON_BASE}/openai.svg`,
|
||||
openrouter: `${ICON_BASE}/openrouter.svg`,
|
||||
siliconflow: `${ICON_BASE}/siliconcloud-color.svg`,
|
||||
'siliconflow-cn': `${ICON_BASE}/siliconcloud-color.svg`,
|
||||
'together': `${ICON_BASE}/together-color.svg`,
|
||||
'zai': `${ICON_BASE}/zai.svg`,
|
||||
together: `${ICON_BASE}/together-color.svg`,
|
||||
zai: `${ICON_BASE}/zai.svg`,
|
||||
'zai-coding-plan': `${ICON_BASE}/zai.svg`,
|
||||
'zhipu': `${ICON_BASE}/zhipu-color.svg`,
|
||||
'zhipuai': `${ICON_BASE}/zhipu-color.svg`,
|
||||
zhipu: `${ICON_BASE}/zhipu-color.svg`,
|
||||
zhipuai: `${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()
|
||||
|
||||
// 从 agentStore 中获取响应式状态
|
||||
const {
|
||||
selectedAgentId,
|
||||
defaultAgentId,
|
||||
selectedAgentConfigId,
|
||||
agentConfigs,
|
||||
isLoadingConfig
|
||||
} = storeToRefs(agentStore)
|
||||
const { selectedAgentId, defaultAgentId, selectedAgentConfigId, agentConfigs, isLoadingConfig } =
|
||||
storeToRefs(agentStore)
|
||||
|
||||
const syncingRouteThread = ref(false)
|
||||
|
||||
|
||||
@ -95,7 +95,9 @@ const filteredProviders = computed(() => {
|
||||
})
|
||||
|
||||
const providerStats = computed(() => {
|
||||
let enabled = 0, warning = 0, models = 0
|
||||
let enabled = 0,
|
||||
warning = 0,
|
||||
models = 0
|
||||
for (const p of providers.value) {
|
||||
if (p.is_enabled) {
|
||||
enabled++
|
||||
@ -989,8 +991,7 @@ onMounted(loadProviders)
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="remote-fetch-actions">
|
||||
</div>
|
||||
<div class="remote-fetch-actions"></div>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user