diff --git a/src/app/deployment/components/modes.py b/src/app/deployment/components/modes.py index f3f6135b1a0d20a7857f530a543629e24ced98c9..f6347f4611399742baa94cebc84e9fd761e567b8 100644 --- a/src/app/deployment/components/modes.py +++ b/src/app/deployment/components/modes.py @@ -16,10 +16,10 @@ from textual.containers import Container, Horizontal from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Static -from app.deployment.validators import validate_oi_connection from config.manager import ConfigManager from config.model import Backend from log.manager import get_logger +from tool.validators import validate_oi_connection from . import EnvironmentCheckScreen diff --git a/src/app/deployment/models.py b/src/app/deployment/models.py index 273e05afccd7571992cf68391a413027480739de..440d49fb2ce6c0355ebb95fff35acfcd529b17db 100644 --- a/src/app/deployment/models.py +++ b/src/app/deployment/models.py @@ -10,6 +10,8 @@ import re from dataclasses import dataclass, field from enum import Enum +from tool.validators import APIValidator + # 常量定义 MAX_TEMPERATURE = 2.0 MIN_TEMPERATURE = 0.0 @@ -110,9 +112,6 @@ class DeploymentConfig: tuple[bool, str, dict]: (是否验证成功, 消息, 验证详细信息) """ - # 懒导入以避免循环导入 - from .validators import APIValidator # noqa: PLC0415 - # 检查必要字段是否完整 if not (self.llm.endpoint.strip() and self.llm.api_key.strip() and self.llm.model.strip()): return False, "LLM 基础配置不完整", {} @@ -138,9 +137,6 @@ class DeploymentConfig: tuple[bool, str, dict]: (是否验证成功, 消息, 验证详细信息) """ - # 懒导入以避免循环导入 - from .validators import APIValidator # noqa: PLC0415 - # 检查必要字段是否完整 if not (self.embedding.endpoint.strip() and self.embedding.api_key.strip() and self.embedding.model.strip()): return False, "Embedding 基础配置不完整", {} diff --git a/src/app/settings.py b/src/app/settings.py index 7e10792a443578430e818e9be9ec9658edebbe98..c7b5074250ad4136f47c2639656fd357eaad30ea 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -13,6 +13,7 @@ from textual.widgets import Button, Input, Label, Static from backend.hermes import HermesChatClient from backend.openai import OpenAIClient from config import Backend, ConfigManager +from tool.validators import APIValidator, validate_oi_connection if TYPE_CHECKING: from textual.app import ComposeResult @@ -37,6 +38,11 @@ class SettingsScreen(Screen): # 添加保存任务的集合 self.background_tasks: set[asyncio.Task] = set() + # 验证相关状态 + self.is_validated = False + self.validation_message = "" + self.validator = APIValidator() + def compose(self) -> ComposeResult: """构建设置页面""" yield Container( @@ -109,6 +115,11 @@ class SettingsScreen(Screen): self.background_tasks.add(task) task.add_done_callback(self.background_tasks.discard) + # 启动配置验证 + validation_task = asyncio.create_task(self._validate_configuration()) + self.background_tasks.add(validation_task) + validation_task.add_done_callback(self.background_tasks.discard) + # 确保操作按钮始终可见 self._ensure_buttons_visible() @@ -134,10 +145,15 @@ class SettingsScreen(Screen): @on(Input.Changed, "#base-url, #api-key") def on_config_changed(self) -> None: - """当 Base URL 或 API Key 改变时更新客户端""" + """当 Base URL 或 API Key 改变时更新客户端并验证配置""" if self.backend == Backend.OPENAI: self._update_llm_client() + # 重新验证配置 + validation_task = asyncio.create_task(self._validate_configuration()) + self.background_tasks.add(validation_task) + validation_task.add_done_callback(self.background_tasks.discard) + @on(Button.Pressed, "#backend-btn") def toggle_backend(self) -> None: """切换后端""" @@ -197,6 +213,11 @@ class SettingsScreen(Screen): # 确保按钮可见 self._ensure_buttons_visible() + # 切换后端后重新验证配置 + validation_task = asyncio.create_task(self._validate_configuration()) + self.background_tasks.add(validation_task) + validation_task.add_done_callback(self.background_tasks.discard) + @on(Button.Pressed, "#model-btn") def toggle_model(self) -> None: """循环切换模型""" @@ -216,6 +237,11 @@ class SettingsScreen(Screen): # 更新按钮文本 model_btn = self.query_one("#model-btn", Button) model_btn.label = self.selected_model + + # 模型改变时重新验证配置 + validation_task = asyncio.create_task(self._validate_configuration()) + self.background_tasks.add(validation_task) + validation_task.add_done_callback(self.background_tasks.discard) except (IndexError, ValueError): # 处理任何可能的异常 self.selected_model = self.models[0] if self.models else "默认模型" @@ -225,6 +251,10 @@ class SettingsScreen(Screen): @on(Button.Pressed, "#save-btn") def save_settings(self) -> None: """保存设置""" + # 检查验证状态 + if not self.is_validated: + return + self.config_manager.set_backend(self.backend) base_url = self.query_one("#base-url", Input).value @@ -271,6 +301,49 @@ class SettingsScreen(Screen): self.background_tasks.add(task) task.add_done_callback(self.background_tasks.discard) + async def _validate_configuration(self) -> None: + """验证当前配置""" + base_url = self.query_one("#base-url", Input).value.strip() + api_key = self.query_one("#api-key", Input).value.strip() + + if not base_url: + self.is_validated = False + self.validation_message = "Base URL 不能为空" + self._update_save_button_state() + return + + try: + if self.backend == Backend.OPENAI: + # 验证 OpenAI 配置 + model = self.selected_model if self.selected_model else "gpt-3.5-turbo" + valid, message, _ = await self.validator.validate_llm_config( + endpoint=base_url, + api_key=api_key, + model=model, + timeout=10, + ) + self.is_validated = valid + self.validation_message = message + else: + # 验证 openEuler Intelligence 配置 + valid, message = await validate_oi_connection(base_url, api_key) + self.is_validated = valid + self.validation_message = message + + except (TimeoutError, ValueError, RuntimeError) as e: + self.is_validated = False + self.validation_message = f"验证过程中发生错误: {e!s}" + + self._update_save_button_state() + + def _update_save_button_state(self) -> None: + """根据验证状态更新保存按钮""" + save_btn = self.query_one("#save-btn", Button) + if self.is_validated: + save_btn.disabled = False + else: + save_btn.disabled = True + def _update_llm_client(self) -> None: """根据当前UI中的配置更新LLM客户端""" base_url_input = self.query_one("#base-url", Input) diff --git a/src/app/tui.py b/src/app/tui.py index 97457f3bef16dc9b66447e8b0c75d319c373105a..6f33963132ee8ec7d163dae5d7a63920637032f6 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -27,8 +27,10 @@ from backend.hermes.mcp_helpers import ( is_mcp_message, ) from config import ConfigManager +from config.model import Backend from log.manager import get_logger, log_exception from tool.command_processor import process_command +from tool.validators import APIValidator, validate_oi_connection if TYPE_CHECKING: from textual.events import Key as KeyEvent @@ -1192,14 +1194,137 @@ class IntelligentTerminal(App): self._initialize_default_agent() def _initialize_default_agent(self) -> None: - """初始化默认智能体,如果需要的话异步更新智能体名称""" - # 如果当前智能体是基于 default_app 配置的,且需要更新名称 - app_id, name = self.current_agent - if app_id and app_id == name: # 这表示我们在 _get_initial_agent 中使用了临时方案 - # 异步获取智能体信息并更新名称 - task = asyncio.create_task(self._update_agent_name_from_list()) - self.background_tasks.add(task) - task.add_done_callback(self._task_done_callback) + """初始化默认智能体,包含配置验证""" + # 首先验证后端配置 + validation_task = asyncio.create_task(self._validate_and_setup_configuration()) + self.background_tasks.add(validation_task) + validation_task.add_done_callback(self._task_done_callback) + + async def _validate_and_setup_configuration(self) -> None: + """验证配置并设置智能体,如果配置无效则弹出设置页面""" + try: + # 获取当前后端配置 + backend = self.config_manager.get_backend() + + # 验证配置 + is_valid = await self._validate_backend_configuration(backend) + + if is_valid: + # 配置验证通过,继续初始化智能体 + await self._setup_agent_after_validation() + else: + # 配置验证失败,显示通知并弹出设置页面 + self._show_config_validation_notification() + await self._show_settings_for_config_fix() + + except Exception: + self.logger.exception("配置验证过程中发生错误") + # 即使验证出错,也弹出设置页面让用户手动配置 + self._show_config_validation_notification() + await self._show_settings_for_config_fix() + + async def _validate_backend_configuration(self, backend: Backend) -> bool: + """验证后端配置""" + try: + validator = APIValidator() + + if backend == Backend.OPENAI: + # 验证 OpenAI 配置 + base_url = self.config_manager.get_base_url() + api_key = self.config_manager.get_api_key() + model = self.config_manager.get_model() + valid, _, _ = await validator.validate_llm_config( + endpoint=base_url, + api_key=api_key, + model=model, + timeout=10, + ) + return valid + + if backend == Backend.EULERINTELLI: + # 验证 openEuler Intelligence 配置 + base_url = self.config_manager.get_eulerintelli_url() + api_key = self.config_manager.get_eulerintelli_key() + valid, _ = await validate_oi_connection(base_url, api_key) + return valid + + except Exception: + self.logger.exception("验证后端配置时发生错误") + return False + + else: + return False + + def _show_config_validation_notification(self) -> None: + """显示配置验证失败的通知""" + self.notify( + "后端配置验证失败,请检查并修改配置", + title="配置错误", + severity="error", + timeout=0.5, + ) + + async def _show_settings_for_config_fix(self) -> None: + """弹出设置页面让用户修改配置""" + try: + # 弹出设置页面 + settings_screen = SettingsScreen(self.config_manager, self.get_llm_client()) + self.push_screen(settings_screen) + + # 等待设置页面退出 + await self._wait_for_settings_screen_exit() + + # 设置页面退出后,重新验证配置 + backend = self.config_manager.get_backend() + is_valid = await self._validate_backend_configuration(backend) + + if not is_valid: + # 如果还是无效,递归调用自己再次弹出设置页面 + self._show_config_validation_notification() + await self._show_settings_for_config_fix() + else: + # 配置验证通过,继续初始化智能体 + await self._setup_agent_after_validation() + + except Exception: + self.logger.exception("显示设置页面时发生错误") + + async def _wait_for_settings_screen_exit(self) -> None: + """等待设置页面退出""" + # 使用事件来等待设置页面退出,而不是轮询 + exit_event = asyncio.Event() + + # 创建一个任务来监控屏幕栈变化 + async def monitor_screen_stack() -> None: + current_stack_length = len(self.screen_stack) + while current_stack_length > 1: + await asyncio.sleep(0.05) # 短暂等待后重新检查 + current_stack_length = len(self.screen_stack) + exit_event.set() + + # 启动监控任务 + monitor_task = asyncio.create_task(monitor_screen_stack()) + + # 等待退出事件或超时(5分钟) + try: + await asyncio.wait_for(exit_event.wait(), timeout=300.0) + except TimeoutError: + self.logger.warning("等待设置页面退出超时") + finally: + # 取消监控任务 + if not monitor_task.done(): + monitor_task.cancel() + + async def _setup_agent_after_validation(self) -> None: + """配置验证通过后设置智能体""" + try: + # 如果当前智能体是基于 default_app 配置的,且需要更新名称 + app_id, name = self.current_agent + if app_id and app_id == name: # 这表示我们在 _get_initial_agent 中使用了临时方案 + # 异步获取智能体信息并更新名称 + await self._update_agent_name_from_list() + except Exception: + self.logger.exception("设置智能体时发生错误") async def _update_agent_name_from_list(self) -> None: """从智能体列表中更新当前智能体的名称""" diff --git a/src/backend/hermes/__init__.py b/src/backend/hermes/__init__.py index cc257b10a0f49e16587956c90a30dd5ee1ecdc7d..4e2b2e33e22ac70c002fb4622d71c82b4eb3ab10 100644 --- a/src/backend/hermes/__init__.py +++ b/src/backend/hermes/__init__.py @@ -1,6 +1,6 @@ """Hermes Chat API 模块""" -from .client import HermesChatClient, validate_url +from .client import HermesChatClient from .exceptions import HermesAPIError from .models import HermesAgent, HermesApp, HermesChatRequest, HermesFeatures, HermesMessage from .services.agent import HermesAgentManager @@ -23,5 +23,4 @@ __all__ = [ "HermesModelManager", "HermesStreamEvent", "HermesStreamProcessor", - "validate_url", ] diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index be2591987078759154d0bd79a88333ddecae2cde..d74c8a39be56bbe1302090763cf49065cbf2e0d9 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -3,7 +3,6 @@ from __future__ import annotations import json -import re import time from typing import TYPE_CHECKING, Self from urllib.parse import urljoin @@ -31,15 +30,6 @@ if TYPE_CHECKING: from .models import HermesAgent -def validate_url(url: str) -> bool: - """ - 校验 URL 是否合法 - - 校验 URL 是否以 http:// 或 https:// 开头。 - """ - return re.match(r"^https?://", url) is not None - - class HermesChatClient(LLMClientBase): """Hermes Chat API 客户端 - 重构版本""" @@ -47,11 +37,6 @@ class HermesChatClient(LLMClientBase): """初始化 Hermes Chat API 客户端""" self.logger = get_logger(__name__) - if not validate_url(base_url): - msg = "无效的 API URL,请确保 URL 以 http:// 或 https:// 开头。" - self.logger.error(msg) - raise ValueError(msg) - # HTTP 管理器 - 立即初始化 self.http_manager = HermesHttpManager(base_url, auth_token) diff --git a/src/backend/openai.py b/src/backend/openai.py index f00cb16f64a31c6cd955b9d071abd04be49349ed..ba0ffb7991fd44154d216bba65a7825f32efe441 100644 --- a/src/backend/openai.py +++ b/src/backend/openai.py @@ -1,6 +1,5 @@ """OpenAI 大模型客户端""" -import re import time from collections.abc import AsyncGenerator from typing import TYPE_CHECKING @@ -14,15 +13,6 @@ if TYPE_CHECKING: from openai.types.chat import ChatCompletionMessageParam -def validate_url(url: str) -> bool: - """ - 校验 URL 是否合法 - - 校验 URL 是否以 http:// 或 https:// 开头。 - """ - return re.match(r"^https?://", url) is not None - - class OpenAIClient(LLMClientBase): """OpenAI 大模型客户端""" @@ -30,11 +20,6 @@ class OpenAIClient(LLMClientBase): """初始化 OpenAI 大模型客户端""" self.logger = get_logger(__name__) - if not validate_url(base_url): - msg = "无效的 API URL,请确保 URL 以 http:// 或 https:// 开头。" - self.logger.error(msg) - raise ValueError(msg) - self.model = model self.base_url = base_url self.client = AsyncOpenAI( diff --git a/src/app/deployment/validators.py b/src/tool/validators.py similarity index 100% rename from src/app/deployment/validators.py rename to src/tool/validators.py