diff --git a/AGENTS.md b/AGENTS.md index f2f03852..533d72ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,13 +11,15 @@ Yuxi-Know/ ├── server/ # 服务端代码(部分) ├── src/ # 主要源代码目录 │ ├── agents/ # 智能体应用 -│ ├── config/ # 配置文件 +│ ├── config/ # 配置文件 │ ├── knowledge/ # 知识库相关 │ ├── models/ # 数据模型 │ ├── plugins/ # 插件(存放OCR) │ ├── static/ # 静态资源(配置文件) │ └── utils/ # 工具函数 -├── web/ # 前端代码 +├── web/src/ # 前端代码 +│ ├── apis/ # 前端的接口定义 +│ └── views/ # 主要 vue 文件 └── docker-compose.yml # Docker Compose 配置 ``` @@ -37,7 +39,20 @@ Yuxi-Know/ 核心原则: 由于 api-dev 和 web-dev 服务均配置了热重载 (hot-reloading),本地修改代码后无需重启容器,服务会自动更新。应该先检查项目是否已经在后台启动(`docker ps`),具体的可以阅读 [docker-compose.yml](docker-compose.yml). -## 风格说明 +前端开发规范: -- UI风格要简洁,同时要保持一致性,颜色要尽量参考 [base.css](web/src/assets/css/base.css) 中的颜色。不要悬停位移,不要过度使用阴影以及渐变色。 -- Python 代码要符合 Python 的规范,尽量使用较新的语法,避免使用旧版本的语法(版本兼容到 3.12+),使用 uvx ruff check 检查 lint。 +- API 接口规范:所有的 API 接口都应该定义在 web/src/apis 下面,并继承自 apiGet/apiPost/apiRequest +- Icon 应该从 @ant-design/icons-vue 或者 lucide-vue-next +- Vue 中的样式使用 less,并尽量使用[base.css](web/src/assets/css/base.css) 中的颜色。 +- UI风格要简洁,同时要保持一致性,颜色要尽量参考 不要悬停位移,不要过度使用阴影以及渐变色。 + + +后端开发规范: + +- 项目使用 uv 来管理依赖 +- Python 代码要符合 Python 的规范,尽量使用较新的语法,避免使用旧版本的语法(版本兼容到 3.12+),使用 make lint 检查 lint。使用 make format 来格式化代码。 + +其他: + +- 如果需要新建说明文档,则保存在 docs/vibe 文件夹下面 +- 测试脚本可以放在 test 文件夹下面,可以从 docker 中启动测试 diff --git a/server/routers/knowledge_router.py b/server/routers/knowledge_router.py index cd01dcfb..b58cd2b5 100644 --- a/server/routers/knowledge_router.py +++ b/server/routers/knowledge_router.py @@ -10,6 +10,7 @@ from server.models.user_model import User from server.utils.auth_middleware import get_admin_user from src import config, knowledge_base from src.knowledge.indexing import process_file_to_markdown +from src.models.embed import test_embedding_model_status, test_all_embedding_models_status from src.utils import hashstr, logger knowledge = APIRouter(prefix="/knowledge", tags=["knowledge"]) @@ -554,3 +555,35 @@ async def get_knowledge_base_statistics(current_user: User = Depends(get_admin_u except Exception as e: logger.error(f"获取知识库统计失败 {e}, {traceback.format_exc()}") return {"message": f"获取知识库统计失败 {e}", "stats": {}} + + +# ============================================================================= +# === Embedding模型状态检查分组 === +# ============================================================================= + + +@knowledge.get("/embedding-models/{model_id}/status") +async def get_embedding_model_status(model_id: str, current_user: User = Depends(get_admin_user)): + """获取指定embedding模型的状态""" + logger.debug(f"Checking embedding model status: {model_id}") + try: + status = await test_embedding_model_status(model_id) + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取embedding模型状态失败 {model_id}: {e}, {traceback.format_exc()}") + return { + "message": f"获取embedding模型状态失败: {e}", + "status": {"model_id": model_id, "status": "error", "message": str(e)}, + } + + +@knowledge.get("/embedding-models/status") +async def get_all_embedding_models_status(current_user: User = Depends(get_admin_user)): + """获取所有embedding模型的状态""" + logger.debug("Checking all embedding models status") + try: + status = await test_all_embedding_models_status() + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取所有embedding模型状态失败: {e}, {traceback.format_exc()}") + return {"message": f"获取所有embedding模型状态失败: {e}", "status": {"models": {}, "total": 0, "available": 0}} diff --git a/server/routers/system_router.py b/server/routers/system_router.py index 0322ad07..65efd484 100644 --- a/server/routers/system_router.py +++ b/server/routers/system_router.py @@ -9,6 +9,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException from server.models.user_model import User from server.utils.auth_middleware import get_admin_user, get_superadmin_user from src import config, graph_base +from src.models.chat import test_chat_model_status, test_all_chat_models_status from src.utils.logging_config import logger system = APIRouter(prefix="/system", tags=["system"]) @@ -244,3 +245,35 @@ async def check_ocr_services_health(current_user: User = Depends(get_admin_user) overall_status = "healthy" if any(svc["status"] == "healthy" for svc in health_status.values()) else "unhealthy" return {"overall_status": overall_status, "services": health_status, "message": "OCR服务健康检查完成"} + + +# ============================================================================= +# === 聊天模型状态检查分组 === +# ============================================================================= + + +@system.get("/chat-models/status") +async def get_chat_model_status(provider: str, model_name: str, current_user: User = Depends(get_admin_user)): + """获取指定聊天模型的状态""" + logger.debug(f"Checking chat model status: {provider}/{model_name}") + try: + status = await test_chat_model_status(provider, model_name) + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取聊天模型状态失败 {provider}/{model_name}: {e}") + return { + "message": f"获取聊天模型状态失败: {e}", + "status": {"provider": provider, "model_name": model_name, "status": "error", "message": str(e)}, + } + + +@system.get("/chat-models/all/status") +async def get_all_chat_models_status(current_user: User = Depends(get_admin_user)): + """获取所有聊天模型的状态""" + logger.debug("Checking all chat models status") + try: + status = await test_all_chat_models_status() + return {"status": status, "message": "success"} + except Exception as e: + logger.error(f"获取所有聊天模型状态失败: {e}") + return {"message": f"获取所有聊天模型状态失败: {e}", "status": {"models": {}, "total": 0, "available": 0}} diff --git a/src/models/chat.py b/src/models/chat.py index cc65cd31..c79f8949 100644 --- a/src/models/chat.py +++ b/src/models/chat.py @@ -131,5 +131,70 @@ def get_custom_model(model_id): return modle_info +async def test_chat_model_status(provider: str, model_name: str) -> dict: + """ + 测试指定聊天模型的状态 + + Args: + provider: 模型提供商 + model_name: 模型名称 + + Returns: + dict: 包含状态信息的字典 + """ + try: + # 加载模型 + logger.debug(f"Selecting chat model {provider}/{model_name}") + model = select_model(provider, model_name) + + # 使用简单的测试消息 + test_messages = [{"role": "user", "content": "Say 1"}] + + # 发送测试请求 + response = model.call(test_messages, stream=False) + logger.debug(f"Test chat model status response: {response}") + + # 检查响应是否有效 + if response and response.content: + return {"provider": provider, "model_name": model_name, "status": "available", "message": "连接正常"} + else: + return {"provider": provider, "model_name": model_name, "status": "unavailable", "message": "响应无效"} + + except Exception as e: + logger.error(f"测试聊天模型状态失败 {provider}/{model_name}: {e}") + return {"provider": provider, "model_name": model_name, "status": "error", "message": str(e)} + + +async def test_all_chat_models_status() -> dict: + """ + 测试所有支持的聊天模型状态 + + Returns: + dict: 包含所有模型状态的字典 + """ + from src import config + + results = {} + + # 获取所有可用的模型 + for provider, provider_info in config.model_names.items(): + if provider == "custom": + # 处理自定义模型 + for custom_model in config.custom_models: + model_id = f"custom/{custom_model['custom_id']}" + status = await test_chat_model_status("custom", custom_model["custom_id"]) + results[model_id] = status + else: + # 处理普通模型 + for model_name in provider_info.models: + model_id = f"{provider}/{model_name}" + status = await test_chat_model_status(provider, model_name) + results[model_id] = status + + available_count = len([m for m in results.values() if m["status"] == "available"]) + + return {"models": results, "total": len(results), "available": available_count} + + if __name__ == "__main__": pass diff --git a/src/models/embed.py b/src/models/embed.py index 61efbd2e..bef6abad 100644 --- a/src/models/embed.py +++ b/src/models/embed.py @@ -168,12 +168,89 @@ class OtherEmbedding(BaseEmbeddingModel): logger.error(f"Other Embedding async request failed: {e}, {payload}") raise ValueError(f"Other Embedding async request failed: {e}") + async def test_connection(self) -> tuple[bool, str]: + """ + 测试embedding模型的连接性 + + Returns: + tuple: (success: bool, message: str) + """ + try: + # 使用简单的测试文本 + test_text = ["Hello world"] + await self.aencode(test_text) + return True, "连接正常" + except Exception as e: + error_msg = str(e) + return False, error_msg + + +async def test_embedding_model_status(model_id: str) -> dict: + """ + 测试指定embedding模型的状态 + + Args: + model_id: 模型ID,格式为 "provider/model_name" + + Returns: + dict: 包含状态信息的字典 + """ + try: + support_embed_models = config.embed_model_names.keys() + if model_id not in support_embed_models: + return {"model_id": model_id, "status": "unsupported", "message": f"不支持的模型: {model_id}"} + + # 选择并创建模型实例 + model = select_embedding_model(model_id) + + # 测试连接 + success, message = await model.test_connection() + + return { + "model_id": model_id, + "status": "available" if success else "unavailable", + "message": message if not success else "连接正常", + "dimension": model.dimension, + } + + except Exception as e: + logger.warning(f"测试embedding模型状态失败 {model_id}: {e}") + return {"model_id": model_id, "status": "error", "message": str(e)} + + +async def test_all_embedding_models_status() -> dict: + """ + 测试所有支持的embedding模型状态 + + Returns: + dict: 包含所有模型状态的字典 + """ + support_embed_models = list(config.embed_model_names.keys()) + results = {} + + # 并发测试所有模型 + tasks = [test_embedding_model_status(model_id) for model_id in support_embed_models] + model_statuses = await asyncio.gather(*tasks, return_exceptions=True) + + for i, status in enumerate(model_statuses): + if isinstance(status, Exception): + model_id = support_embed_models[i] + results[model_id] = {"model_id": model_id, "status": "error", "message": str(status)} + else: + results[status["model_id"]] = status + + return { + "models": results, + "total": len(support_embed_models), + "available": len([m for m in results.values() if m["status"] == "available"]), + } + def select_embedding_model(model_id): provider, model_name = model_id.split("/", 1) if model_id else ("", "") 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}" - logger.debug(f"Loading embedding model {model_id}") + logger.info(f"Loading embedding model {model_id}") if provider == "local": raise ValueError("Local embedding model is not supported, please use other embedding models") diff --git a/src/utils/logging_config.py b/src/utils/logging_config.py index 3aea635f..22182af6 100644 --- a/src/utils/logging_config.py +++ b/src/utils/logging_config.py @@ -20,7 +20,7 @@ def setup_logger(name, level="DEBUG", console=True): loguru_logger.add( LOG_FILE, level=level, - format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {name}:{line} - {message}", + format="{time:YYYY-MM-DD HH:mm:ss} - {level} - {file}:{line} - {message}", encoding="utf-8", rotation="10 MB", # 文件大小达到 10MB 时轮转 retention="30 days", # 保留30天的日志 @@ -35,7 +35,7 @@ def setup_logger(name, level="DEBUG", console=True): format=( "{time:MM-DD HH:mm:ss} " "{level} " - "{name}:{line}: " + "{file}:{line}: " "{message}" ), colorize=True, diff --git a/web/src/apis/knowledge_api.js b/web/src/apis/knowledge_api.js index e438e3b4..45b71c7c 100644 --- a/web/src/apis/knowledge_api.js +++ b/web/src/apis/knowledge_api.js @@ -198,3 +198,26 @@ export const typeApi = { } } +// ============================================================================= +// === Embedding模型状态检查分组 === +// ============================================================================= + +export const embeddingApi = { + /** + * 获取指定embedding模型的状态 + * @param {string} modelId - 模型ID + * @returns {Promise} - 模型状态 + */ + getModelStatus: async (modelId) => { + return apiAdminGet(`/api/knowledge/embedding-models/${modelId}/status`) + }, + + /** + * 获取所有embedding模型的状态 + * @returns {Promise} - 所有模型状态 + */ + getAllModelsStatus: async () => { + return apiAdminGet('/api/knowledge/embedding-models/status') + } +} + diff --git a/web/src/apis/system_api.js b/web/src/apis/system_api.js index a9d191cd..7cf079fa 100644 --- a/web/src/apis/system_api.js +++ b/web/src/apis/system_api.js @@ -100,5 +100,29 @@ export const ocrApi = { getHealth: async () => apiAdminGet('/api/system/ocr/health') } +// ============================================================================= +// === 聊天模型状态检查分组 === +// ============================================================================= + +export const chatModelApi = { + /** + * 获取指定聊天模型的状态 + * @param {string} provider - 模型提供商 + * @param {string} modelName - 模型名称 + * @returns {Promise} - 模型状态 + */ + getModelStatus: async (provider, modelName) => { + return apiAdminGet(`/api/system/chat-models/status?provider=${encodeURIComponent(provider)}&model_name=${encodeURIComponent(modelName)}`) + }, + + /** + * 获取所有聊天模型的状态 + * @returns {Promise} - 所有模型状态 + */ + getAllModelsStatus: async () => { + return apiAdminGet('/api/system/chat-models/all/status') + } +} + diff --git a/web/src/components/ModelSelectorComponent.vue b/web/src/components/ModelSelectorComponent.vue index 7bf67a0c..225551ac 100644 --- a/web/src/components/ModelSelectorComponent.vue +++ b/web/src/components/ModelSelectorComponent.vue @@ -1,12 +1,35 @@ - - - - {{ model_name }} - - {{ model_provider }} - + + + + + {{ model_name }} + + {{ model_provider }} + + + + {{ modelStatusIcon }} + + + {{ state.checkingStatus ? '检查中...' : '检查' }} + + + + @@ -25,9 +48,9 @@ diff --git a/web/src/views/SettingView.vue b/web/src/views/SettingView.vue index 6cc68b0a..31deea0b 100644 --- a/web/src/views/SettingView.vue +++ b/web/src/views/SettingView.vue @@ -26,20 +26,50 @@ /> - {{ items?.embed_model.des }} - + {{ items?.embed_model.des }} + + + {{ name }} + :value="name" + > + + + {{ name }} + + + {{ getModelStatusIcon(name) }} + + {{ items?.reranker.des }} - @@ -156,6 +186,7 @@ import ModelProvidersComponent from '@/components/ModelProvidersComponent.vue'; import UserManagementComponent from '@/components/UserManagementComponent.vue'; import { notification, Button } from 'ant-design-vue'; import { configApi } from '@/apis/system_api' +import { embeddingApi } from '@/apis/knowledge_api' import ModelSelectorComponent from '@/components/ModelSelectorComponent.vue'; const configStore = useConfigStore() @@ -165,7 +196,9 @@ const isNeedRestart = ref(false) const state = reactive({ loading: false, section: 'base', - windowWidth: window?.innerWidth || 0 + windowWidth: window?.innerWidth || 0, + modelStatuses: {}, // 存储embedding模型状态 + checkingStatus: false // 是否正在检查状态 }) const handleModelLocalPathsUpdate = (config) => { @@ -225,6 +258,8 @@ onMounted(() => { updateWindowWidth() window.addEventListener('resize', updateWindowWidth) state.section = userStore.isSuperAdmin ? 'base' : 'user' + + checkAllModelStatus() }) onUnmounted(() => { @@ -249,6 +284,56 @@ const sendRestart = () => { }); } + +// 检查所有embedding模型状态 +const checkAllModelStatus = async () => { + try { + state.checkingStatus = true + const response = await embeddingApi.getAllModelsStatus() + if (response.status.models) { + state.modelStatuses = response.status.models + } + } catch (error) { + console.error('检查所有模型状态失败:', error) + message.error('获取模型状态失败') + } finally { + state.checkingStatus = false + } +} + +// 获取模型状态图标 +const getModelStatusIcon = (modelId) => { + const status = state.modelStatuses[modelId] + if (!status) return '○' + if (status.status === 'available') return '✓' + if (status.status === 'unavailable') return '✗' + if (status.status === 'error') return '⚠' + return '○' +} + +// 获取模型状态颜色 +const getModelStatusColor = (modelId) => { + const status = state.modelStatuses[modelId] + if (!status) return '#999' + if (status.status === 'available') return '#52c41a' + if (status.status === 'unavailable') return '#ff4d4f' + if (status.status === 'error') return '#faad14' + return '#999' +} +// 获取模型状态提示文本 +const getModelStatusTooltip = (modelId) => { + const status = state.modelStatuses[modelId] + if (!status) return '状态未知' + + let statusText = '' + if (status.status === 'available') statusText = '可用' + else if (status.status === 'unavailable') statusText = '不可用' + else if (status.status === 'error') statusText = '错误' + + const message = status.message || '无详细信息' + return `${statusText}: ${message}` +} + const openLink = (url) => { window.open(url, '_blank') }