From aa05c804fb138c9696ec81ff1411534a44bc0218 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Tue, 19 Aug 2025 16:20:18 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(config):=20=E6=B7=BB=E5=8A=A0=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=96=87=E4=BB=B6=E9=AA=8C=E8=AF=81=E5=92=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/deployment/service.py | 48 +++++++- src/config/manager.py | 210 +++++++++++++++++++++++++++++++++- src/tool/oi_backend_init.py | 15 +++ 3 files changed, 268 insertions(+), 5 deletions(-) diff --git a/src/app/deployment/service.py b/src/app/deployment/service.py index 49ffe27..b621d46 100644 --- a/src/app/deployment/service.py +++ b/src/app/deployment/service.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING import httpx import toml +from config.manager import ConfigManager from log.manager import get_logger from .agent import AgentManager @@ -293,11 +294,14 @@ class DeploymentService: return False - # 部署完成 + # 部署完成,创建全局配置模板供其他用户使用 self.state.is_running = False self.state.is_completed = True self.state.add_log("✓ openEuler Intelligence 后端部署完成!") + # 创建全局配置模板,包含部署时的配置信息 + await self._create_global_config_template(config) + if progress_callback: progress_callback(self.state) @@ -945,3 +949,45 @@ class DeploymentService: self.state.add_log("✗ Agent 初始化失败") return success + + async def _create_global_config_template(self, config: DeploymentConfig) -> None: + """ + 创建全局配置模板 + + 基于当前 root 用户的实际配置创建全局配置模板,供其他用户使用 + 这样可以确保模板包含部署过程中生成的所有配置信息(如 Agent AppID 等) + 同时将部署时经过验证的大模型配置设置为默认的 OpenAI 配置 + + Args: + config: 部署配置 + + """ + try: + # 获取当前root用户的实际配置(包含 Agent 初始化后的完整配置) + current_config_manager = ConfigManager() + + # 创建专用的模板配置管理器 + template_manager = ConfigManager.create_deployment_manager() + + # 将当前root用户的完整配置复制到模板中 + template_manager.data = current_config_manager.data + + # 将部署时用户输入的经过验证的大模型信息设置为默认的 OpenAI 配置 + # 这样其他用户可以直接使用这些已验证的配置 + template_manager.set_base_url(config.llm.endpoint) + template_manager.set_model(config.llm.model) + template_manager.set_api_key(config.llm.api_key) + + # 创建全局配置模板文件 + success = template_manager.create_global_template() + + if success: + self.state.add_log("✓ 全局配置模板创建成功,包含已验证的大模型配置,其他用户可正常使用") + logger.info("全局配置模板创建成功,包含部署时的大模型配置") + else: + self.state.add_log("⚠ 全局配置模板创建失败,可能影响其他用户使用") + logger.warning("全局配置模板创建失败") + + except Exception: + logger.exception("创建全局配置模板时发生异常") + self.state.add_log("⚠ 配置模板创建异常,可能影响其他用户使用") diff --git a/src/config/manager.py b/src/config/manager.py index 2c7264d..e6ef9c0 100644 --- a/src/config/manager.py +++ b/src/config/manager.py @@ -4,6 +4,7 @@ import json from pathlib import Path from config.model import Backend, ConfigModel, LogLevel +from log.manager import get_logger class ConfigManager: @@ -11,15 +12,126 @@ class ConfigManager: 配置管理器 负责管理与持久化储存 base_url、模型及 api_key + 支持多用户环境,包括全局配置和用户私有配置 """ data = ConfigModel() - config_path = Path.home() / ".config" / "eulerintelli" / "smart-shell.json" + + # 全局配置路径(用于部署时创建的模板配置) + GLOBAL_CONFIG_DIR = Path("/etc/openEuler-Intelligence") + GLOBAL_CONFIG_PATH = GLOBAL_CONFIG_DIR / "smart-shell-template.json" + + # 用户配置目录和文件 + USER_CONFIG_DIR = Path.home() / ".config" / "eulerintelli" + USER_CONFIG_PATH = USER_CONFIG_DIR / "smart-shell.json" def __init__(self) -> None: - """初始化配置管理器""" + """ + 初始化配置管理器 + + 默认使用用户配置,部署阶段使用专用的类方法创建全局配置管理器 + """ + self.config_path = self.USER_CONFIG_PATH self._load_settings() + # 如果是普通用户且配置文件不存在,尝试从模板初始化 + if self.config_path == self.USER_CONFIG_PATH and not self.USER_CONFIG_PATH.exists(): + self.ensure_user_config_exists() + + @classmethod + def create_deployment_manager(cls) -> "ConfigManager": + """ + 创建部署专用的配置管理器 + + 用于在部署阶段创建全局配置模板 + + Returns: + ConfigManager: 使用全局配置路径的配置管理器 + + """ + manager = cls.__new__(cls) + manager.data = ConfigModel() + manager.config_path = cls.GLOBAL_CONFIG_PATH + # 由于是类方法创建的实例,直接访问私有方法 + ConfigManager._load_settings(manager) + return manager + + def ensure_user_config_exists(self) -> bool: + """ + 确保用户配置文件存在 + + 如果用户配置不存在,会尝试从全局模板复制; + 如果全局模板也不存在,则创建默认配置 + + Returns: + bool: 如果配置被创建或更新则返回 True,否则返回 False + + """ + logger = get_logger(__name__) + + # 如果用户配置已存在,直接返回 + if self.USER_CONFIG_PATH.exists(): + return False + + logger.info("用户配置文件不存在,尝试初始化配置") + + # 尝试从全局模板复制 + if self.GLOBAL_CONFIG_PATH.exists(): + try: + # 确保用户配置目录存在 + self.USER_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True) + + # 复制全局配置模板到用户目录 + with self.GLOBAL_CONFIG_PATH.open(encoding="utf-8") as global_file: + global_config = json.load(global_file) + + with self.USER_CONFIG_PATH.open("w", encoding="utf-8") as user_file: + json.dump(global_config, user_file, indent=4, ensure_ascii=False) + + except (OSError, json.JSONDecodeError) as e: + logger.warning("复制全局配置模板失败: %s,将创建默认配置", e) + else: + logger.info("已从全局模板复制配置到用户目录") + # 重新加载用户配置 + self._load_settings() + return True + + # 如果无法从模板复制,创建默认配置 + logger.info("创建默认用户配置") + self.data = ConfigModel() + self._save_settings() + return True + + def create_global_template(self) -> bool: + """ + 创建全局配置模板 + + 仅在部署阶段使用,将当前配置保存为全局模板 + + Returns: + bool: 创建成功返回 True,否则返回 False + + """ + logger = get_logger(__name__) + + try: + # 确保全局配置目录存在 + self.GLOBAL_CONFIG_DIR.mkdir(parents=True, exist_ok=True) + + # 保存当前配置为全局模板 + with self.GLOBAL_CONFIG_PATH.open("w", encoding="utf-8") as f: + json.dump(self.data.to_dict(), f, indent=4, ensure_ascii=False) + + # 设置文件权限,让所有用户都可以读取 + self.GLOBAL_CONFIG_PATH.chmod(0o644) + + except (OSError, PermissionError): + logger.exception("创建全局配置模板失败") + return False + else: + logger.info("全局配置模板已创建: %s", self.GLOBAL_CONFIG_PATH) + return True + def set_base_url(self, url: str) -> None: """更新 base_url 并保存""" self.data.openai.base_url = url @@ -92,14 +204,104 @@ class ConfigManager: self.data.eulerintelli.default_app = app_id self._save_settings() + def validate_and_update_config(self) -> bool: + """ + 检查配置文件完整性并更新缺失字段 + + 对于普通用户,会先尝试确保配置文件存在(从模板复制或创建默认) + + Returns: + bool: 如果配置被更新则返回 True,否则返回 False + + """ + logger = get_logger(__name__) + + # 如果是用户配置路径,先确保配置文件存在 + if self.config_path == self.USER_CONFIG_PATH: + config_created = self.ensure_user_config_exists() + if config_created: + return True + + # 如果配置文件仍然不存在,创建默认配置 + if not self.config_path.exists(): + logger.info("配置文件不存在,创建默认配置") + self._save_settings() + return True + + # 配置文件存在,检查完整性 + return self._validate_existing_config() + + def _validate_existing_config(self) -> bool: + """验证现有配置文件的完整性""" + logger = get_logger(__name__) + + try: + # 读取现有配置文件 + with self.config_path.open(encoding="utf-8") as f: + existing_config = json.load(f) + + except json.JSONDecodeError: + logger.exception("配置文件格式错误,将重置为默认配置") + self.data = ConfigModel() + self._save_settings() + return True + + except OSError: + logger.exception("配置文件读取失败,将重置为默认配置") + self.data = ConfigModel() + self._save_settings() + return True + + # 检查并补充缺失的字段 + return self._merge_and_update_config(existing_config) + + def _merge_and_update_config(self, existing_config: dict) -> bool: + """合并并更新配置""" + logger = get_logger(__name__) + + # 创建默认配置用于比较 + default_config = ConfigModel().to_dict() + + # 检查并补充缺失的字段 + def merge_config(existing: dict, default: dict, path: str = "") -> bool: + """递归合并配置,返回是否有更新""" + has_update = False + for key, default_value in default.items(): + current_path = f"{path}.{key}" if path else key + + if key not in existing: + existing[key] = default_value + has_update = True + logger.info("添加缺失字段: %s = %s", current_path, default_value) + elif isinstance(default_value, dict) and isinstance(existing[key], dict): + # 递归处理嵌套对象 + nested_updated = merge_config(existing[key], default_value, current_path) + has_update = has_update or nested_updated + + return has_update + + updated = merge_config(existing_config, default_config) + + if updated: + # 重新加载更新后的配置 + self.data = ConfigModel.from_dict(existing_config) + # 保存更新后的配置 + self._save_settings() + logger.info("配置文件已更新并保存") + else: + logger.info("配置文件完整,无需更新") + + return updated + def _load_settings(self) -> None: """从文件载入设置""" if self.config_path.exists(): try: with self.config_path.open(encoding="utf-8") as f: self.data = ConfigModel.from_dict(json.load(f)) - except json.JSONDecodeError: - pass + except (json.JSONDecodeError, OSError): + # 如果加载失败,使用默认配置 + self.data = ConfigModel() def _save_settings(self) -> None: """将设置保存到文件""" diff --git a/src/tool/oi_backend_init.py b/src/tool/oi_backend_init.py index e512757..d361473 100644 --- a/src/tool/oi_backend_init.py +++ b/src/tool/oi_backend_init.py @@ -7,6 +7,7 @@ from pathlib import Path from textual.app import App from app.deployment.ui import EnvironmentCheckScreen +from config.manager import ConfigManager from log.manager import get_logger @@ -15,6 +16,20 @@ def oi_backend_init() -> None: logger = get_logger(__name__) try: + # 首先检查和更新配置文件 + logger.info("检查配置文件...") + + # 在部署阶段,使用普通配置管理器操作 root 用户的配置 + # 这样 Agent 初始化时可以正常写入 AppID 等信息 + # 部署完成后会将完整配置复制为全局模板 + config_manager = ConfigManager() + config_updated = config_manager.validate_and_update_config() + + if config_updated: + logger.info("配置文件已更新") + else: + logger.info("配置文件检查完成") + # 获取项目根目录的绝对路径 project_root = Path(__file__).parent.parent css_path = str(project_root / "app" / "css" / "styles.tcss") -- Gitee From 7b289c10684253061e4f393b0a4e360cf25f76e9 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Tue, 19 Aug 2025 16:37:28 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(deploy):=20=E9=83=A8=E7=BD=B2=E5=BC=80?= =?UTF-8?q?=E5=A7=8B=E5=89=8D=E6=A3=80=E6=9F=A5=E5=B9=B6=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E6=97=A7=E7=9A=84=20framework=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/deployment/service.py | 85 ++++++++++++++++++++++++++++++++++- 1 file changed, 84 insertions(+), 1 deletion(-) diff --git a/src/app/deployment/service.py b/src/app/deployment/service.py index b621d46..54ea63d 100644 --- a/src/app/deployment/service.py +++ b/src/app/deployment/service.py @@ -371,6 +371,10 @@ class DeploymentService: progress_callback: Callable[[DeploymentState], None] | None, ) -> bool: """执行所有部署步骤""" + # 检查并停止旧的 framework 服务 + if not await self._check_and_stop_old_framework_service(config, progress_callback): + return False + # 定义基础部署步骤 steps = [ self._check_environment, @@ -982,7 +986,7 @@ class DeploymentService: success = template_manager.create_global_template() if success: - self.state.add_log("✓ 全局配置模板创建成功,包含已验证的大模型配置,其他用户可正常使用") + self.state.add_log("✓ 全局配置模板创建成功,其他用户可正常使用") logger.info("全局配置模板创建成功,包含部署时的大模型配置") else: self.state.add_log("⚠ 全局配置模板创建失败,可能影响其他用户使用") @@ -991,3 +995,82 @@ class DeploymentService: except Exception: logger.exception("创建全局配置模板时发生异常") self.state.add_log("⚠ 配置模板创建异常,可能影响其他用户使用") + + async def _check_and_stop_old_framework_service( + self, + config: DeploymentConfig, + progress_callback: Callable[[DeploymentState], None] | None, + ) -> bool: + """ + 检查并停止旧的 framework 服务 + + Args: + config: 部署配置 + progress_callback: 进度回调函数 + + Returns: + bool: 处理是否成功 + + """ + self.state.add_log("正在检查是否存在旧的 framework 服务...") + + if progress_callback: + progress_callback(self.state) + + try: + # 检查 framework 服务状态 + process = await asyncio.create_subprocess_exec( + "systemctl", + "is-active", + "framework", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + stdout, stderr = await process.communicate() + status = stdout.decode("utf-8").strip() + + if process.returncode == 0 and status == "active": + logger.info("发现正在运行的 framework 服务,正在停止...") + + if progress_callback: + progress_callback(self.state) + + # 停止 framework 服务 + stop_process = await asyncio.create_subprocess_exec( + "sudo", + "systemctl", + "stop", + "framework", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + _, stop_stderr = await stop_process.communicate() + + if stop_process.returncode == 0: + logger.info("旧的 framework 服务已停止") + else: + error_msg = stop_stderr.decode("utf-8", errors="ignore").strip() + logger.warning("⚠ 停止 framework 服务时出现警告: %s", error_msg) + # 继续部署,不因停止服务失败而中断 + + # 等待服务完全停止 + await asyncio.sleep(2.0) + + elif status in ("inactive", "failed"): + logger.info("✓ 没有发现运行中的 framework 服务") + else: + logger.warning("Framework 服务状态: %s", status) + + except (OSError, TimeoutError) as e: + # 如果系统中没有 framework 服务,systemctl 命令可能会失败 + # 这种情况下我们记录信息但不阻止部署继续进行 + logger.warning("检查 framework 服务状态时发生错误: %s", e) + return True + + except Exception: + logger.exception("处理 framework 服务时发生异常") + return False + else: + return True -- Gitee From 2d3c8ecc541e8d6e04ee5c03d112202edfa3af3c Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Tue, 19 Aug 2025 16:41:08 +0800 Subject: [PATCH 3/4] =?UTF-8?q?chore(config):=20=E9=87=8D=E7=BD=AE=20OpenA?= =?UTF-8?q?IConfig=20=E5=92=8C=20HermesConfig=20=E7=9A=84=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/config/model.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/config/model.py b/src/config/model.py index ae67373..e3f3dd4 100644 --- a/src/config/model.py +++ b/src/config/model.py @@ -32,9 +32,9 @@ class LogLevel(str, Enum): class OpenAIConfig: """OpenAI 后端配置""" - base_url: str = field(default="http://127.0.0.1:1234/v1") - model: str = field(default="qwen/qwen3-30b-a3b-2507") - api_key: str = field(default="lm-studio") + base_url: str = field(default="") + model: str = field(default="") + api_key: str = field(default="") @classmethod def from_dict(cls, d: dict) -> "OpenAIConfig": @@ -54,8 +54,8 @@ class OpenAIConfig: class HermesConfig: """Hermes 后端配置""" - base_url: str = field(default="https://www.eulerintelli.com") - api_key: str = field(default="your-eulerintelli-api-key") + base_url: str = field(default="http://127.0.0.1:8002") + api_key: str = field(default="") default_app: str = field(default="") @classmethod @@ -80,7 +80,7 @@ class HermesConfig: class ConfigModel: """配置模型""" - backend: Backend = field(default=Backend.OPENAI) + backend: Backend = field(default=Backend.EULERINTELLI) openai: OpenAIConfig = field(default_factory=OpenAIConfig) eulerintelli: HermesConfig = field(default_factory=HermesConfig) log_level: LogLevel = field(default=LogLevel.INFO) -- Gitee From 4cc34216f7e6647e7c203041dac21a61403435e0 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Tue, 19 Aug 2025 16:50:37 +0800 Subject: [PATCH 4/4] =?UTF-8?q?feat(deploy):=20=E6=9B=B4=E6=96=B0=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1=E6=A3=80=E6=9F=A5=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=90=8C=E6=97=B6=E5=81=9C=E6=AD=A2=E6=97=A7=E7=9A=84?= =?UTF-8?q?=20framework=20=E5=92=8C=20rag=20=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/deployment/service.py | 106 +++++++++++++++++----------------- 1 file changed, 54 insertions(+), 52 deletions(-) diff --git a/src/app/deployment/service.py b/src/app/deployment/service.py index 54ea63d..2668d09 100644 --- a/src/app/deployment/service.py +++ b/src/app/deployment/service.py @@ -372,7 +372,7 @@ class DeploymentService: ) -> bool: """执行所有部署步骤""" # 检查并停止旧的 framework 服务 - if not await self._check_and_stop_old_framework_service(config, progress_callback): + if not await self._check_and_stop_old_service(progress_callback): return False # 定义基础部署步骤 @@ -996,81 +996,83 @@ class DeploymentService: logger.exception("创建全局配置模板时发生异常") self.state.add_log("⚠ 配置模板创建异常,可能影响其他用户使用") - async def _check_and_stop_old_framework_service( + async def _check_and_stop_old_service( self, - config: DeploymentConfig, progress_callback: Callable[[DeploymentState], None] | None, ) -> bool: """ - 检查并停止旧的 framework 服务 + 检查并停止旧的 framework 和 rag 服务 Args: - config: 部署配置 progress_callback: 进度回调函数 Returns: bool: 处理是否成功 """ - self.state.add_log("正在检查是否存在旧的 framework 服务...") - if progress_callback: progress_callback(self.state) - try: - # 检查 framework 服务状态 - process = await asyncio.create_subprocess_exec( - "systemctl", - "is-active", - "framework", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - - stdout, stderr = await process.communicate() - status = stdout.decode("utf-8").strip() + services_to_check = ["framework", "rag"] - if process.returncode == 0 and status == "active": - logger.info("发现正在运行的 framework 服务,正在停止...") - - if progress_callback: - progress_callback(self.state) - - # 停止 framework 服务 - stop_process = await asyncio.create_subprocess_exec( - "sudo", + for service_name in services_to_check: + try: + # 检查服务状态 + process = await asyncio.create_subprocess_exec( "systemctl", - "stop", - "framework", + "is-active", + service_name, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) - _, stop_stderr = await stop_process.communicate() + stdout, stderr = await process.communicate() + status = stdout.decode("utf-8").strip() + + if process.returncode == 0 and status == "active": + logger.info("发现正在运行的 %s 服务,正在停止...", service_name) + + if progress_callback: + progress_callback(self.state) + + # 停止服务 + stop_process = await asyncio.create_subprocess_exec( + "sudo", + "systemctl", + "stop", + service_name, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + _, stop_stderr = await stop_process.communicate() - if stop_process.returncode == 0: - logger.info("旧的 framework 服务已停止") + if stop_process.returncode == 0: + logger.info("旧的 %s 服务已停止", service_name) + else: + error_msg = stop_stderr.decode("utf-8", errors="ignore").strip() + logger.warning("⚠ 停止 %s 服务时出现警告: %s", service_name, error_msg) + # 继续部署,不因停止服务失败而中断 + + # 等待服务完全停止 + await asyncio.sleep(1.0) + + elif status in ("inactive", "failed"): + logger.info("✓ 没有发现运行中的 %s 服务", service_name) else: - error_msg = stop_stderr.decode("utf-8", errors="ignore").strip() - logger.warning("⚠ 停止 framework 服务时出现警告: %s", error_msg) - # 继续部署,不因停止服务失败而中断 + logger.warning("%s 服务状态: %s", service_name.capitalize(), status) - # 等待服务完全停止 - await asyncio.sleep(2.0) + except (OSError, TimeoutError) as e: + # 如果系统中没有该服务,systemctl 命令可能会失败 + # 这种情况下我们记录信息但不阻止部署继续进行 + logger.warning("检查 %s 服务状态时发生错误: %s", service_name, e) + continue - elif status in ("inactive", "failed"): - logger.info("✓ 没有发现运行中的 framework 服务") - else: - logger.warning("Framework 服务状态: %s", status) + except Exception: + logger.exception("处理 %s 服务时发生异常", service_name) + return False - except (OSError, TimeoutError) as e: - # 如果系统中没有 framework 服务,systemctl 命令可能会失败 - # 这种情况下我们记录信息但不阻止部署继续进行 - logger.warning("检查 framework 服务状态时发生错误: %s", e) - return True + # 等待所有服务完全停止 + await asyncio.sleep(1.0) - except Exception: - logger.exception("处理 framework 服务时发生异常") - return False - else: - return True + return True -- Gitee