diff --git a/README.md b/README.md index ca286984cf2fa43e3e9c35676c77758a674214d4..9c54a0b9b47c8fcc15ac874f57336011d24589c5 100644 --- a/README.md +++ b/README.md @@ -9,61 +9,6 @@ - **流式响应**: 实时显示 AI 回复内容 - **部署助手**: 内置 openEuler Intelligence 自动部署功能 -## 项目结构 - -```text -smart-shell/ -├── README.md # 项目说明文档 -├── requirements.txt # Python 依赖包列表 -├── setup.py # 包安装配置文件 -├── LICENSE # 开源许可证 -├── distribution/ # 发布相关文件 -├── docs/ # 项目文档目录 -│ └── development/ # 开发设计文档 -│ └── server-side/ # 服务端相关文档 -├── scripts/ # 部署脚本目录 -│ └── build/ # RPM 包构建脚本 -│ └── deploy/ # 自动化部署脚本 -├── tests/ # 测试文件目录 -└── src/ # 源代码目录 - ├── main.py # 应用程序入口点 - ├── app/ # TUI 应用模块 - │ ├── tui.py # 主界面应用类 - │ ├── mcp_widgets.py # MCP 交互组件 - │ ├── tui_mcp_handler.py # MCP 事件处理器 - │ ├── settings.py # 设置界面 - │ ├── css/ - │ │ └── styles.tcss # TUI 样式文件 - │ ├── deployment/ # 部署助手模块 - │ │ ├── models.py # 部署配置模型 - │ │ ├── service.py # 部署服务逻辑 - │ │ ├── ui.py # 部署界面组件 - │ │ └── validators.py # 配置验证器 - │ └── dialogs/ # 对话框组件 - │ ├── agent.py # 智能体选择对话框 - │ └── common.py # 通用对话框组件 - ├── backend/ # 后端适配模块 - │ ├── base.py # 后端客户端基类 - │ ├── factory.py # 后端工厂类 - │ ├── mcp_handler.py # MCP 事件处理接口 - │ ├── openai.py # OpenAI 兼容客户端 - │ └── hermes/ # openEuler Intelligence 客户端 - │ ├── client.py # Hermes API 客户端 - │ ├── constants.py # 常量定义 - │ ├── exceptions.py # 异常类定义 - │ ├── models.py # 数据模型 - │ ├── stream.py # 流式响应处理 - │ └── services/ # 服务层组件 - ├── config/ # 配置管理模块 - │ ├── manager.py # 配置管理器 - │ └── model.py # 配置数据模型 - ├── log/ # 日志管理模块 - │ └── manager.py # 日志管理器 - └── tool/ # 工具模块 - ├── command_processor.py # 命令处理器 - └── oi_backend_init.py # 后端初始化工具 -``` - ## 安装说明 ### 方式一:从源码安装(推荐开发者) @@ -266,9 +211,8 @@ oi --init 1. 克隆仓库并切换到对应分支: ```sh - git clone https://gitee.com/openeuler/euler-copilot-shell.git + git clone https://gitee.com/openeuler/euler-copilot-shell.git -b dev cd euler-copilot-shell - git checkout main ``` 2. 为构建脚本添加可执行权限: @@ -280,12 +224,66 @@ oi --init 3. 运行 RPM 构建脚本: ```sh - cd scripts/build - ./build_rpm.sh + ./scripts/build/build_rpm.sh ``` 脚本执行完成后,会在临时构建目录下的 `RPMS` 和 `SRPMS` 子目录中生成相应的二进制包和源码包,并在终端输出具体路径。 +## 项目结构 + +```text +smart-shell/ +├── README.md # 项目说明文档 +├── requirements.txt # Python 依赖包列表 +├── setup.py # 包安装配置文件 +├── LICENSE # 开源许可证 +├── distribution/ # 发布相关文件 +├── docs/ # 项目文档目录 +│ └── development/ # 开发设计文档 +│ └── server-side/ # 服务端相关文档 +├── scripts/ # 部署脚本目录 +│ └── build/ # RPM 包构建脚本 +│ └── deploy/ # 自动化部署脚本 +├── tests/ # 测试文件目录 +└── src/ # 源代码目录 + ├── main.py # 应用程序入口点 + ├── app/ # TUI 应用模块 + │ ├── tui.py # 主界面应用类 + │ ├── mcp_widgets.py # MCP 交互组件 + │ ├── tui_mcp_handler.py # MCP 事件处理器 + │ ├── settings.py # 设置界面 + │ ├── css/ + │ │ └── styles.tcss # TUI 样式文件 + │ ├── deployment/ # 部署助手模块 + │ │ ├── models.py # 部署配置模型 + │ │ ├── service.py # 部署服务逻辑 + │ │ ├── ui.py # 部署界面组件 + │ │ └── validators.py # 配置验证器 + │ └── dialogs/ # 对话框组件 + │ ├── agent.py # 智能体选择对话框 + │ └── common.py # 通用对话框组件 + ├── backend/ # 后端适配模块 + │ ├── base.py # 后端客户端基类 + │ ├── factory.py # 后端工厂类 + │ ├── mcp_handler.py # MCP 事件处理接口 + │ ├── openai.py # OpenAI 兼容客户端 + │ └── hermes/ # openEuler Intelligence 客户端 + │ ├── client.py # Hermes API 客户端 + │ ├── constants.py # 常量定义 + │ ├── exceptions.py # 异常类定义 + │ ├── models.py # 数据模型 + │ ├── stream.py # 流式响应处理 + │ └── services/ # 服务层组件 + ├── config/ # 配置管理模块 + │ ├── manager.py # 配置管理器 + │ └── model.py # 配置数据模型 + ├── log/ # 日志管理模块 + │ └── manager.py # 日志管理器 + └── tool/ # 工具模块 + ├── command_processor.py # 命令处理器 + └── oi_backend_init.py # 后端初始化工具 +``` + ## 贡献 欢迎贡献代码!请随时提交 PR 或开启问题讨论任何功能增强或错误修复建议。 diff --git a/src/app/deployment/ui.py b/src/app/deployment/ui.py index f0ff7b03b3030b8e328f3e5971e4a4f982bd96d1..132cc7b5ef0b4ed635538fa5c6e90de7249916b4 100644 --- a/src/app/deployment/ui.py +++ b/src/app/deployment/ui.py @@ -51,7 +51,6 @@ class DeploymentConfigScreen(ModalScreen[bool]): height: 90%; background: $surface; border: solid $primary; - border-radius: 3; padding: 1; } @@ -59,7 +58,6 @@ class DeploymentConfigScreen(ModalScreen[bool]): margin: 1 0; padding: 1; border: solid $secondary; - border-radius: 2; } .form-row { @@ -69,7 +67,7 @@ class DeploymentConfigScreen(ModalScreen[bool]): .form-label { width: 1fr; - content-align: center left; + text-align: left; text-style: bold; } @@ -139,7 +137,7 @@ class DeploymentConfigScreen(ModalScreen[bool]): with Horizontal(classes="form-row"): yield Label("服务器 IP 地址:", classes="form-label") yield Input( - placeholder="例如:192.168.1.100", + placeholder="例如:127.0.0.1", id="server_ip", classes="form-input", ) @@ -504,7 +502,6 @@ class DeploymentProgressScreen(ModalScreen[bool]): height: 90%; background: $surface; border: solid $primary; - border-radius: 3; padding: 1; } @@ -517,7 +514,6 @@ class DeploymentProgressScreen(ModalScreen[bool]): margin: 1 0; height: 1fr; border: solid $secondary; - border-radius: 2; } .button-section { @@ -665,7 +661,6 @@ class ErrorMessageScreen(ModalScreen[None]): height: auto; background: $surface; border: solid $error; - border-radius: 3; padding: 1; } diff --git a/src/app/settings.py b/src/app/settings.py index 3e248f56e64083f188d3bc8468ced85104ff73b3..934af6739e129ff39924db82074bc60b5fdb1982 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -10,10 +10,13 @@ from textual.containers import Container, Horizontal from textual.screen import Screen from textual.widgets import Button, Input, Label, Static +from backend.hermes import HermesChatClient +from backend.openai import OpenAIClient from config import Backend, ConfigManager if TYPE_CHECKING: from textual.app import ComposeResult + from textual.events import Key from backend.base import LLMClientBase @@ -42,7 +45,11 @@ class SettingsScreen(Screen): # 后端选择 Horizontal( Label("后端:", classes="settings-label"), - Button(f"{self.backend}", id="backend-btn", classes="settings-value settings-button"), + Button( + f"{self.backend.get_display_name()}", + id="backend-btn", + classes="settings-value settings-button", + ), classes="settings-option", ), # Base URL 输入 @@ -140,7 +147,7 @@ class SettingsScreen(Screen): # 更新按钮文本 backend_btn = self.query_one("#backend-btn", Button) - backend_btn.label = new + backend_btn.label = new.get_display_name() # 更新 URL 和 API Key base_url = self.query_one("#base-url", Input) @@ -232,9 +239,9 @@ class SettingsScreen(Screen): self.config_manager.set_eulerintelli_key(api_key) # 通知主应用刷新客户端 - from app.tui import IntelligentTerminal - if isinstance(self.app, IntelligentTerminal): - self.app.refresh_llm_client() + refresh_method = getattr(self.app, "refresh_llm_client", None) + if refresh_method: + refresh_method() self.app.pop_screen() @@ -243,6 +250,12 @@ class SettingsScreen(Screen): """取消设置""" self.app.pop_screen() + def on_key(self, event: Key) -> None: + """处理键盘事件""" + if event.key == "escape": + # ESC 键退出设置页面,等效于取消 + self.app.pop_screen() + def _ensure_buttons_visible(self) -> None: """确保操作按钮始终可见""" @@ -264,16 +277,12 @@ class SettingsScreen(Screen): api_key_input = self.query_one("#api-key", Input) if self.backend == Backend.OPENAI: - from backend.openai import OpenAIClient - self.llm_client = OpenAIClient( base_url=base_url_input.value, model=self.selected_model, api_key=api_key_input.value, ) else: # EULERINTELLI - from backend.hermes.client import HermesChatClient - self.llm_client = HermesChatClient( base_url=base_url_input.value, auth_token=api_key_input.value, diff --git a/src/app/tui.py b/src/app/tui.py index 45fdbf2c6b455a5ff658721e76ffe37efa0b07fd..fcc7b12d08e69ee593e832f55e77b2db8fef322c 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -218,10 +218,10 @@ class IntelligentTerminal(App): CSS_PATH = "css/styles.tcss" BINDINGS: ClassVar[list[BindingType]] = [ + Binding(key="ctrl+q", action="request_quit", description="退出"), Binding(key="ctrl+s", action="settings", description="设置"), Binding(key="ctrl+r", action="reset_conversation", description="重置对话"), Binding(key="ctrl+t", action="choose_agent", description="选择智能体"), - Binding(key="esc", action="request_quit", description="退出"), Binding(key="tab", action="toggle_focus", description="切换焦点"), ] @@ -272,14 +272,23 @@ class IntelligentTerminal(App): def action_settings(self) -> None: """打开设置页面""" + # 只有在主界面(无其他屏幕)时才响应 + if not self._is_in_main_interface(): + return self.push_screen(SettingsScreen(self.config_manager, self.get_llm_client())) def action_request_quit(self) -> None: """请求退出应用""" + # 检查是否已经在退出对话框 + if self._is_exit_dialog_open(): + return self.push_screen(ExitDialog()) def action_reset_conversation(self) -> None: """重置对话历史记录的动作""" + # 只有在主界面(无其他屏幕)时才响应 + if not self._is_in_main_interface(): + return if self._llm_client is not None and hasattr(self._llm_client, "reset_conversation"): self._llm_client.reset_conversation() # 清除屏幕上的所有内容 @@ -290,6 +299,9 @@ class IntelligentTerminal(App): def action_choose_agent(self) -> None: """选择智能体的动作""" + # 只有在主界面(无其他屏幕)时才响应 + if not self._is_in_main_interface(): + return # 获取 Hermes 客户端 llm_client = self.get_llm_client() @@ -442,6 +454,17 @@ class IntelligentTerminal(App): self.background_tasks.add(task) task.add_done_callback(self._task_done_callback) + def _is_in_main_interface(self) -> bool: + """检查是否在主界面(没有其他屏幕弹出)""" + # 检查是否有活动的屏幕栈,除了主屏幕外没有其他屏幕 + return len(self.screen_stack) <= 1 + + def _is_exit_dialog_open(self) -> bool: + """检查是否已经打开了退出对话框""" + # 检查当前活动屏幕是否是退出对话框 + current_screen = self.screen + return hasattr(current_screen, "__class__") and current_screen.__class__.__name__ == "ExitDialog" + def _task_done_callback(self, task: asyncio.Task) -> None: """任务完成回调,从任务集合中移除""" if task in self.background_tasks: diff --git a/src/config/model.py b/src/config/model.py index 158599d3891fe7122caa02aad77f6a8a8dfe58a0..3c3b7660f6f2650915c4b2ad1e9be150a41d1fce 100644 --- a/src/config/model.py +++ b/src/config/model.py @@ -10,6 +10,14 @@ class Backend(str, Enum): OPENAI = "openai" EULERINTELLI = "eulerintelli" + def get_display_name(self) -> str: + """获取后端的可读显示名称""" + display_names = { + Backend.OPENAI: "OpenAI 大模型接口", + Backend.EULERINTELLI: "openEuler Intelligence", + } + return display_names.get(self, self.value) + class LogLevel(str, Enum): """日志级别""" diff --git a/src/tool/oi_backend_init.py b/src/tool/oi_backend_init.py index b429094cab54537e60f85fbd0c0f057b37f39613..229010bc8ece90bfc16217636db42dd994bb56f9 100644 --- a/src/tool/oi_backend_init.py +++ b/src/tool/oi_backend_init.py @@ -2,27 +2,32 @@ from __future__ import annotations +from pathlib import Path +from typing import TYPE_CHECKING + +from textual.app import App + from log.manager import get_logger +if TYPE_CHECKING: + from app.deployment.models import DeploymentConfig + +from app.deployment.ui import DeploymentConfigScreen, DeploymentProgressScreen + def oi_backend_init() -> None: """初始化后端系统 - 启动 TUI 部署助手""" logger = get_logger(__name__) 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 + # 获取项目根目录的绝对路径 + project_root = Path(__file__).parent.parent + css_path = str(project_root / "app" / "css" / "styles.tcss") class DeploymentApp(App): """部署 TUI 应用""" - CSS_PATH = "app/css/styles.tcss" + CSS_PATH = css_path TITLE = "openEuler Intelligence 部署助手" def __init__(self) -> None: