diff --git a/scripts/tools/uninstaller.sh b/scripts/tools/uninstaller.sh new file mode 100755 index 0000000000000000000000000000000000000000..17dc8942bb902f53e1f2f65d95d106d7df672666 --- /dev/null +++ b/scripts/tools/uninstaller.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# Uninstaller for openEuler Intelligence +# Run as root or with sudo on openEuler + +# Check openEuler environment +if [ ! -f /etc/openEuler-release ]; then + echo "Error: This script must be run on openEuler environment." >&2 + exit 1 +fi + +set -e + +echo "Stopping services..." +systemctl stop framework || true +systemctl stop rag || true + +echo "Removing packages..." +dnf remove -y openeuler-intelligence-* || true +dnf remove -y euler-copilot-framework euler-copilot-rag || true + +echo "Cleaning deployment files..." +rm -rf /opt/copilot +rm -rf /usr/lib/euler-copilot-framework +rm -rf /etc/euler-copilot-framework + +echo "Clearing user configs & cache logs..." +for home in /root /home/*; do + cache_dir="$home/.cache/openEuler Intelligence/logs" + if [ -d "$cache_dir" ]; then + echo "Removing $cache_dir" + rm -rf "$cache_dir" + fi + config_dir="$home/.config/eulerintelli" + if [ -d "$config_dir" ]; then + echo "Removing $config_dir" + rm -rf "$config_dir" + fi +done + +echo "Removing configuration template..." +rm -f /etc/openEuler-Intelligence/smart-shell-template.json + +echo "Uninstallation complete." diff --git a/src/app/css/styles.tcss b/src/app/css/styles.tcss index f359ddbe2ae7b117e802a1104284a7368b020ee4..a9029c20b044665bdcb5d542a11a65a7725a795f 100644 --- a/src/app/css/styles.tcss +++ b/src/app/css/styles.tcss @@ -366,3 +366,117 @@ Static { .progress-line:focus { background: rgba(76, 175, 80, 0.2); } + +/* 初始化模式选择界面样式 */ +.mode-container { + background: $surface; + border: solid $primary; + padding: 2; +} + +.mode-title { + color: #4963b1; + text-style: bold; + text-align: center; + padding: 1; + margin-bottom: 1; +} + +.mode-description { + color: #888888; + text-align: center; + padding: 1; + margin-bottom: 2; +} + +.options-row { + height: auto; + width: 100%; + align: center middle; + margin: 2 0; +} + +.mode-option { + width: 1fr; + height: auto; + min-height: 11; + max-height: 12; + background: $surface; + border: solid #888888; + padding: 1; + margin: 0 2; + text-align: center; + text-wrap: wrap; + color: $text; +} + +.mode-option:hover { + border: solid #4963b1; + background: rgba(73, 99, 177, 0.1); +} + +.mode-option:focus { + border: solid #688efd; + background: rgba(104, 142, 253, 0.2); + outline: none; +} + +/* 连接现有服务界面样式 */ +.connect-container { + background: $surface; + border: solid $primary; + padding: 2; +} + +.connect-title { + color: #4963b1; + text-style: bold; + text-align: center; + padding: 1; +} + +.connect-description { + color: #888888; + text-align: center; + padding: 1; +} + +.validation-status { + color: #888888; + text-style: italic; + text-align: center; + padding: 1; +} + +.form-row { + height: 3; + margin: 1 0; + align: left middle; +} + +.form-label { + color: #4963b1; + text-style: bold; + width: 20; + content-align: left middle; + padding-right: 1; + padding-top: 1; +} + +.form-input { + width: 1fr; + margin-left: 1; +} + +.button-row { + height: 3; + align: center middle; + dock: bottom; +} + +.button-row > Button { + margin: 0 1; + width: auto; + min-height: 3; + height: 3; +} diff --git a/src/app/deployment/__init__.py b/src/app/deployment/__init__.py index 108fd84b0c5e90f24ac84522b02bf4aa00d094b8..e42d98a6a18d5d30b7eafad6f3c88863ab81b334 100644 --- a/src/app/deployment/__init__.py +++ b/src/app/deployment/__init__.py @@ -3,3 +3,9 @@ 此模块包含 openEuler Intelligence 后端部署的 TUI 界面和相关功能。 """ + +from .components import InitializationModeScreen + +__all__ = [ + "InitializationModeScreen", +] diff --git a/src/app/deployment/components/__init__.py b/src/app/deployment/components/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..5f99157986c6e27bfdbfbf570686e0d95d54e7ca --- /dev/null +++ b/src/app/deployment/components/__init__.py @@ -0,0 +1,13 @@ +""" +组件模块 + +包含与部署相关的组件。 +""" + +from .env_check import EnvironmentCheckScreen +from .modes import InitializationModeScreen + +__all__ = [ + "EnvironmentCheckScreen", + "InitializationModeScreen", +] diff --git a/src/app/deployment/components/env_check.py b/src/app/deployment/components/env_check.py new file mode 100644 index 0000000000000000000000000000000000000000..af81faa887796399c9c8eb6ae41a16455859ccf4 --- /dev/null +++ b/src/app/deployment/components/env_check.py @@ -0,0 +1,194 @@ +""" +环境检查 + +在进入部署配置之前先检查系统环境是否满足要求。 +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from textual import on +from textual.containers import Container, Horizontal +from textual.screen import ModalScreen +from textual.widgets import ( + Button, + Static, +) + +from app.deployment.service import DeploymentService +from app.deployment.ui import DeploymentConfigScreen + +if TYPE_CHECKING: + from textual.app import ComposeResult + + +class EnvironmentCheckScreen(ModalScreen[bool]): + """ + 环境检查屏幕 + + 在进入部署配置之前先检查系统环境是否满足要求。 + """ + + CSS = """ + EnvironmentCheckScreen { + align: center middle; + } + + .check-container { + width: 70%; + max-width: 80; + height: 60%; + background: $surface; + border: solid $primary; + padding: 1; + } + + .check-title { + text-style: bold; + color: $primary; + margin: 0 0 2 0; + text-align: center; + } + + .check-item { + margin: 1 0; + height: 3; + } + + .check-status { + width: 4; + text-align: center; + } + + .check-description { + width: 1fr; + margin-left: 2; + } + + .button-row { + height: 3; + margin: 2 0 0 0; + align: center middle; + dock: bottom; + } + + .continue-button, .back-button, .exit-button { + margin: 0 1; + } + """ + + def __init__(self) -> None: + """初始化环境检查屏幕""" + super().__init__() + self.service = DeploymentService() + self.check_results: dict[str, bool] = {} + self.error_messages: list[str] = [] + + def compose(self) -> ComposeResult: + """组合界面组件""" + with Container(classes="check-container"): + yield Static("环境检查", classes="check-title") + + with Horizontal(classes="check-item"): + yield Static("", id="os_status", classes="check-status") + yield Static("检查操作系统类型...", id="os_desc", classes="check-description") + + with Horizontal(classes="check-item"): + yield Static("", id="sudo_status", classes="check-status") + yield Static("检查管理员权限...", id="sudo_desc", classes="check-description") + + with Horizontal(classes="button-row"): + yield Button("继续配置", id="continue", variant="success", classes="continue-button", disabled=True) + yield Button("返回", id="back", variant="primary", classes="back-button") + yield Button("退出", id="exit", variant="error", classes="exit-button") + + async def on_mount(self) -> None: + """界面挂载时开始环境检查""" + await self._perform_environment_check() + + async def _perform_environment_check(self) -> None: + """执行环境检查""" + try: + # 检查操作系统 + await self._check_operating_system() + + # 检查 sudo 权限 + await self._check_sudo_privileges() + + # 更新界面状态 + self._update_ui_state() + + except (OSError, RuntimeError) as e: + self.notify(f"环境检查过程中发生异常: {e}", severity="error") + + async def _check_operating_system(self) -> None: + """检查操作系统类型""" + try: + is_openeuler = self.service.detect_openeuler() + self.check_results["os"] = is_openeuler + + os_status = self.query_one("#os_status", Static) + os_desc = self.query_one("#os_desc", Static) + + if is_openeuler: + os_status.update("[green]✓[/green]") + os_desc.update("操作系统: openEuler (支持)") + else: + os_status.update("[red]✗[/red]") + os_desc.update("操作系统: 非 openEuler (不支持)") + self.error_messages.append("仅支持 openEuler 操作系统") + + except (OSError, RuntimeError) as e: + self.check_results["os"] = False + self.query_one("#os_status", Static).update("[red]✗[/red]") + self.query_one("#os_desc", Static).update(f"操作系统检查失败: {e}") + self.error_messages.append(f"操作系统检查异常: {e}") + + async def _check_sudo_privileges(self) -> None: + """检查管理员权限""" + try: + has_sudo = await self.service.check_sudo_privileges() + self.check_results["sudo"] = has_sudo + + sudo_status = self.query_one("#sudo_status", Static) + sudo_desc = self.query_one("#sudo_desc", Static) + + if has_sudo: + sudo_status.update("[green]✓[/green]") + sudo_desc.update("管理员权限: 可用") + else: + sudo_status.update("[red]✗[/red]") + sudo_desc.update("管理员权限: 不可用 (需要 sudo)") + self.error_messages.append("需要管理员权限,请确保可以使用 sudo") + + except (OSError, RuntimeError) as e: + self.check_results["sudo"] = False + self.query_one("#sudo_status", Static).update("[red]✗[/red]") + self.query_one("#sudo_desc", Static).update(f"权限检查失败: {e}") + self.error_messages.append(f"权限检查异常: {e}") + + def _update_ui_state(self) -> None: + """更新界面状态""" + all_checks_passed = all(self.check_results.values()) + continue_button = self.query_one("#continue", Button) + if all_checks_passed: + continue_button.disabled = False + + @on(Button.Pressed, "#continue") + async def on_continue_button_pressed(self) -> None: + """处理继续按钮点击""" + # 推送部署配置屏幕 + await self.app.push_screen(DeploymentConfigScreen()) + # 关闭当前屏幕 + self.dismiss(result=True) + + @on(Button.Pressed, "#back") + async def on_back_button_pressed(self) -> None: + """处理返回按钮点击""" + self.dismiss(result=False) + + @on(Button.Pressed, "#exit") + def on_exit_button_pressed(self) -> None: + """处理退出按钮点击""" + self.app.exit() diff --git a/src/app/deployment/components/modes.py b/src/app/deployment/components/modes.py new file mode 100644 index 0000000000000000000000000000000000000000..860336fbac0d5cd94abc9ed1f3f1648c747f73b1 --- /dev/null +++ b/src/app/deployment/components/modes.py @@ -0,0 +1,309 @@ +""" +初始化模式选择 TUI 界面 + +提供用户选择初始化方式的界面。 +""" + +from __future__ import annotations + +import asyncio +import os +from typing import TYPE_CHECKING, Any, ClassVar + +from textual import on +from textual.binding import Binding +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 . import EnvironmentCheckScreen + +if TYPE_CHECKING: + from textual.app import ComposeResult + from textual.events import Focus + + +class ModeOptionButton(Button): + """自定义的模式选择按钮,禁用文字高亮""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """初始化自定义按钮""" + super().__init__(*args, **kwargs) + # 禁用默认的文字样式 + self.styles.text_style = "none" + + def _on_focus(self, event: Focus) -> None: + """覆盖焦点事件,禁用文字高亮""" + super()._on_focus(event) + self.styles.text_style = "none" + + +class InitializationModeScreen(ModalScreen[bool]): + """ + 初始化模式选择屏幕 + + 让用户选择连接现有服务或部署新服务。 + """ + + BINDINGS: ClassVar = [ + Binding("escape", "app.quit", "退出"), + ] + + def __init__(self) -> None: + """初始化模式选择屏幕""" + super().__init__() + + def compose(self) -> ComposeResult: + """组合界面组件""" + with Container(classes="mode-container"): + yield Static("openEuler Intelligence 初始化", classes="mode-title") + yield Static( + "请选择您的初始化方式:", + classes="mode-description", + ) + + with Horizontal(classes="options-row"): + # 连接现有服务选项 + yield ModeOptionButton( + "连接现有服务\n\n输入现有服务的 URL 和 Token 即可连接使用", + id="connect_existing", + classes="mode-option", + variant="default", + ) + + # 部署新服务选项 + yield ModeOptionButton( + "部署新服务\n\n在本机部署全新的服务环境和配置", + id="deploy_new", + classes="mode-option", + variant="default", + ) + + with Horizontal(classes="button-row"): + yield Button("退出", id="exit", variant="error", classes="exit-button") + + def on_mount(self) -> None: + """组件挂载时的处理""" + # 设置默认焦点到第一个按钮 + try: + connect_btn = self.query_one("#connect_existing", Button) + connect_btn.focus() + except (ValueError, AttributeError): + pass + + @on(Button.Pressed, "#connect_existing") + async def on_connect_existing_pressed(self) -> None: + """处理连接现有服务按钮点击""" + await self.app.push_screen(ConnectExistingServiceScreen()) + self.dismiss(result=True) + + @on(Button.Pressed, "#deploy_new") + async def on_deploy_new_pressed(self) -> None: + """处理部署新服务按钮点击""" + await self.app.push_screen(EnvironmentCheckScreen()) + self.dismiss(result=True) + + @on(Button.Pressed, "#exit") + def on_exit_button_pressed(self) -> None: + """处理退出按钮点击""" + self.app.exit() + + +class ConnectExistingServiceScreen(ModalScreen[bool]): + """ + 连接现有服务配置屏幕 + + 允许用户输入现有 openEuler Intelligence 服务的连接信息。 + """ + + BINDINGS: ClassVar = [ + Binding("escape", "back", "返回"), + Binding("ctrl+q", "app.quit", "退出"), + ] + + def __init__(self) -> None: + """初始化连接现有服务屏幕""" + super().__init__() + self.validation_task: asyncio.Task[None] | None = None + self.is_validated = False + + def compose(self) -> ComposeResult: + """组合界面组件""" + with Container(classes="connect-container"): + yield Static("连接现有 openEuler Intelligence 服务", classes="connect-title") + yield Static( + "请输入您的 openEuler Intelligence 服务连接信息:", + classes="connect-description", + ) + + with Horizontal(classes="form-row"): + yield Label("服务 URL:", classes="form-label") + yield Input( + placeholder="例如:http://your-server.com:8002", + id="service_url", + classes="form-input", + ) + + with Horizontal(classes="form-row"): + yield Label("Access Token:", classes="form-label") + yield Input( + placeholder="可选,您的访问令牌", + password=True, + id="access_token", + classes="form-input", + ) + + yield Static("未验证", id="validation_status", classes="validation-status") + + yield Static( + "提示:\n" + "• 服务 URL 通常以 http:// 或 https:// 开头\n" + "• Access Token 为可选项,如果服务无需认证可留空\n" + "• 如有 Token,可以从 openEuler Intelligence 管理界面获取\n" + "• 系统会自动验证连接并保存配置", + classes="help-text", + ) + + with Horizontal(classes="button-row"): + yield Button("连接并保存", id="connect", variant="success", disabled=True) + yield Button("返回", id="back", variant="primary") + yield Button("退出", id="exit", variant="error") + + @on(Input.Changed, "#service_url, #access_token") + async def on_field_changed(self, event: Input.Changed) -> None: + """处理输入字段变化""" + # 取消之前的验证任务 + if self.validation_task and not self.validation_task.done(): + self.validation_task.cancel() + + # 检查是否需要验证 + if self._should_validate(): + # 延迟验证,避免频繁触发 + self.validation_task = asyncio.create_task(self._delayed_validation()) + + def _should_validate(self) -> bool: + """检查是否应该进行验证""" + try: + url = self.query_one("#service_url", Input).value.strip() + # 只需要 URL 不为空即可进行验证,Token 是可选的 + return bool(url) + except (AttributeError, ValueError): + return False + + async def _delayed_validation(self) -> None: + """延迟验证""" + try: + await asyncio.sleep(1) # 等待 1 秒 + await self._validate_connection() + except asyncio.CancelledError: + pass + + async def _validate_connection(self) -> None: + """验证连接""" + status_widget = self.query_one("#validation_status", Static) + connect_button = self.query_one("#connect", Button) + + # 更新状态为验证中 + status_widget.update("[yellow]验证连接中...[/yellow]") + connect_button.disabled = True + self.is_validated = False + + try: + # 获取输入值 + url = self.query_one("#service_url", Input).value.strip() + token = self.query_one("#access_token", Input).value.strip() + + # 执行连接验证 + is_valid, message = await validate_oi_connection(url, token) + + if is_valid: + status_widget.update(f"[green]✓ {message}[/green]") + connect_button.disabled = False + self.is_validated = True + self.notify("连接验证成功", severity="information") + else: + status_widget.update(f"[red]✗ {message}[/red]") + connect_button.disabled = True + self.is_validated = False + self.notify(f"连接验证失败: {message}", severity="error") + + except (OSError, RuntimeError, ValueError) as e: + status_widget.update(f"[red]✗ 验证异常: {e}[/red]") + connect_button.disabled = True + self.is_validated = False + self.notify(f"验证过程中出现异常: {e}", severity="error") + + @on(Button.Pressed, "#connect") + async def on_connect_pressed(self) -> None: + """处理连接按钮点击""" + if not self.is_validated: + self.notify("请等待连接验证完成", severity="warning") + return + + try: + # 获取输入值 + url = self.query_one("#service_url", Input).value.strip() + token = self.query_one("#access_token", Input).value.strip() + + # 保存配置 + await self._save_configuration(url, token) + + # 显示成功信息 + self.notify("配置已保存,初始化完成!", severity="information") + self.app.exit() + + except (OSError, RuntimeError, ValueError) as e: + self.notify(f"保存配置时发生错误: {e}", severity="error") + + async def _save_configuration(self, url: str, token: str) -> None: + """保存连接配置""" + logger = get_logger(__name__) + + try: + # 确定是否为 root 用户 + is_root = os.geteuid() == 0 + + if is_root: + # root 用户:同时更新全局模板和当前配置 + logger.info("检测到 root 用户,将同时更新全局模板和当前配置") + + # 创建部署专用的配置管理器来操作全局模板 + deployment_manager = ConfigManager.create_deployment_manager() + deployment_manager.set_eulerintelli_url(url) + deployment_manager.set_eulerintelli_key(token) + deployment_manager.set_backend(Backend.EULERINTELLI) + + # 创建全局模板 + if not deployment_manager.create_global_template(): + logger.warning("创建全局配置模板失败,但会继续保存用户配置") + + # 更新当前用户配置(无论是否为 root) + config_manager = ConfigManager() + config_manager.set_eulerintelli_url(url) + config_manager.set_eulerintelli_key(token) + config_manager.set_backend(Backend.EULERINTELLI) + + logger.info("配置已保存: URL=%s", url) + + except (OSError, RuntimeError, ValueError): + logger.exception("保存配置时发生错误") + raise + + @on(Button.Pressed, "#back") + async def on_back_pressed(self) -> None: + """处理返回按钮点击""" + self.dismiss(result=False) + + @on(Button.Pressed, "#exit") + def on_exit_pressed(self) -> None: + """处理退出按钮点击""" + self.app.exit() + + async def action_back(self) -> None: + """键盘返回操作""" + self.dismiss(result=False) diff --git a/src/app/deployment/ui.py b/src/app/deployment/ui.py index 06f1ce962a89897cd0af3967dd66390207b8c280..da302ab55aeac2e318cad4acaa7d54d15f076d88 100644 --- a/src/app/deployment/ui.py +++ b/src/app/deployment/ui.py @@ -32,258 +32,6 @@ from .models import DeploymentConfig, DeploymentState, EmbeddingConfig, LLMConfi from .service import DeploymentService -class EnvironmentCheckScreen(ModalScreen[bool]): - """ - 环境检查屏幕 - - 在进入部署配置之前先检查系统环境是否满足要求。 - """ - - CSS = """ - EnvironmentCheckScreen { - align: center middle; - } - - .check-container { - width: 70%; - max-width: 80; - height: 60%; - background: $surface; - border: solid $primary; - padding: 1; - } - - .check-title { - text-style: bold; - color: $primary; - margin: 0 0 2 0; - text-align: center; - } - - .check-item { - margin: 1 0; - height: 3; - } - - .check-status { - width: 4; - text-align: center; - } - - .check-description { - width: 1fr; - margin-left: 2; - } - - .button-row { - height: 3; - margin: 2 0 0 0; - align: center middle; - } - - .continue-button, .exit-button { - margin: 0 1; - } - """ - - def __init__(self) -> None: - """初始化环境检查屏幕""" - super().__init__() - self.service = DeploymentService() - self.check_results: dict[str, bool] = {} - self.error_messages: list[str] = [] - - def compose(self) -> ComposeResult: - """组合界面组件""" - with Container(classes="check-container"): - yield Static("环境检查", classes="check-title") - - with Horizontal(classes="check-item"): - yield Static("", id="os_status", classes="check-status") - yield Static("检查操作系统类型...", id="os_desc", classes="check-description") - - with Horizontal(classes="check-item"): - yield Static("", id="sudo_status", classes="check-status") - yield Static("检查管理员权限...", id="sudo_desc", classes="check-description") - - with Horizontal(classes="button-row"): - yield Button("继续配置", id="continue", variant="success", classes="continue-button", disabled=True) - yield Button("退出", id="exit", variant="error", classes="exit-button") - - async def on_mount(self) -> None: - """界面挂载时开始环境检查""" - await self._perform_environment_check() - - async def _perform_environment_check(self) -> None: - """执行环境检查""" - try: - # 检查操作系统 - await self._check_operating_system() - - # 检查 sudo 权限 - await self._check_sudo_privileges() - - # 更新界面状态 - self._update_ui_state() - - except (OSError, RuntimeError) as e: - self.notify(f"环境检查过程中发生异常: {e}", severity="error") - - async def _check_operating_system(self) -> None: - """检查操作系统类型""" - try: - is_openeuler = self.service.detect_openeuler() - self.check_results["os"] = is_openeuler - - os_status = self.query_one("#os_status", Static) - os_desc = self.query_one("#os_desc", Static) - - if is_openeuler: - os_status.update("[green]✓[/green]") - os_desc.update("操作系统: openEuler (支持)") - else: - os_status.update("[red]✗[/red]") - os_desc.update("操作系统: 非 openEuler (不支持)") - self.error_messages.append("仅支持 openEuler 操作系统") - - except (OSError, RuntimeError) as e: - self.check_results["os"] = False - self.query_one("#os_status", Static).update("[red]✗[/red]") - self.query_one("#os_desc", Static).update(f"操作系统检查失败: {e}") - self.error_messages.append(f"操作系统检查异常: {e}") - - async def _check_sudo_privileges(self) -> None: - """检查管理员权限""" - try: - has_sudo = await self.service.check_sudo_privileges() - self.check_results["sudo"] = has_sudo - - sudo_status = self.query_one("#sudo_status", Static) - sudo_desc = self.query_one("#sudo_desc", Static) - - if has_sudo: - sudo_status.update("[green]✓[/green]") - sudo_desc.update("管理员权限: 可用") - else: - sudo_status.update("[red]✗[/red]") - sudo_desc.update("管理员权限: 不可用 (需要 sudo)") - self.error_messages.append("需要管理员权限,请确保可以使用 sudo") - - except (OSError, RuntimeError) as e: - self.check_results["sudo"] = False - self.query_one("#sudo_status", Static).update("[red]✗[/red]") - self.query_one("#sudo_desc", Static).update(f"权限检查失败: {e}") - self.error_messages.append(f"权限检查异常: {e}") - - def _update_ui_state(self) -> None: - """更新界面状态""" - all_checks_passed = all(self.check_results.values()) - continue_button = self.query_one("#continue", Button) - - if all_checks_passed: - continue_button.disabled = False - self.notify("环境检查通过,可以开始配置部署参数", severity="information") - else: - continue_button.disabled = True - error_summary = " | ".join(self.error_messages) - self.notify(f"环境检查失败: {error_summary}", severity="error") - - @on(Button.Pressed, "#continue") - async def on_continue_button_pressed(self) -> None: - """处理继续按钮点击""" - # 推送部署配置屏幕 - await self.app.push_screen(DeploymentConfigScreen()) - # 关闭当前屏幕 - self.dismiss(result=True) - - @on(Button.Pressed, "#exit") - def on_exit_button_pressed(self) -> None: - """处理退出按钮点击""" - if self.error_messages: - # 显示错误信息确认对话框 - self.app.push_screen( - EnvironmentErrorScreen("环境检查失败", self.error_messages), - ) - else: - # 直接退出 - self.app.exit() - - -class EnvironmentErrorScreen(ModalScreen[None]): - """ - 环境错误屏幕 - - 显示环境检查失败的详细信息。 - """ - - CSS = """ - EnvironmentErrorScreen { - align: center middle; - } - - .error-container { - width: 80%; - max-width: 100; - height: auto; - max-height: 80%; - background: $surface; - border: solid $error; - padding: 1; - } - - .error-title { - color: $error; - text-style: bold; - margin: 1 0; - text-align: center; - } - - .error-content { - margin: 1 0; - height: auto; - max-height: 15; - overflow-y: auto; - scrollbar-size: 1 1; - border: solid $secondary; - padding: 1; - } - """ - - def __init__(self, title: str, messages: list[str]) -> None: - """ - 初始化环境错误屏幕 - - Args: - title: 错误标题 - messages: 错误消息列表 - - """ - super().__init__() - self.title = title - self.messages = messages - - def compose(self) -> ComposeResult: - """组合界面组件""" - with Container(classes="error-container"): - yield Static(self.title or "环境检查失败", classes="error-title") - - with Container(classes="error-content"): - yield Static("环境检查发现以下问题:") - for i, message in enumerate(self.messages, 1): - yield Static(f"{i}. {message}") - - yield Static("") - yield Static("请解决上述问题后重新运行部署助手。") - - yield Button("确定退出", id="ok", variant="primary") - - @on(Button.Pressed, "#ok") - def on_ok_button_pressed(self) -> None: - """处理确定按钮点击""" - # 退出整个应用程序 - self.app.exit() - - class DeploymentConfigScreen(ModalScreen[bool]): """ 部署配置屏幕 diff --git a/src/app/deployment/validators.py b/src/app/deployment/validators.py index e82b713fa34519cac177b18721bcf86004075f05..d13f38994410d559aaffa6841363d44d67f73cda 100644 --- a/src/app/deployment/validators.py +++ b/src/app/deployment/validators.py @@ -6,12 +6,17 @@ from typing import Any +import httpx from openai import APIError, AsyncOpenAI, AuthenticationError, OpenAIError from log.manager import get_logger # 常量定义 MAX_MODEL_DISPLAY = 5 +HTTP_OK = 200 +HTTP_UNAUTHORIZED = 401 +HTTP_FORBIDDEN = 403 +HTTP_NOT_FOUND = 404 class APIValidator: @@ -26,7 +31,7 @@ class APIValidator: endpoint: str, api_key: str, model: str, - timeout: int = 30, + timeout: int = 30, # noqa: ASYNC109 ) -> tuple[bool, str, dict[str, Any]]: """ 验证 LLM 配置 @@ -93,7 +98,7 @@ class APIValidator: endpoint: str, api_key: str, model: str, - timeout: int = 30, + timeout: int = 30, # noqa: ASYNC109 ) -> tuple[bool, str, dict[str, Any]]: """ 验证 Embedding 配置 @@ -251,10 +256,88 @@ class APIValidator: choice = response.choices[0] if hasattr(choice.message, "tool_calls") and choice.message.tool_calls: tool_call = choice.message.tool_calls[0] + # 安全地访问 function 属性 + function_name = "" + function_obj = getattr(tool_call, "function", None) + if function_obj and hasattr(function_obj, "name"): + function_name = function_obj.name return True, "支持 tools 格式的 function_call", { - "function_name": tool_call.function.name, + "function_name": function_name, "supports_functions": True, "format": "tools", } return False, "不支持 function_call 功能", {"supports_functions": False} + + +async def validate_oi_connection(base_url: str, access_token: str) -> tuple[bool, str]: # noqa: PLR0911 + """ + 验证 openEuler Intelligence 服务连接 + + Args: + base_url: 服务 URL + access_token: 访问令牌(可为空) + + Returns: + tuple[bool, str]: (连接是否成功, 消息) + + """ + logger = get_logger(__name__) + + try: + # 确保 URL 格式正确 + if not base_url.startswith(("http://", "https://")): + return False, "服务 URL 必须以 http:// 或 https:// 开头" + + # 移除尾部的斜杠 + base_url = base_url.rstrip("/") + + # 构造用户信息 API URL + api_url = f"{base_url}/api/user" + + # 准备请求头 + headers = {} + if access_token and access_token.strip(): + headers["Authorization"] = f"Bearer {access_token}" + + async with httpx.AsyncClient(timeout=10) as client: + # 发送请求 + response = await client.get(api_url, headers=headers) + + # 检查 HTTP 状态码 + if response.status_code != HTTP_OK: + return _handle_http_error(response.status_code) + + # 检查响应内容 + try: + response_data = response.json() + except (ValueError, TypeError, KeyError): + return False, "服务返回的数据格式不正确" + + # 检查 code 字段 + code = response_data.get("code") + if code == HTTP_OK: + logger.info("openEuler Intelligence 服务连接成功") + return True, "连接成功" + + return False, f"服务返回错误代码: {code}" + + except httpx.ConnectError: + return False, "无法连接到服务,请检查 URL 和网络连接" + except httpx.TimeoutException: + return False, "连接超时,请检查网络连接或服务状态" + except Exception as e: + logger.exception("验证 openEuler Intelligence 连接时发生异常") + return False, f"连接验证失败: {e}" + + +def _handle_http_error(status_code: int) -> tuple[bool, str]: + """处理 HTTP 错误状态码""" + error_messages = { + HTTP_UNAUTHORIZED: "访问令牌无效或已过期", + HTTP_FORBIDDEN: "访问权限不足", + HTTP_NOT_FOUND: "API 接口不存在,请检查服务版本", + } + + message = error_messages.get(status_code, f"服务响应异常,状态码: {status_code}") + return False, message diff --git a/src/tool/oi_backend_init.py b/src/tool/oi_backend_init.py index d361473f94bd6da0d04a8dbff4c4440639a9c40d..ec7b7ba2a8da6a12868ec6cb63747b21b218ff2b 100644 --- a/src/tool/oi_backend_init.py +++ b/src/tool/oi_backend_init.py @@ -6,7 +6,7 @@ from pathlib import Path from textual.app import App -from app.deployment.ui import EnvironmentCheckScreen +from app.deployment import InitializationModeScreen from config.manager import ConfigManager from log.manager import get_logger @@ -41,8 +41,8 @@ def oi_backend_init() -> None: TITLE = "openEuler Intelligence 部署助手" def on_mount(self) -> None: - """启动时先显示环境检查界面""" - self.push_screen(EnvironmentCheckScreen()) + """启动时先显示初始化模式选择界面""" + self.push_screen(InitializationModeScreen()) app = DeploymentApp() result = app.run()