diff --git a/README.md b/README.md index 3807a26d8ee1b996371c2470c70894b51a3cc4b0..5effcb969078733a195ceff257022b93590e2011 100644 --- a/README.md +++ b/README.md @@ -73,8 +73,31 @@ python src/main.py python src/main.py --logs ``` +初始化 openEuler Intelligence 后端(仅支持 openEuler 操作系统): + +```sh +python src/main.py --init +``` + 应用启动后,您可以直接在输入框中输入命令。如果命令无效或无法执行,应用程序将基于您的输入提供智能建议。 +### --init 命令详细说明 + +`--init` 命令用于在 openEuler 操作系统上自动安装和配置 openEuler Intelligence 后端,它将执行以下步骤: + +1. **系统检测**: 检测当前操作系统是否为 openEuler +2. **环境检查**: 验证 dnf 包管理器和管理员权限 +3. **包安装**: 通过 dnf 安装 `openeuler-intelligence-installer` RPM 包 +4. **服务部署**: 运行部署脚本完成系统初始化 + +**使用要求**: + +- 仅支持 openEuler 操作系统 +- 需要管理员权限(sudo) +- 需要网络连接以下载 RPM 包 + +**注意**: 此命令会自动安装系统服务,请在生产环境使用前仔细评估。 + ## 大型语言模型集成 该应用程序利用大型语言模型(LLM)增强用户体验,提供智能命令建议。集成在 `backend/openai.py` 文件中处理,该文件与 OpenAI 兼容的 API 通信以获取基于用户输入的建议。应用支持配置不同的后端和模型。 diff --git a/distribution/linux/euler-copilot-shell.spec b/distribution/linux/euler-copilot-shell.spec index 6ea49bb35f15c2b197f9f26a670e8f68fbaf6414..608f1ccefa48b0af37a6d477fb9d7ea3c6e07227 100644 --- a/distribution/linux/euler-copilot-shell.spec +++ b/distribution/linux/euler-copilot-shell.spec @@ -3,7 +3,7 @@ Name: euler-copilot-shell Version: 0.10.0 Release: 1%{?dist} -Summary: 智能 Shell 命令行工具 +Summary: openEuler Intelligence 智能命令行工具集 License: MulanPSL-2.0 URL: https://gitee.com/openeuler/euler-copilot-shell Source0: %{name}-%{version}.tar.gz @@ -15,13 +15,29 @@ BuildRequires: python3-devel BuildRequires: python3-virtualenv BuildRequires: python3-pip -# 运行时不再需要Python依赖,因为已经被打包到可执行文件中 +%description +openEuler Intelligence 智能命令行工具集,包含智能 Shell 命令行程序和部署安装工具。 + +# 智能命令行工具子包 +%package -n openeuler-intelligence-cli +Summary: openEuler Intelligence 智能 Shell 命令行工具 Requires: glibc +# 替换原来的 euler-copilot-shell 包 +Obsoletes: euler-copilot-shell < %{version}-%{release} +Provides: euler-copilot-shell = %{version}-%{release} -%description +%description -n openeuler-intelligence-cli openEuler Intelligence 智能 Shell 是一个智能命令行程序。 它允许用户输入命令,通过集成大语言模型提供命令建议,帮助用户更高效地使用命令行。 +# 部署安装工具子包 +%package -n openeuler-intelligence-installer +Summary: openEuler Intelligence 部署安装脚本 +BuildArch: noarch + +%description -n openeuler-intelligence-installer +openEuler Intelligence 部署安装工具包,包含部署脚本和相关资源文件。 + %prep %autosetup -n %{name}-%{version} @@ -50,17 +66,35 @@ pyinstaller --noconfirm --onefile \ deactivate %install -# 创建目标目录 +# 安装智能命令行工具 mkdir -p %{buildroot}%{_bindir} - -# 复制PyInstaller生成的可执行文件 install -m 0755 dist/%{pypi_name} %{buildroot}%{_bindir}/%{pypi_name} -%files +# 安装部署脚本和资源 +mkdir -p %{buildroot}/usr/lib/openeuler-intelligence/{scripts,resources} +mkdir -p %{buildroot}%{_bindir} + +# 复制部署脚本和资源 +install -m 755 scripts/deploy/deploy.sh %{buildroot}/usr/lib/openeuler-intelligence/scripts/deploy +cp -r scripts/deploy/0-one-click-deploy scripts/deploy/1-check-env scripts/deploy/2-install-dependency scripts/deploy/3-install-server scripts/deploy/4-other-script scripts/deploy/5-resource %{buildroot}/usr/lib/openeuler-intelligence/scripts/ +chmod -R +x %{buildroot}/usr/lib/openeuler-intelligence/scripts/ + +# 创建可执行文件的符号链接 +ln -sf /usr/lib/openeuler-intelligence/scripts/deploy %{buildroot}%{_bindir}/openeuler-intelligence-installer + +%files -n openeuler-intelligence-cli %license LICENSE %doc README.md %{_bindir}/%{pypi_name} +%files -n openeuler-intelligence-installer +%license LICENSE +%doc scripts/deploy/安装部署手册.md +/usr/lib/openeuler-intelligence +%{_bindir}/openeuler-intelligence-installer + %changelog * %{_builddate} openEuler - 0.10.0-1 -- 首次构建RPM包,支持x86_64和aarch64架构 \ No newline at end of file +- 重构为子包形式:openeuler-intelligence-cli 和 openeuler-intelligence-installer +- openeuler-intelligence-cli 替换原 euler-copilot-shell 包 +- 新增 openeuler-intelligence-installer 子包,包含部署安装脚本 \ No newline at end of file diff --git a/src/app/deployment/__init__.py b/src/app/deployment/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..108fd84b0c5e90f24ac84522b02bf4aa00d094b8 --- /dev/null +++ b/src/app/deployment/__init__.py @@ -0,0 +1,5 @@ +""" +部署相关模块 + +此模块包含 openEuler Intelligence 后端部署的 TUI 界面和相关功能。 +""" diff --git a/src/app/deployment/models.py b/src/app/deployment/models.py new file mode 100644 index 0000000000000000000000000000000000000000..0edcc2bbdd26968b7d86186ae1ab4eaf2a6fbdef --- /dev/null +++ b/src/app/deployment/models.py @@ -0,0 +1,170 @@ +""" +部署配置数据模型 + +定义部署过程中需要的配置项数据结构。 +""" + +from dataclasses import dataclass, field + +# 常量定义 +MAX_TEMPERATURE = 2.0 +MIN_TEMPERATURE = 0.0 + + +@dataclass +class LLMConfig: + """ + LLM 配置 + + 包含大语言模型的配置信息。 + """ + + endpoint: str = "" + api_key: str = "" + model: str = "" + max_tokens: int = 8192 + temperature: float = 0.7 + request_timeout: int = 300 + + +@dataclass +class EmbeddingConfig: + """ + Embedding 配置 + + 包含嵌入模型的配置信息。 + """ + + type: str = "openai" + endpoint: str = "" + api_key: str = "" + model: str = "" + + +@dataclass +class DeploymentConfig: + """ + 部署配置 + + 包含完整的部署配置信息。 + """ + + # 基础设置 + server_ip: str = "" + deployment_mode: str = "light" # light: 轻量部署, full: 全量部署 + + # LLM 配置 + llm: LLMConfig = field(default_factory=LLMConfig) + + # Embedding 配置 + embedding: EmbeddingConfig = field(default_factory=EmbeddingConfig) + + # 高级配置(可选) + enable_web: bool = False + enable_rag: bool = False + + def validate(self) -> tuple[bool, list[str]]: + """ + 验证配置的有效性 + + Returns: + tuple[bool, list[str]]: (是否有效, 错误消息列表) + + """ + errors = [] + + # 验证基础字段 + errors.extend(self._validate_basic_fields()) + + # 验证 LLM 字段 + errors.extend(self._validate_llm_fields()) + + # 验证 Embedding 字段 + errors.extend(self._validate_embedding_fields()) + + # 验证数值范围 + errors.extend(self._validate_numeric_fields()) + + return len(errors) == 0, errors + + def _validate_basic_fields(self) -> list[str]: + """验证基础字段""" + errors = [] + if not self.server_ip.strip(): + errors.append("服务器 IP 地址不能为空") + return errors + + def _validate_llm_fields(self) -> list[str]: + """验证 LLM 配置字段""" + errors = [] + if not self.llm.endpoint.strip(): + errors.append("LLM API 端点不能为空") + if not self.llm.api_key.strip(): + errors.append("LLM API 密钥不能为空") + if not self.llm.model.strip(): + errors.append("LLM 模型名称不能为空") + return errors + + def _validate_embedding_fields(self) -> list[str]: + """验证 Embedding 配置字段""" + errors = [] + if not self.embedding.endpoint.strip(): + errors.append("Embedding API 端点不能为空") + if not self.embedding.api_key.strip(): + errors.append("Embedding API 密钥不能为空") + if not self.embedding.model.strip(): + errors.append("Embedding 模型名称不能为空") + return errors + + def _validate_numeric_fields(self) -> list[str]: + """验证数值字段""" + errors = [] + if self.llm.max_tokens <= 0: + errors.append("LLM max_tokens 必须大于 0") + if not (MIN_TEMPERATURE <= self.llm.temperature <= MAX_TEMPERATURE): + errors.append(f"LLM temperature 必须在 {MIN_TEMPERATURE} 到 {MAX_TEMPERATURE} 之间") + if self.llm.request_timeout <= 0: + errors.append("LLM 请求超时时间必须大于 0") + return errors + + +@dataclass +class DeploymentState: + """ + 部署状态 + + 跟踪部署过程的状态信息。 + """ + + current_step: int = 0 + total_steps: int = 0 + current_step_name: str = "" + is_running: bool = False + is_completed: bool = False + is_failed: bool = False + error_message: str = "" + output_log: list[str] = field(default_factory=list) + + def add_log(self, message: str) -> None: + """ + 添加日志消息 + + Args: + message: 日志消息 + + """ + self.output_log.append(message) + + def clear_log(self) -> None: + """清空日志""" + self.output_log.clear() + + def reset(self) -> None: + """重置状态""" + self.current_step = 0 + self.current_step_name = "" + self.is_running = False + self.is_completed = False + self.is_failed = False + self.error_message = "" + self.clear_log() diff --git a/src/app/deployment/service.py b/src/app/deployment/service.py new file mode 100644 index 0000000000000000000000000000000000000000..997b922d56e971d7e7da648b94852b160ebefe24 --- /dev/null +++ b/src/app/deployment/service.py @@ -0,0 +1,482 @@ +""" +部署服务模块 + +处理 openEuler Intelligence 后端部署的核心逻辑。 +""" + +from __future__ import annotations + +import asyncio +import os +import platform +import re +from pathlib import Path +from typing import TYPE_CHECKING + +from log.manager import get_logger + +from .models import DeploymentConfig, DeploymentState + +if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Callable + +logger = get_logger(__name__) + + +class DeploymentResourceManager: + """部署资源管理器,管理 RPM 包安装的资源文件""" + + # RPM 包安装的资源文件路径 + INSTALLER_BASE_PATH = Path("/usr/lib/openeuler-intelligence/scripts") + RESOURCE_PATH = INSTALLER_BASE_PATH / "5-resource" + DEPLOY_SCRIPT = INSTALLER_BASE_PATH / "deploy" + + # 配置文件模板路径 + ENV_TEMPLATE = RESOURCE_PATH / "env" + CONFIG_TEMPLATE = RESOURCE_PATH / "config.toml" + + # 系统配置文件路径 + INSTALL_MODEL_FILE = Path("/etc/euler_Intelligence_install_model") + + @classmethod + def check_installer_available(cls) -> bool: + """检查安装器是否可用""" + return ( + cls.INSTALLER_BASE_PATH.exists() + and cls.RESOURCE_PATH.exists() + and cls.DEPLOY_SCRIPT.exists() + and cls.ENV_TEMPLATE.exists() + and cls.CONFIG_TEMPLATE.exists() + ) + + @classmethod + def get_template_content(cls, template_path: Path) -> str: + """获取模板文件内容""" + try: + return template_path.read_text(encoding="utf-8") + except OSError as e: + logger.exception("读取模板文件失败 %s", template_path) + msg = f"无法读取模板文件: {template_path}" + raise RuntimeError(msg) from e + + @classmethod + def update_config_values(cls, content: str, config: DeploymentConfig) -> str: + """根据用户配置更新配置文件内容""" + # 更新 LLM 配置 + content = re.sub(r"(MODEL_NAME\s*=\s*).*", rf"\1{config.llm.model}", content) + content = re.sub(r"(OPENAI_API_BASE\s*=\s*).*", rf"\1{config.llm.endpoint}", content) + content = re.sub(r"(OPENAI_API_KEY\s*=\s*).*", rf"\1{config.llm.api_key}", content) + content = re.sub(r"(MAX_TOKENS\s*=\s*).*", rf"\1{config.llm.max_tokens}", content) + content = re.sub(r"(TEMPERATURE\s*=\s*).*", rf"\1{config.llm.temperature}", content) + content = re.sub(r"(REQUEST_TIMEOUT\s*=\s*).*", rf"\1{config.llm.request_timeout}", content) + + # 更新 Embedding 配置 + content = re.sub(r"(EMBEDDING_TYPE\s*=\s*).*", rf"\1{config.embedding.type}", content) + content = re.sub(r"(EMBEDDING_API_KEY\s*=\s*).*", rf"\1{config.embedding.api_key}", content) + content = re.sub(r"(EMBEDDING_ENDPOINT\s*=\s*).*", rf"\1{config.embedding.endpoint}", content) + return re.sub(r"(EMBEDDING_MODEL_NAME\s*=\s*).*", rf"\1{config.embedding.model}", content) + + @classmethod + def update_toml_values(cls, content: str, config: DeploymentConfig) -> str: + """更新 TOML 配置文件的值""" + import re + + # 更新服务器 IP + content = re.sub(r"(host\s*=\s*')[^']*(')", rf"\1http://{config.server_ip}:8000\2", content) + content = re.sub(r"(login_api\s*=\s*')[^']*(')", rf"\1http://{config.server_ip}:8080/api/auth/login\2", content) + content = re.sub(r"(domain\s*=\s*')[^']*(')", rf"\1{config.server_ip}\2", content) + + # 更新 LLM 配置 + content = re.sub(r'(endpoint\s*=\s*")[^"]*(")', rf"\1{config.llm.endpoint}\2", content) + content = re.sub(r"(key\s*=\s*')[^']*(')", rf"\1{config.llm.api_key}\2", content) + content = re.sub(r"(model\s*=\s*')[^']*(')", rf"\1{config.llm.model}\2", content) + content = re.sub(r"(max_tokens\s*=\s*)\d+", rf"\1{config.llm.max_tokens}", content) + content = re.sub(r"(temperature\s*=\s*)[\d.]+", rf"\1{config.llm.temperature}", content) + + # 更新 Embedding 配置 + content = re.sub(r"(type\s*=\s*')[^']*(')", rf"\1{config.embedding.type}\2", content) + return re.sub(r"(api_key\s*=\s*')[^']*(')", rf"\1{config.embedding.api_key}\2", content) + + @classmethod + def create_deploy_mode_content(cls, config: DeploymentConfig) -> str: + """创建部署模式配置内容""" + web_install = "y" if config.enable_web else "n" + rag_install = "y" if config.enable_rag else "n" + + return f"""web_install={web_install} +rag_install={rag_install} +""" + + +class DeploymentService: + """ + 部署服务 + + 负责执行 openEuler Intelligence 后端的部署流程。 + 基于已安装的 openeuler-intelligence-installer RPM 包资源。 + """ + + def __init__(self) -> None: + """初始化部署服务""" + self.state = DeploymentState() + self._process: asyncio.subprocess.Process | None = None + self.resource_manager = DeploymentResourceManager() + + async def deploy( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None = None, + ) -> bool: + """ + 执行部署 + + Args: + config: 部署配置 + progress_callback: 进度回调函数 + + Returns: + bool: 部署是否成功 + + """ + try: + logger.info("开始部署 openEuler Intelligence 后端") + + # 重置状态 + self.state.reset() + self.state.is_running = True + self.state.total_steps = 4 + + # 步骤1:检查系统环境和资源 + if not await self._check_environment(config, progress_callback): + return False + + # 步骤2:生成配置文件 + if not await self._generate_config_files(config, progress_callback): + return False + + # 步骤3:设置部署模式 + if not await self._setup_deploy_mode(config, progress_callback): + return False + + # 步骤4:执行部署脚本 + if not await self._run_deployment_script(config, progress_callback): + return False + + except Exception: + logger.exception("部署过程中发生错误") + self.state.is_running = False + self.state.is_failed = True + self.state.error_message = "部署过程中发生异常" + self.state.add_log("✗ 部署失败") + + if progress_callback: + progress_callback(self.state) + + return False + else: + # 部署完成 + self.state.is_running = False + self.state.is_completed = True + self.state.add_log("✓ openEuler Intelligence 后端部署完成!") + + if progress_callback: + progress_callback(self.state) + + logger.info("部署完成") + return True + + async def _check_environment( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """检查系统环境和资源""" + self.state.current_step = 1 + self.state.current_step_name = "检查系统环境" + self.state.add_log("正在检查系统环境...") + + if progress_callback: + progress_callback(self.state) + + # 检查操作系统 + if not self._detect_openeuler(): + self.state.add_log("✗ 错误: 仅支持 openEuler 操作系统") + return False + self.state.add_log("✓ 检测到 openEuler 操作系统") + + # 检查安装器资源 + if not self.resource_manager.check_installer_available(): + self.state.add_log("✗ 错误: openeuler-intelligence-installer 包未安装或资源缺失") + self.state.add_log("请先安装: sudo dnf install -y openeuler-intelligence-installer") + return False + self.state.add_log("✓ openeuler-intelligence-installer 资源可用") + + # 检查权限 + if not await self._check_sudo_privileges(): + self.state.add_log("✗ 错误: 需要管理员权限") + return False + self.state.add_log("✓ 具有管理员权限") + + return True + + async def _generate_config_files( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """生成配置文件""" + self.state.current_step = 2 + self.state.current_step_name = "更新配置文件" + self.state.add_log("正在更新配置文件...") + + if progress_callback: + progress_callback(self.state) + + try: + # 更新 env 文件 + await self._update_env_file(config) + self.state.add_log("✓ 更新 env 配置文件") + + # 更新 config.toml 文件 + await self._update_config_toml(config) + self.state.add_log("✓ 更新 config.toml 配置文件") + + except Exception as e: + self.state.add_log(f"✗ 更新配置文件失败: {e}") + logger.exception("更新配置文件失败") + return False + else: + return True + + async def _setup_deploy_mode( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """设置部署模式""" + self.state.current_step = 3 + self.state.current_step_name = "设置部署模式" + self.state.add_log("正在设置部署模式...") + + if progress_callback: + progress_callback(self.state) + + try: + # 生成部署模式文件内容 + mode_content = self.resource_manager.create_deploy_mode_content(config) + + # 写入系统配置文件 + cmd = [ + "sudo", + "tee", + str(self.resource_manager.INSTALL_MODEL_FILE), + ] + + process = await asyncio.create_subprocess_exec( + *cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await process.communicate(mode_content.encode()) + + if process.returncode != 0: + error_msg = stderr.decode("utf-8", errors="ignore").strip() + self.state.add_log(f"✗ 设置部署模式失败: {error_msg}") + return False + + web_status = "启用" if config.enable_web else "禁用" + rag_status = "启用" if config.enable_rag else "禁用" + self.state.add_log(f"✓ 部署模式设置完成 (Web界面: {web_status}, RAG: {rag_status})") + + except Exception as e: + self.state.add_log(f"✗ 设置部署模式失败: {e}") + logger.exception("设置部署模式失败") + return False + else: + return True + + async def _run_deployment_script( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """运行部署脚本""" + self.state.current_step = 4 + self.state.current_step_name = "执行部署脚本" + self.state.add_log("正在运行部署脚本...") + + if progress_callback: + progress_callback(self.state) + + try: + # 运行部署脚本 + cmd = [ + "sudo", + str(self.resource_manager.DEPLOY_SCRIPT), + config.deployment_mode, + ] + + # 设置环境变量,启用自动部署模式 + env = os.environ.copy() + env["AUTO_DEPLOY"] = "1" + + self._process = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + env=env, + ) + + # 读取输出 + async for line in self._read_process_output(): + self.state.add_log(line) + if progress_callback: + progress_callback(self.state) + + # 等待进程结束 + return_code = await self._process.wait() + self._process = None + + if return_code == 0: + self.state.add_log("✓ 部署脚本执行成功") + return True + + except Exception as e: + self.state.add_log(f"✗ 运行部署脚本时发生错误: {e}") + logger.exception("运行部署脚本失败") + return False + else: + self.state.add_log(f"✗ 部署脚本执行失败,返回码: {return_code}") + return False + + async def _update_env_file(self, config: DeploymentConfig) -> None: + """更新 env 配置文件""" + template_content = self.resource_manager.get_template_content( + self.resource_manager.ENV_TEMPLATE, + ) + + updated_content = self.resource_manager.update_config_values( + template_content, + config, + ) + + # 备份原文件并写入新内容 + backup_cmd = [ + "sudo", + "cp", + str(self.resource_manager.ENV_TEMPLATE), + f"{self.resource_manager.ENV_TEMPLATE}.backup", + ] + await asyncio.create_subprocess_exec(*backup_cmd) + + # 写入更新后的内容 + write_cmd = ["sudo", "tee", str(self.resource_manager.ENV_TEMPLATE)] + process = await asyncio.create_subprocess_exec( + *write_cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + await process.communicate(updated_content.encode()) + + async def _update_config_toml(self, config: DeploymentConfig) -> None: + """更新 config.toml 配置文件""" + template_content = self.resource_manager.get_template_content( + self.resource_manager.CONFIG_TEMPLATE, + ) + + updated_content = self.resource_manager.update_toml_values( + template_content, + config, + ) + + # 备份原文件并写入新内容 + backup_cmd = [ + "sudo", + "cp", + str(self.resource_manager.CONFIG_TEMPLATE), + f"{self.resource_manager.CONFIG_TEMPLATE}.backup", + ] + await asyncio.create_subprocess_exec(*backup_cmd) + + # 写入更新后的内容 + write_cmd = ["sudo", "tee", str(self.resource_manager.CONFIG_TEMPLATE)] + process = await asyncio.create_subprocess_exec( + *write_cmd, + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + await process.communicate(updated_content.encode()) + + async def _read_process_output(self) -> AsyncGenerator[str, None]: + """读取进程输出""" + if not self._process or not self._process.stdout: + return + + while True: + try: + line = await self._process.stdout.readline() + if not line: + break + + decoded_line = line.decode("utf-8", errors="ignore").strip() + if decoded_line: + yield decoded_line + + except OSError as e: + logger.warning("读取进程输出时发生错误: %s", e) + break + + def _detect_openeuler(self) -> bool: + """检测是否为 openEuler 系统""" + try: + # 检查 /etc/os-release + os_release_path = Path("/etc/os-release") + if os_release_path.exists(): + content = os_release_path.read_text(encoding="utf-8").lower() + if "openeuler" in content: + return True + + # 检查 /etc/openEuler-release + openeuler_release_path = Path("/etc/openEuler-release") + if openeuler_release_path.exists(): + return True + + except OSError as e: + logger.warning("检测操作系统时发生错误: %s", e) + return False + + else: + # 检查 platform 信息 + system_info = platform.platform().lower() + return "openeuler" in system_info + + async def _check_sudo_privileges(self) -> bool: + """检查 sudo 权限""" + try: + process = await asyncio.create_subprocess_exec( + "sudo", + "-n", + "true", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + return_code = await process.wait() + except OSError: + return False + else: + return return_code == 0 + + def cancel_deployment(self) -> None: + """取消部署""" + if self._process: + try: + self._process.terminate() + logger.info("部署进程已终止") + except OSError as e: + logger.warning("终止部署进程时发生错误: %s", e) diff --git a/src/main.py b/src/main.py index 4b3c9166899894cf019ca286acb192f48ac72101..fe247f0abdc5c6686bc5a97a82e15a0fbfc4ea9c 100644 --- a/src/main.py +++ b/src/main.py @@ -13,6 +13,7 @@ from log.manager import ( get_logger, setup_logging, ) +from tool.oi_backend_init import oi_backend_init def parse_args() -> argparse.Namespace: @@ -23,6 +24,11 @@ def parse_args() -> argparse.Namespace: action="store_true", help="显示最新的日志内容(最多1000行)", ) + parser.add_argument( + "--init", + action="store_true", + help="初始化 openEuler Intelligence 后端(仅支持 openEuler 操作系统)", + ) return parser.parse_args() @@ -51,6 +57,10 @@ def main() -> None: show_logs() return + if args.init: + oi_backend_init() + return + # 初始化日志系统 setup_logging() # 在 TUI 模式下禁用控制台日志输出,避免干扰界面 diff --git a/src/tool/__init__.py b/src/tool/__init__.py index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f54433633e9f605de28cf4c110013377f29c48b1 100644 --- a/src/tool/__init__.py +++ b/src/tool/__init__.py @@ -0,0 +1,6 @@ +"""工具模块""" + +from .command_processor import execute_command, is_command_safe, process_command +from .oi_backend_init import oi_backend_init + +__all__ = ["execute_command", "is_command_safe", "oi_backend_init", "process_command"] diff --git a/src/tool/oi_backend_init.py b/src/tool/oi_backend_init.py new file mode 100644 index 0000000000000000000000000000000000000000..7148f21743902da5c542973d1f11c6c319bb87b5 --- /dev/null +++ b/src/tool/oi_backend_init.py @@ -0,0 +1,55 @@ +"""系统初始化工具模块""" + +from __future__ import annotations + +from log.manager import get_logger + +logger = get_logger(__name__) + + +def oi_backend_init() -> None: + """初始化后端系统 - 启动 TUI 部署助手""" + try: + from typing import TYPE_CHECKING + + from textual.app import App + + if TYPE_CHECKING: + from app.deployment.models import DeploymentConfig + + from app.deployment.ui import DeploymentConfigScreen, DeploymentProgressScreen + + class DeploymentApp(App): + """部署 TUI 应用""" + + CSS_PATH = "app/css/styles.tcss" + TITLE = "openEuler Intelligence 部署助手" + + def __init__(self) -> None: + super().__init__() + self.deployment_config: DeploymentConfig | None = None + + def on_mount(self) -> None: + """启动时显示配置界面""" + self.push_screen(DeploymentConfigScreen()) + + def on_deployment_config_submitted(self, config: DeploymentConfig) -> None: + """配置提交后开始部署""" + self.deployment_config = config + if self.deployment_config is not None: + self.push_screen( + DeploymentProgressScreen(self.deployment_config), + ) + + app = DeploymentApp() + result = app.run() + logger.info("部署结果: %s", result) + except KeyboardInterrupt: + logger.warning("用户中断部署") + except ImportError: + logger.exception("导入模块失败") + except RuntimeError: + logger.exception("部署过程中发生错误") + except Exception: + logger.exception("未预期的错误") + raise