From b6a4b43fd65ec90e7c1205613ceba4c182878556 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Thu, 23 Oct 2025 17:22:13 +0800 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20=E5=88=9D=E6=AD=A5=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=94=A8=E6=88=B7=E8=AE=BE=E7=BD=AE=E7=95=8C=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/css/styles.tcss | 53 ++++- src/app/dialogs/__init__.py | 4 +- src/app/dialogs/{llm.py => user.py} | 292 +++++++++++++++++----------- src/app/settings.py | 126 +----------- src/config/manager.py | 17 +- src/config/model.py | 29 +-- 6 files changed, 243 insertions(+), 278 deletions(-) rename src/app/dialogs/{llm.py => user.py} (48%) diff --git a/src/app/css/styles.tcss b/src/app/css/styles.tcss index 572a777..65014a7 100644 --- a/src/app/css/styles.tcss +++ b/src/app/css/styles.tcss @@ -486,18 +486,18 @@ BackendRequiredDialog { height: 3; } -/* LLM 配置对话框样式 */ -LLMConfigDialog { +/* 用户配置对话框样式 */ +UserConfigDialog { align: center middle; } -#llm-dialog-screen { +#user-dialog-screen { align: center middle; width: 70%; height: 80%; } -#llm-dialog { +#user-dialog { background: $surface; border: solid #4963b1; padding-left: 1; @@ -508,11 +508,52 @@ LLMConfigDialog { } /* 标签页容器 */ -#llm-tabs { +#user-tabs { height: 1fr; margin: 1 0; } +/* 常规设置表单样式 */ +.general-settings-form { + padding: 2; + width: 100%; + height: auto; +} + +.form-label { + color: #888888; + padding: 1 0; + width: auto; +} + +.form-input { + width: 100%; + margin-bottom: 2; +} + +.mcp-toggle-container { + height: 3; + margin-bottom: 2; +} + +.form-button { + width: auto; + min-width: 20; +} + +.form-buttons { + height: 3; + align: center middle; + margin-top: 2; +} + +.form-buttons > Button { + margin: 0 1; + width: auto; + min-height: 3; + height: 3; +} + /* 模型列表容器 */ .llm-model-list { height: 1fr; @@ -568,7 +609,7 @@ LLMConfigDialog { } /* 帮助文本 */ -#llm-dialog-help { +#user-dialog-help { text-align: center; color: #888888; text-style: italic; diff --git a/src/app/dialogs/__init__.py b/src/app/dialogs/__init__.py index 287816b..eb86d08 100644 --- a/src/app/dialogs/__init__.py +++ b/src/app/dialogs/__init__.py @@ -2,6 +2,6 @@ from .agent import AgentSelectionDialog, BackendRequiredDialog from .common import ExitDialog -from .llm import LLMConfigDialog +from .user import UserConfigDialog -__all__ = ["AgentSelectionDialog", "BackendRequiredDialog", "ExitDialog", "LLMConfigDialog"] +__all__ = ["AgentSelectionDialog", "BackendRequiredDialog", "ExitDialog", "UserConfigDialog"] diff --git a/src/app/dialogs/llm.py b/src/app/dialogs/user.py similarity index 48% rename from src/app/dialogs/llm.py rename to src/app/dialogs/user.py index b32b1fb..340217d 100644 --- a/src/app/dialogs/llm.py +++ b/src/app/dialogs/user.py @@ -1,4 +1,4 @@ -"""LLM 模型配置对话框""" +"""用户配置对话框""" from __future__ import annotations @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, ClassVar from textual import on from textual.containers import Container, Horizontal from textual.screen import ModalScreen -from textual.widgets import Label, Static, TabbedContent, TabPane +from textual.widgets import Button, Input, Label, Static, TabbedContent, TabPane from backend.models import LLMType, ModelInfo from i18n.manager import _ @@ -19,16 +19,13 @@ if TYPE_CHECKING: from config.manager import ConfigManager -class LLMConfigDialog(ModalScreen): - """LLM 模型配置对话框""" +class UserConfigDialog(ModalScreen): + """用户配置对话框""" BINDINGS: ClassVar[list[tuple[str, str, str]]] = [ - ("left", "previous_tab", _("上一个标签")), - ("right", "next_tab", _("下一个标签")), ("up", "previous_model", _("上一个模型")), ("down", "next_model", _("下一个模型")), ("space", "select_model", _("选择模型")), - ("enter", "save_and_close", _("保存并关闭")), ("escape", "cancel", _("取消")), ] @@ -38,7 +35,7 @@ class LLMConfigDialog(ModalScreen): llm_client: LLMClientBase, ) -> None: """ - 初始化 LLM 配置对话框 + 初始化用户配置对话框 Args: config_manager: 配置管理器 @@ -49,21 +46,25 @@ class LLMConfigDialog(ModalScreen): self.config_manager = config_manager self.llm_client = llm_client - # 模型数据 + # 模型数据(仅 chat 模型) self.all_models: list[ModelInfo] = [] self.chat_models: list[ModelInfo] = [] - self.function_models: list[ModelInfo] = [] # 当前选择的模型(临时状态,未保存) self.selected_chat_model = config_manager.get_llm_chat_model() - self.selected_function_model = config_manager.get_llm_function_model() + + # 用户名(临时状态,未保存) + self.username = "" # TODO: 从 config_manager 读取用户名 + + # MCP 工具授权相关状态(仅 EULERINTELLI 后端) + self.auto_execute_status = False # 默认为手动确认 + self.mcp_status_loaded = False # 是否已成功加载状态 # 当前标签页 - self.current_tab = "chat" + self.current_tab = "general" - # 当前光标位置(每个标签页独立) + # 模型列表光标位置 self.chat_cursor = 0 - self.function_cursor = 0 # 加载状态 self.models_loaded = False @@ -71,22 +72,23 @@ class LLMConfigDialog(ModalScreen): def compose(self) -> ComposeResult: """构建对话框""" - with Container(id="llm-dialog-screen"), Container(id="llm-dialog"): + with Container(id="user-dialog-screen"), Container(id="user-dialog"): # 标签页容器 - with TabbedContent(id="llm-tabs", initial="chat-tab"): - with TabPane(_("基础模型"), id="chat-tab"): + with TabbedContent(id="user-tabs", initial="general-tab"): + with TabPane(_("常规设置"), id="general-tab"): pass # 内容将在 on_mount 中动态添加 - with TabPane(_("工具调用"), id="function-tab"): + with TabPane(_("大模型设置"), id="llm-tab"): pass # 内容将在 on_mount 中动态添加 # 帮助文本 yield Static( - _("↑↓: 选择模型 ←→: 切换标签 空格: 确认 回车: 保存 ESC: 取消"), - id="llm-dialog-help", + _("Tab: 切换焦点 ↑↓: 选择模型 空格: 确认 ESC: 取消"), + id="user-dialog-help", ) async def on_mount(self) -> None: - """组件挂载时加载模型列表""" + """组件挂载时加载模型列表和渲染内容""" await self._load_models() + await self._load_mcp_status() self._render_all_tabs() self._update_cursor_positions() @@ -97,9 +99,8 @@ class LLMConfigDialog(ModalScreen): if hasattr(self.llm_client, "get_available_models"): self.all_models = await self.llm_client.get_available_models() # type: ignore[attr-defined] - # 按类型分类模型 + # 只需要 chat 模型 self.chat_models = [model for model in self.all_models if LLMType.CHAT in model.llm_type] - self.function_models = [model for model in self.all_models if LLMType.FUNCTION in model.llm_type] self.models_loaded = True self.loading_error = False @@ -111,15 +112,74 @@ class LLMConfigDialog(ModalScreen): self.models_loaded = False self.loading_error = True + async def _load_mcp_status(self) -> None: + """异步加载 MCP 工具授权状态""" + try: + # 从客户端获取自动执行状态 + if hasattr(self.llm_client, "get_auto_execute_status"): + self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] + self.mcp_status_loaded = True + else: + self.auto_execute_status = False + self.mcp_status_loaded = False + + except (OSError, ValueError, RuntimeError): + self.auto_execute_status = False + self.mcp_status_loaded = False + def _render_all_tabs(self) -> None: """渲染所有标签页内容""" - self._render_tab_content("chat-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) - self._render_tab_content( - "function-tab", - self.function_models, - self.selected_function_model, - self.function_cursor, + self._render_general_tab() + self._render_llm_tab() + + def _render_general_tab(self) -> None: + """渲染常规设置标签页""" + tab_pane = self.query_one("#general-tab", TabPane) + + # 清空现有内容 + tab_pane.remove_children() + + # 创建表单容器 + form_container = Container(classes="general-settings-form") + + # 先挂载表单容器到 tab_pane + tab_pane.mount(form_container) + + # 然后再向表单容器添加子组件 + # 用户名输入 + form_container.mount(Label(_("用户名:"), classes="form-label")) + username_input = Input( + placeholder=_("请输入用户名"), + value=self.username, + id="username-input", + classes="form-input", ) + form_container.mount(username_input) + + # MCP 工具授权设置(仅当支持时显示) + if self.mcp_status_loaded: + form_container.mount(Label(_("MCP 工具授权:"), classes="form-label")) + mcp_button_container = Horizontal(classes="mcp-toggle-container") + form_container.mount(mcp_button_container) + mcp_button_container.mount( + Button( + _("自动执行") if self.auto_execute_status else _("手动确认"), + id="mcp-toggle-btn", + classes="form-button", + ), + ) + + # 按钮区域 + button_container = Horizontal(classes="form-buttons") + form_container.mount(button_container) + + # 向按钮容器添加按钮 + button_container.mount(Button(_("保存"), id="save-username-btn", variant="primary")) + button_container.mount(Button(_("取消"), id="cancel-general-btn", variant="default")) + + def _render_llm_tab(self) -> None: + """渲染大模型设置标签页""" + self._render_tab_content("llm-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) def _render_tab_content( self, @@ -233,117 +293,119 @@ class LLMConfigDialog(ModalScreen): self.chat_cursor = i break - # 工具调用模型光标 - if self.selected_function_model: - for i, model in enumerate(self.function_models): - if model.llm_id == self.selected_function_model: - self.function_cursor = i - break - @on(TabbedContent.TabActivated) def on_tab_changed(self, event: TabbedContent.TabActivated) -> None: """标签页切换事件""" tab_id = event.tab.id - if tab_id == "chat-tab": - self.current_tab = "chat" - elif tab_id == "function-tab": - self.current_tab = "function" - - def action_previous_tab(self) -> None: - """切换到上一个标签页""" - tabs = self.query_one("#llm-tabs", TabbedContent) - if self.current_tab == "chat": - tabs.active = "function-tab" - elif self.current_tab == "function": - tabs.active = "chat-tab" - - def action_next_tab(self) -> None: - """切换到下一个标签页""" - tabs = self.query_one("#llm-tabs", TabbedContent) - if self.current_tab == "chat": - tabs.active = "function-tab" - elif self.current_tab == "function": - tabs.active = "chat-tab" + if tab_id == "general-tab": + self.current_tab = "general" + elif tab_id == "llm-tab": + self.current_tab = "llm" + + @on(Button.Pressed, "#save-username-btn") + async def on_save_username(self) -> None: + """保存用户名""" + username_input = self.query_one("#username-input", Input) + new_username = username_input.value.strip() + + if not new_username: + # TODO: 显示错误提示 + return + + # TODO: 调用后端 API 保存用户名 + # await self._save_username_to_backend(new_username) + + # 暂时只保存到本地状态 + self.username = new_username + + # TODO: 显示保存成功提示 + # self.notify(_("用户名已保存")) + + @on(Button.Pressed, "#cancel-general-btn") + def on_cancel_general(self) -> None: + """取消常规设置的修改""" + # 重新渲染常规设置标签页,恢复原始值 + self._render_general_tab() + + @on(Button.Pressed, "#mcp-toggle-btn") + async def on_toggle_mcp(self) -> None: + """切换 MCP 工具授权模式""" + if not self.mcp_status_loaded: + return + + try: + # 检查客户端是否支持 MCP 操作 + if ( + not hasattr(self.llm_client, "enable_auto_execute") + or not hasattr(self.llm_client, "disable_auto_execute") + ): + return + + # 先禁用按钮防止重复点击 + mcp_btn = self.query_one("#mcp-toggle-btn", Button) + mcp_btn.disabled = True + mcp_btn.label = _("切换中...") + + # 根据当前状态调用相应的方法 + if self.auto_execute_status: + # 当前是自动执行,切换为手动确认 + await self.llm_client.disable_auto_execute() # type: ignore[attr-defined] + else: + # 当前是手动确认,切换为自动执行 + await self.llm_client.enable_auto_execute() # type: ignore[attr-defined] + + # 重新获取状态以确保同步 + if hasattr(self.llm_client, "get_auto_execute_status"): + self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] + + # 更新按钮状态 + mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") + mcp_btn.disabled = False + + except (OSError, ValueError, RuntimeError): + # 发生错误时恢复按钮状态 + mcp_btn = self.query_one("#mcp-toggle-btn", Button) + mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") + mcp_btn.disabled = False def action_previous_model(self) -> None: - """选择上一个模型""" - if not self.models_loaded: + """选择上一个模型(仅在大模型设置标签页生效)""" + if not self.models_loaded or self.current_tab != "llm": return - models = self._get_current_models() - if not models: + if not self.chat_models: return - if self.current_tab == "chat": - self.chat_cursor = max(0, self.chat_cursor - 1) - self._render_tab_content("chat-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) - elif self.current_tab == "function": - self.function_cursor = max(0, self.function_cursor - 1) - self._render_tab_content( - "function-tab", - self.function_models, - self.selected_function_model, - self.function_cursor, - ) + self.chat_cursor = max(0, self.chat_cursor - 1) + self._render_llm_tab() def action_next_model(self) -> None: - """选择下一个模型""" - if not self.models_loaded: + """选择下一个模型(仅在大模型设置标签页生效)""" + if not self.models_loaded or self.current_tab != "llm": return - models = self._get_current_models() - if not models: + if not self.chat_models: return - if self.current_tab == "chat": - self.chat_cursor = min(len(self.chat_models) - 1, self.chat_cursor + 1) - self._render_tab_content("chat-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) - elif self.current_tab == "function": - self.function_cursor = min(len(self.function_models) - 1, self.function_cursor + 1) - self._render_tab_content( - "function-tab", - self.function_models, - self.selected_function_model, - self.function_cursor, - ) + self.chat_cursor = min(len(self.chat_models) - 1, self.chat_cursor + 1) + self._render_llm_tab() def action_select_model(self) -> None: """确认选择当前光标所在的模型(临时选择,未保存)""" - if not self.models_loaded: + if not self.models_loaded or self.current_tab != "llm": return - models = self._get_current_models() - if not models: + if not self.chat_models: return - if self.current_tab == "chat" and 0 <= self.chat_cursor < len(self.chat_models): + if 0 <= self.chat_cursor < len(self.chat_models): self.selected_chat_model = self.chat_models[self.chat_cursor].llm_id or "" - self._render_tab_content("chat-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) - elif self.current_tab == "function" and 0 <= self.function_cursor < len(self.function_models): - self.selected_function_model = self.function_models[self.function_cursor].llm_id or "" - self._render_tab_content( - "function-tab", - self.function_models, - self.selected_function_model, - self.function_cursor, - ) - - def action_save_and_close(self) -> None: - """保存配置并关闭对话框""" - # 保存所有选择 - self.config_manager.set_llm_chat_model(self.selected_chat_model) - self.config_manager.set_llm_function_model(self.selected_function_model) - - self.app.pop_screen() + self._render_llm_tab() def action_cancel(self) -> None: """取消并关闭对话框""" - self.app.pop_screen() + # 如果在大模型设置标签页,保存已选择的模型 + if self.current_tab == "llm": + self.config_manager.set_llm_chat_model(self.selected_chat_model) - def _get_current_models(self) -> list[ModelInfo]: - """获取当前标签页的模型列表""" - if self.current_tab == "chat": - return self.chat_models - if self.current_tab == "function": - return self.function_models - return [] + self.app.pop_screen() diff --git a/src/app/settings.py b/src/app/settings.py index 2158c31..4d477e2 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -11,7 +11,7 @@ from textual.css.query import NoMatches from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Static -from app.dialogs import ExitDialog, LLMConfigDialog +from app.dialogs import ExitDialog, UserConfigDialog from backend.hermes import HermesChatClient from backend.openai import OpenAIClient from config import Backend, ConfigManager @@ -41,10 +41,6 @@ class SettingsScreen(ModalScreen): # 添加保存任务的集合 self.background_tasks: set[asyncio.Task] = set() - # MCP 工具授权相关状态 - self.auto_execute_status = False # 默认为手动确认 - self.mcp_status_loaded = False # 是否已成功加载状态 - # 验证相关状态 self.is_validated = False self.validation_message = "" @@ -79,12 +75,6 @@ class SettingsScreen(ModalScreen): def on_mount(self) -> None: """组件挂载时加载可用模型""" - if self.backend == Backend.EULERINTELLI: - task = asyncio.create_task(self.load_mcp_status()) - # 保存任务引用 - self.background_tasks.add(task) - task.add_done_callback(self.background_tasks.discard) - # 启动配置验证 self._schedule_validation() @@ -151,55 +141,17 @@ class SettingsScreen(ModalScreen): # EULERINTELLI 后端 return [ Horizontal( - Label(_("MCP 工具授权:"), classes="settings-label"), - Button( - _("自动执行") if self.auto_execute_status else _("手动确认"), - id="mcp-btn", - classes="settings-button", - disabled=not self.mcp_status_loaded, - ), - id="mcp-section", - classes="settings-option", - ), - Horizontal( - Label(_("LLM 配置:"), classes="settings-label"), + Label(_("用户设置:"), classes="settings-label"), Button( - _("配置模型"), - id="llm-config-btn", + _("更改用户设置"), + id="user-config-btn", classes="settings-button", ), - id="llm-config-section", + id="user-config-section", classes="settings-option", ), ] - async def load_mcp_status(self) -> None: - """异步加载 MCP 工具授权状态""" - try: - # 只有 EULERINTELLI 后端才支持 MCP 状态 - if self.backend != Backend.EULERINTELLI: - return - - # 从 Hermes 客户端获取自动执行状态 - if hasattr(self.llm_client, "get_auto_execute_status"): - self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] - self.mcp_status_loaded = True - else: - self.auto_execute_status = False - self.mcp_status_loaded = False - - # 更新 MCP 按钮文本和状态 - mcp_btn = self.query_one("#mcp-btn", Button) - mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") - mcp_btn.disabled = not self.mcp_status_loaded - - except (OSError, ValueError, RuntimeError): - self.auto_execute_status = False - self.mcp_status_loaded = False - mcp_btn = self.query_one("#mcp-btn", Button) - mcp_btn.label = _("手动确认") - mcp_btn.disabled = True - @on(Input.Changed, "#base-url, #api-key, #model-input") def on_config_changed(self) -> None: """当 Base URL、API Key 或模型改变时更新客户端并验证配置""" @@ -215,10 +167,6 @@ class SettingsScreen(ModalScreen): self._update_llm_client() else: # EULERINTELLI self._update_llm_client() - # 重新加载 MCP 状态 - task = asyncio.create_task(self.load_mcp_status()) - self.background_tasks.add(task) - task.add_done_callback(self.background_tasks.discard) # 重新验证配置 self._schedule_validation() @@ -250,21 +198,10 @@ class SettingsScreen(ModalScreen): # 切换后端后重新验证配置 self._schedule_validation() - @on(Button.Pressed, "#mcp-btn") - def toggle_mcp_authorization(self) -> None: - """切换 MCP 工具授权模式""" - if not self.mcp_status_loaded: - return - - # 创建切换任务 - task = asyncio.create_task(self._toggle_mcp_authorization_async()) - self.background_tasks.add(task) - task.add_done_callback(self.background_tasks.discard) - - @on(Button.Pressed, "#llm-config-btn") - def open_llm_config(self) -> None: - """打开 LLM 配置对话框""" - dialog = LLMConfigDialog(self.config_manager, self.llm_client) + @on(Button.Pressed, "#user-config-btn") + def open_user_config(self) -> None: + """打开用户配置对话框""" + dialog = UserConfigDialog(self.config_manager, self.llm_client) self.app.push_screen(dialog) @on(Button.Pressed, "#save-btn") @@ -402,7 +339,7 @@ class SettingsScreen(ModalScreen): spacer = self.query_one("#spacer") # 移除所有后端特定的组件 - for section_id in ["#model-section", "#mcp-section", "#llm-config-section"]: + for section_id in ["#model-section", "#user-config-section"]: sections = self.query(section_id) for section in sections: section.remove() @@ -414,12 +351,6 @@ class SettingsScreen(ModalScreen): else: container.mount(widget) - # 如果是 EULERINTELLI 后端,需要重新加载 MCP 状态 - if self.backend == Backend.EULERINTELLI: - task = asyncio.create_task(self.load_mcp_status()) - self.background_tasks.add(task) - task.add_done_callback(self.background_tasks.discard) - async def _validate_configuration(self) -> None: """验证当前配置""" try: @@ -511,40 +442,3 @@ class SettingsScreen(ModalScreen): # 恢复智能体状态 if current_agent_id: self.llm_client.set_current_agent(current_agent_id) - - async def _toggle_mcp_authorization_async(self) -> None: - """异步切换 MCP 工具授权模式""" - try: - # 检查客户端是否支持 MCP 操作 - if ( - not hasattr(self.llm_client, "enable_auto_execute") - or not hasattr(self.llm_client, "disable_auto_execute") - ): - return - - # 先禁用按钮防止重复点击 - mcp_btn = self.query_one("#mcp-btn", Button) - mcp_btn.disabled = True - mcp_btn.label = _("切换中...") - - # 根据当前状态调用相应的方法 - if self.auto_execute_status: - # 当前是自动执行,切换为手动确认 - await self.llm_client.disable_auto_execute() # type: ignore[attr-defined] - else: - # 当前是手动确认,切换为自动执行 - await self.llm_client.enable_auto_execute() # type: ignore[attr-defined] - - # 重新获取状态以确保同步 - if hasattr(self.llm_client, "get_auto_execute_status"): - self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] - - # 更新按钮状态 - mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") - mcp_btn.disabled = False - - except (OSError, ValueError, RuntimeError): - # 发生错误时恢复按钮状态 - mcp_btn = self.query_one("#mcp-btn", Button) - mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") - mcp_btn.disabled = False diff --git a/src/config/manager.py b/src/config/manager.py index b9305f6..1d8d026 100644 --- a/src/config/manager.py +++ b/src/config/manager.py @@ -205,21 +205,12 @@ class ConfigManager: self._save_settings() def get_llm_chat_model(self) -> str: - """获取基础模型的 llmId""" - return self.data.eulerintelli.llm.chat + """获取 Chat 模型的 llmId""" + return self.data.eulerintelli.llm_chat def set_llm_chat_model(self, llm_id: str) -> None: - """更新基础模型的 llmId 并保存""" - self.data.eulerintelli.llm.chat = llm_id - self._save_settings() - - def get_llm_function_model(self) -> str: - """获取工具调用模型的 llmId""" - return self.data.eulerintelli.llm.function - - def set_llm_function_model(self, llm_id: str) -> None: - """更新工具调用模型的 llmId 并保存""" - self.data.eulerintelli.llm.function = llm_id + """更新 Chat 模型的 llmId 并保存""" + self.data.eulerintelli.llm_chat = llm_id self._save_settings() def get_locale(self) -> str: diff --git a/src/config/model.py b/src/config/model.py index 5f95bb6..4f02f11 100644 --- a/src/config/model.py +++ b/src/config/model.py @@ -50,29 +50,6 @@ class OpenAIConfig: return {"base_url": self.base_url, "model": self.model, "api_key": self.api_key} -@dataclass -class LLMConfig: - """LLM 模型配置""" - - chat: str = field(default="") # 基础模型的 llmId - function: str = field(default="") # 工具调用模型的 llmId - - @classmethod - def from_dict(cls, d: dict) -> "LLMConfig": - """从字典初始化配置""" - return cls( - chat=d.get("chat", ""), - function=d.get("function", ""), - ) - - def to_dict(self) -> dict: - """转换为字典""" - return { - "chat": self.chat, - "function": self.function, - } - - @dataclass class HermesConfig: """Hermes 后端配置""" @@ -80,7 +57,7 @@ class HermesConfig: base_url: str = field(default="http://127.0.0.1:8002") api_key: str = field(default="") default_app: str = field(default="") - llm: LLMConfig = field(default_factory=LLMConfig) + llm_chat: str = field(default="") # Chat 模型的 llmId @classmethod def from_dict(cls, d: dict) -> "HermesConfig": @@ -89,7 +66,7 @@ class HermesConfig: base_url=d.get("base_url", cls.base_url), api_key=d.get("api_key", cls.api_key), default_app=d.get("default_app", cls.default_app), - llm=LLMConfig.from_dict(d.get("llm", {})), + llm_chat=d.get("llm_chat", cls.llm_chat), ) def to_dict(self) -> dict: @@ -98,7 +75,7 @@ class HermesConfig: "base_url": self.base_url, "api_key": self.api_key, "default_app": self.default_app, - "llm": self.llm.to_dict(), + "llm_chat": self.llm_chat, } -- Gitee From b588c221bf04053bd36fdc27e81e21277ad1ab54 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Fri, 24 Oct 2025 10:59:59 +0800 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=88=9D?= =?UTF-8?q?=E6=AD=A5=E6=94=AF=E6=8C=81=E7=94=A8=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=B8=8E=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/css/styles.tcss | 80 ++++--- src/app/dialogs/user.py | 332 ++++++++++++++-------------- src/app/settings.py | 22 +- src/app/tui.py | 10 +- src/backend/__init__.py | 4 +- src/backend/hermes/client.py | 126 +++++++---- src/backend/hermes/services/user.py | 59 ++--- 7 files changed, 363 insertions(+), 270 deletions(-) diff --git a/src/app/css/styles.tcss b/src/app/css/styles.tcss index 65014a7..c85b5d2 100644 --- a/src/app/css/styles.tcss +++ b/src/app/css/styles.tcss @@ -493,16 +493,15 @@ UserConfigDialog { #user-dialog-screen { align: center middle; - width: 70%; + width: 80%; height: 80%; } #user-dialog { - background: $surface; border: solid #4963b1; padding-left: 1; padding-right: 1; - padding-bottom: 1; + padding-bottom: 0; width: 100%; height: 100%; } @@ -513,32 +512,22 @@ UserConfigDialog { margin: 1 0; } -/* 常规设置表单样式 */ -.general-settings-form { - padding: 2; - width: 100%; - height: auto; -} - -.form-label { - color: #888888; - padding: 1 0; - width: auto; +/* 标签页面板 */ +#general-tab, #llm-tab { + layout: vertical; } -.form-input { +/* 大模型设置标签页内容容器 */ +.llm-tab-content { + height: 1fr; width: 100%; - margin-bottom: 2; } -.mcp-toggle-container { - height: 3; - margin-bottom: 2; -} - -.form-button { - width: auto; - min-width: 20; +/* 常规设置表单样式 */ +.general-settings-form { + padding: 2; + width: 100%; + height: 1fr; } .form-buttons { @@ -556,12 +545,13 @@ UserConfigDialog { /* 模型列表容器 */ .llm-model-list { - height: 1fr; + height: auto; + max-height: 1fr; overflow-y: auto; scrollbar-size: 1 1; border: solid #688efd; padding: 1; - margin-bottom: 1; + margin: 1 0; } /* 模型项样式 */ @@ -570,8 +560,14 @@ UserConfigDialog { color: #ffffff; } -/* 选中的模型 */ -.llm-model-selected { +/* 已保存的模型(当前配置中保存的) */ +.llm-model-saved { + color: #888888; + text-style: italic; +} + +/* 已激活的模型(用空格确认,等待保存) */ +.llm-model-activated { color: #4caf50; text-style: bold; } @@ -585,7 +581,9 @@ UserConfigDialog { .llm-model-detail { border: solid #888888; padding: 1; + margin: 1 0; background: rgba(73, 99, 177, 0.1); + height: auto; } /* 详情行 */ @@ -608,12 +606,30 @@ UserConfigDialog { width: 1fr; } -/* 帮助文本 */ -#user-dialog-help { +/* 常规设置按钮区域 */ +#general-buttons { + height: auto; + min-height: 3; + align: center middle; + padding: 0; + dock: bottom; +} + +#general-buttons > Button { + margin: 0 1; + width: auto; + min-height: 3; + height: 3; +} + +/* 大模型设置帮助文本 */ +#llm-dialog-help { text-align: center; color: #888888; - text-style: italic; - padding-top: 1; + padding: 0 1; + height: auto; + min-height: 1; + dock: bottom; } /* 加载状态 */ diff --git a/src/app/dialogs/user.py b/src/app/dialogs/user.py index 340217d..cdcadbf 100644 --- a/src/app/dialogs/user.py +++ b/src/app/dialogs/user.py @@ -9,13 +9,14 @@ from textual.containers import Container, Horizontal from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Static, TabbedContent, TabPane +from backend import HermesChatClient from backend.models import LLMType, ModelInfo from i18n.manager import _ if TYPE_CHECKING: from textual.app import ComposeResult - from backend.base import LLMClientBase + from backend import LLMClientBase from config.manager import ConfigManager @@ -25,7 +26,8 @@ class UserConfigDialog(ModalScreen): BINDINGS: ClassVar[list[tuple[str, str, str]]] = [ ("up", "previous_model", _("上一个模型")), ("down", "next_model", _("下一个模型")), - ("space", "select_model", _("选择模型")), + ("space", "activate_model", _("激活模型")), + ("enter", "save_llm_settings", _("保存大模型设置")), ("escape", "cancel", _("取消")), ] @@ -46,19 +48,31 @@ class UserConfigDialog(ModalScreen): self.config_manager = config_manager self.llm_client = llm_client - # 模型数据(仅 chat 模型) + # 模型数据 self.all_models: list[ModelInfo] = [] self.chat_models: list[ModelInfo] = [] - # 当前选择的模型(临时状态,未保存) - self.selected_chat_model = config_manager.get_llm_chat_model() + # 当前已保存的模型(从配置读取) + self.saved_chat_model = config_manager.get_llm_chat_model() + # 已激活的模型(用空格键确认,等待保存) + self.activated_chat_model = self.saved_chat_model # 用户名(临时状态,未保存) - self.username = "" # TODO: 从 config_manager 读取用户名 + self.username = "" + if isinstance(self.llm_client, HermesChatClient): + self.username = self.llm_client.get_user_name() - # MCP 工具授权相关状态(仅 EULERINTELLI 后端) + # 是否管理员 + self.is_admin = False + if isinstance(self.llm_client, HermesChatClient): + self.is_admin = self.llm_client.is_admin() + + # MCP 工具授权相关状态 self.auto_execute_status = False # 默认为手动确认 self.mcp_status_loaded = False # 是否已成功加载状态 + if isinstance(self.llm_client, HermesChatClient): + self.auto_execute_status = self.llm_client.get_auto_execute_status() + self.mcp_status_loaded = True # 当前标签页 self.current_tab = "general" @@ -72,65 +86,38 @@ class UserConfigDialog(ModalScreen): def compose(self) -> ComposeResult: """构建对话框""" - with Container(id="user-dialog-screen"), Container(id="user-dialog"): - # 标签页容器 - with TabbedContent(id="user-tabs", initial="general-tab"): - with TabPane(_("常规设置"), id="general-tab"): - pass # 内容将在 on_mount 中动态添加 - with TabPane(_("大模型设置"), id="llm-tab"): - pass # 内容将在 on_mount 中动态添加 - # 帮助文本 - yield Static( - _("Tab: 切换焦点 ↑↓: 选择模型 空格: 确认 ESC: 取消"), - id="user-dialog-help", - ) + with ( + Container(id="user-dialog-screen"), + Container(id="user-dialog"), + TabbedContent( + id="user-tabs", + initial="general-tab", + ), + ): + yield TabPane(_("常规设置"), id="general-tab") + yield TabPane(_("大模型设置"), id="llm-tab") async def on_mount(self) -> None: """组件挂载时加载模型列表和渲染内容""" await self._load_models() - await self._load_mcp_status() self._render_all_tabs() self._update_cursor_positions() async def _load_models(self) -> None: """异步加载模型列表""" try: - # 获取所有可用模型 - if hasattr(self.llm_client, "get_available_models"): - self.all_models = await self.llm_client.get_available_models() # type: ignore[attr-defined] - - # 只需要 chat 模型 - self.chat_models = [model for model in self.all_models if LLMType.CHAT in model.llm_type] - - self.models_loaded = True - self.loading_error = False - else: - self.models_loaded = False - self.loading_error = True - + self.all_models = await self.llm_client.get_available_models() + self.chat_models = [model for model in self.all_models if LLMType.CHAT in model.llm_type] + self.models_loaded = True + self.loading_error = False except (OSError, ValueError, RuntimeError): self.models_loaded = False self.loading_error = True - async def _load_mcp_status(self) -> None: - """异步加载 MCP 工具授权状态""" - try: - # 从客户端获取自动执行状态 - if hasattr(self.llm_client, "get_auto_execute_status"): - self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] - self.mcp_status_loaded = True - else: - self.auto_execute_status = False - self.mcp_status_loaded = False - - except (OSError, ValueError, RuntimeError): - self.auto_execute_status = False - self.mcp_status_loaded = False - def _render_all_tabs(self) -> None: """渲染所有标签页内容""" self._render_general_tab() - self._render_llm_tab() + self._render_chat_llm_tab() def _render_general_tab(self) -> None: """渲染常规设置标签页""" @@ -145,47 +132,54 @@ class UserConfigDialog(ModalScreen): # 先挂载表单容器到 tab_pane tab_pane.mount(form_container) - # 然后再向表单容器添加子组件 # 用户名输入 - form_container.mount(Label(_("用户名:"), classes="form-label")) - username_input = Input( - placeholder=_("请输入用户名"), - value=self.username, - id="username-input", - classes="form-input", + form_container.mount( + Horizontal( + Label(_("用户名:"), classes="settings-label"), + Input( + placeholder=_("请输入用户名"), + value=self.username, + id="username-input", + classes="settings-input", + ), + classes="settings-option", + ), ) - form_container.mount(username_input) # MCP 工具授权设置(仅当支持时显示) if self.mcp_status_loaded: - form_container.mount(Label(_("MCP 工具授权:"), classes="form-label")) - mcp_button_container = Horizontal(classes="mcp-toggle-container") - form_container.mount(mcp_button_container) - mcp_button_container.mount( - Button( - _("自动执行") if self.auto_execute_status else _("手动确认"), - id="mcp-toggle-btn", - classes="form-button", + form_container.mount( + Horizontal( + Label(_("MCP 工具授权:"), classes="settings-label"), + Button( + _("自动执行") if self.auto_execute_status else _("手动确认"), + id="mcp-toggle-btn", + classes="settings-button", + ), + classes="settings-option", ), ) # 按钮区域 - button_container = Horizontal(classes="form-buttons") - form_container.mount(button_container) - - # 向按钮容器添加按钮 - button_container.mount(Button(_("保存"), id="save-username-btn", variant="primary")) + button_container = Horizontal(id="general-buttons", classes="form-buttons") + tab_pane.mount(button_container) + + # 创建按钮并挂载到按钮容器 + save_btn = Button(_("保存"), id="save-user-settings-btn", variant="primary") + # 如果用户名为空,禁用保存按钮 + save_btn.disabled = not self.username + button_container.mount(save_btn) button_container.mount(Button(_("取消"), id="cancel-general-btn", variant="default")) - def _render_llm_tab(self) -> None: + def _render_chat_llm_tab(self) -> None: """渲染大模型设置标签页""" - self._render_tab_content("llm-tab", self.chat_models, self.selected_chat_model, self.chat_cursor) + self._render_tab_content("llm-tab", self.chat_models, self.activated_chat_model, self.chat_cursor) def _render_tab_content( self, tab_id: str, models: list[ModelInfo], - selected_llm_id: str, + activated_llm_id: str, cursor_index: int, ) -> None: """ @@ -194,7 +188,7 @@ class UserConfigDialog(ModalScreen): Args: tab_id: 标签页 ID models: 模型列表 - selected_llm_id: 当前选中的模型 llmId + activated_llm_id: 已激活的模型 llmId(用空格键确认) cursor_index: 光标位置 """ @@ -203,40 +197,54 @@ class UserConfigDialog(ModalScreen): # 清空现有内容 tab_pane.remove_children() + # 创建内容容器 + content_container = Container(id=f"{tab_id}-content", classes="llm-tab-content") + tab_pane.mount(content_container) + if not self.models_loaded: if self.loading_error: - tab_pane.mount(Static(_("加载模型失败"), classes="llm-error")) + content_container.mount(Static(_("加载模型失败"), classes="llm-error")) else: - tab_pane.mount(Static(_("加载中..."), classes="llm-loading")) - return - - if not models: - tab_pane.mount(Static(_("暂无可用模型"), classes="llm-empty")) - return - - # 渲染模型列表 - model_list_container = Container(id=f"{tab_id}-list", classes="llm-model-list") - for i, model in enumerate(models): - is_selected = model.llm_id == selected_llm_id - is_cursor = i == cursor_index - - # 构建样式类 - classes = "llm-model-item" - if is_selected: - classes += " llm-model-selected" - if is_cursor: - classes += " llm-model-cursor" - - model_item = Static(model.model_name, classes=classes) - model_list_container.mount(model_item) - - tab_pane.mount(model_list_container) - - # 显示当前光标所指模型的详细信息 - if 0 <= cursor_index < len(models): - current_model = models[cursor_index] - detail_container = self._create_model_detail(current_model) - tab_pane.mount(detail_container) + content_container.mount(Static(_("加载中..."), classes="llm-loading")) + + elif not models: + content_container.mount(Static(_("暂无可用模型"), classes="llm-empty")) + + else: + # 渲染模型列表 + model_list_container = Container(id=f"{tab_id}-list", classes="llm-model-list") + for i, model in enumerate(models): + is_saved = model.llm_id == self.saved_chat_model # 已保存的 + is_activated = model.llm_id == activated_llm_id # 已激活的(用空格确认) + is_cursor = i == cursor_index # 光标选中的 + + # 构建样式类 + classes = "llm-model-item" + if is_saved: + classes += " llm-model-saved" + if is_activated: + classes += " llm-model-activated" + if is_cursor: + classes += " llm-model-cursor" + + model_item = Static(model.model_name, classes=classes) + model_list_container.mount(model_item) + + content_container.mount(model_list_container) + + # 显示当前光标所指模型的详细信息 + if 0 <= cursor_index < len(models): + current_model = models[cursor_index] + detail_container = self._create_model_detail(current_model) + content_container.mount(detail_container) + + # 添加帮助文本 + tab_pane.mount( + Static( + _("↑↓: 选择模型 空格: 激活 回车: 保存 ESC: 取消"), + id="llm-dialog-help", + ), + ) def _create_model_detail(self, model: ModelInfo) -> Container: """ @@ -287,9 +295,9 @@ class UserConfigDialog(ModalScreen): def _update_cursor_positions(self) -> None: """根据已保存的配置更新光标位置""" # 基础模型光标 - if self.selected_chat_model: + if self.activated_chat_model: for i, model in enumerate(self.chat_models): - if model.llm_id == self.selected_chat_model: + if model.llm_id == self.activated_chat_model: self.chat_cursor = i break @@ -302,71 +310,53 @@ class UserConfigDialog(ModalScreen): elif tab_id == "llm-tab": self.current_tab = "llm" - @on(Button.Pressed, "#save-username-btn") - async def on_save_username(self) -> None: - """保存用户名""" + @on(Input.Changed, "#username-input") + def on_username_changed(self, event: Input.Changed) -> None: + """用户名输入框内容变化时,更新保存按钮的状态""" + username = event.value.strip() + # 同步更新本地状态 + self.username = username + # 更新保存按钮的禁用状态 + save_btn = self.query_one("#save-user-settings-btn", Button) + save_btn.disabled = not username + + @on(Button.Pressed, "#save-user-settings-btn") + async def on_save_user_settings(self) -> None: + """保存用户设置(用户名和自动执行状态)""" username_input = self.query_one("#username-input", Input) new_username = username_input.value.strip() if not new_username: - # TODO: 显示错误提示 return - # TODO: 调用后端 API 保存用户名 - # await self._save_username_to_backend(new_username) - - # 暂时只保存到本地状态 - self.username = new_username + # 调用后端 API 保存用户信息 + if hasattr(self.llm_client, "update_user_info"): + success = await self.llm_client.update_user_info( # type: ignore[attr-defined] + user_name=new_username, + auto_execute=self.auto_execute_status, + ) - # TODO: 显示保存成功提示 - # self.notify(_("用户名已保存")) + if success: + # 更新本地状态 + self.username = new_username @on(Button.Pressed, "#cancel-general-btn") def on_cancel_general(self) -> None: - """取消常规设置的修改""" - # 重新渲染常规设置标签页,恢复原始值 - self._render_general_tab() + """取消常规设置的修改,关闭对话框""" + self.app.pop_screen() @on(Button.Pressed, "#mcp-toggle-btn") - async def on_toggle_mcp(self) -> None: - """切换 MCP 工具授权模式""" + def on_toggle_mcp(self) -> None: + """切换 MCP 工具授权模式(仅改变本地临时状态)""" if not self.mcp_status_loaded: return - try: - # 检查客户端是否支持 MCP 操作 - if ( - not hasattr(self.llm_client, "enable_auto_execute") - or not hasattr(self.llm_client, "disable_auto_execute") - ): - return - - # 先禁用按钮防止重复点击 - mcp_btn = self.query_one("#mcp-toggle-btn", Button) - mcp_btn.disabled = True - mcp_btn.label = _("切换中...") - - # 根据当前状态调用相应的方法 - if self.auto_execute_status: - # 当前是自动执行,切换为手动确认 - await self.llm_client.disable_auto_execute() # type: ignore[attr-defined] - else: - # 当前是手动确认,切换为自动执行 - await self.llm_client.enable_auto_execute() # type: ignore[attr-defined] - - # 重新获取状态以确保同步 - if hasattr(self.llm_client, "get_auto_execute_status"): - self.auto_execute_status = await self.llm_client.get_auto_execute_status() # type: ignore[attr-defined] + # 切换本地状态 + self.auto_execute_status = not self.auto_execute_status - # 更新按钮状态 - mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") - mcp_btn.disabled = False - - except (OSError, ValueError, RuntimeError): - # 发生错误时恢复按钮状态 - mcp_btn = self.query_one("#mcp-toggle-btn", Button) - mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") - mcp_btn.disabled = False + # 更新按钮显示 + mcp_btn = self.query_one("#mcp-toggle-btn", Button) + mcp_btn.label = _("自动执行") if self.auto_execute_status else _("手动确认") def action_previous_model(self) -> None: """选择上一个模型(仅在大模型设置标签页生效)""" @@ -377,7 +367,7 @@ class UserConfigDialog(ModalScreen): return self.chat_cursor = max(0, self.chat_cursor - 1) - self._render_llm_tab() + self._render_chat_llm_tab() def action_next_model(self) -> None: """选择下一个模型(仅在大模型设置标签页生效)""" @@ -388,10 +378,10 @@ class UserConfigDialog(ModalScreen): return self.chat_cursor = min(len(self.chat_models) - 1, self.chat_cursor + 1) - self._render_llm_tab() + self._render_chat_llm_tab() - def action_select_model(self) -> None: - """确认选择当前光标所在的模型(临时选择,未保存)""" + def action_activate_model(self) -> None: + """激活当前光标所在的模型(用空格键),等待保存""" if not self.models_loaded or self.current_tab != "llm": return @@ -399,13 +389,23 @@ class UserConfigDialog(ModalScreen): return if 0 <= self.chat_cursor < len(self.chat_models): - self.selected_chat_model = self.chat_models[self.chat_cursor].llm_id or "" - self._render_llm_tab() + self.activated_chat_model = self.chat_models[self.chat_cursor].llm_id or "" + self._render_chat_llm_tab() + + def action_save_llm_settings(self) -> None: + """保存大模型设置(用回车键)""" + if not self.models_loaded or self.current_tab != "llm": + return + + if not self.chat_models: + return + + # 保存已激活的模型到配置 + self.config_manager.set_llm_chat_model(self.activated_chat_model) + self.saved_chat_model = self.activated_chat_model + # 刷新显示以更新已保存状态 + self._render_chat_llm_tab() def action_cancel(self) -> None: """取消并关闭对话框""" - # 如果在大模型设置标签页,保存已选择的模型 - if self.current_tab == "llm": - self.config_manager.set_llm_chat_model(self.selected_chat_model) - self.app.pop_screen() diff --git a/src/app/settings.py b/src/app/settings.py index 4d477e2..a62ec2f 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -217,6 +217,17 @@ class SettingsScreen(ModalScreen): if not self.is_validated: return + # 仅在真正修改了后端/URL/API Key 时才重建 LLM 客户端 + old_backend = self.config_manager.get_backend() + + if old_backend == Backend.OPENAI: + old_base = self.config_manager.get_base_url() + old_key = self.config_manager.get_api_key() + else: + old_base = self.config_manager.get_eulerintelli_url() + old_key = self.config_manager.get_eulerintelli_key() + + # 先保存新的配置到 ConfigManager self.config_manager.set_backend(self.backend) base_url = self.query_one("#base-url", Input).value @@ -238,9 +249,16 @@ class SettingsScreen(ModalScreen): self.config_manager.set_eulerintelli_url(base_url) self.config_manager.set_eulerintelli_key(api_key) - # 通知主应用刷新客户端 + # 判断是否需要刷新 LLM 客户端(只有后端、URL 或 API Key 发生变化时) + need_refresh = False + if old_backend != self.backend: + need_refresh = True + elif old_base != base_url or old_key != api_key: + # 同一后端,比较 URL 和 Key + need_refresh = True + refresh_method = getattr(self.app, "refresh_llm_client", None) - if refresh_method: + if need_refresh and refresh_method: refresh_method() self.app.pop_screen() diff --git a/src/app/tui.py b/src/app/tui.py index 292662c..88cb45a 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -422,8 +422,8 @@ class IntelligentTerminal(App): return self._llm_client def refresh_llm_client(self) -> None: - """刷新 LLM 客户端实例,用于配置更改后重新创建客户端""" - # 保存当前智能体状态 + """重新创建 LLM 客户端实例,用于后端/URL/API Key 变更后刷新连接""" + # 保存当前智能体状态以便恢复 current_agent_id = self.current_agent[0] if self.current_agent else "" self._llm_client = BackendFactory.create_client(self.config_manager) @@ -432,11 +432,15 @@ class IntelligentTerminal(App): if current_agent_id and isinstance(self._llm_client, HermesChatClient): self._llm_client.set_current_agent(current_agent_id) - # 为 Hermes 客户端设置 MCP 事件处理器 + # 为 Hermes 客户端设置 MCP 事件处理器并加载用户信息 if isinstance(self._llm_client, HermesChatClient): mcp_handler = TUIMCPEventHandler(self, self._llm_client) self._llm_client.set_mcp_handler(mcp_handler) + task = asyncio.create_task(self._llm_client.ensure_user_info_loaded()) + self.background_tasks.add(task) + task.add_done_callback(self.background_tasks.discard) + # 后端切换时重新初始化智能体状态 self._reinitialize_agent_state() diff --git a/src/backend/__init__.py b/src/backend/__init__.py index 89907cc..62d732c 100644 --- a/src/backend/__init__.py +++ b/src/backend/__init__.py @@ -2,6 +2,8 @@ from .base import LLMClientBase from .factory import BackendFactory +from .hermes import HermesChatClient from .models import LLMType, ModelInfo +from .openai import OpenAIClient -__all__ = ["BackendFactory", "LLMClientBase", "LLMType", "ModelInfo"] +__all__ = ["BackendFactory", "HermesChatClient", "LLMClientBase", "LLMType", "ModelInfo", "OpenAIClient"] diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index 8362096..ca70820 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import time -from typing import TYPE_CHECKING, Self +from typing import TYPE_CHECKING, Any, Self from urllib.parse import urljoin import httpx @@ -58,6 +58,10 @@ class HermesChatClient(LLMClientBase): # MCP 事件处理器(可选) self._mcp_handler: MCPEventHandler | None = None + # 用户信息缓存(在初始化时加载) + self._user_info: dict[str, Any] | None = None + self._user_info_loaded: bool = False + self.logger.info("Hermes 客户端初始化成功 - URL: %s", base_url) @property @@ -110,6 +114,86 @@ class HermesChatClient(LLMClientBase): self.current_agent_id = agent_id self.logger.info("设置当前智能体ID: %s", agent_id or "无智能体") + async def ensure_user_info_loaded(self) -> bool: + """ + 确保用户信息已加载 + + 在 Hermes 后端初始化时调用,加载并缓存用户信息。 + 后续可以直接通过 get_user_xxx 方法从内存获取,无需重复请求。 + + Returns: + bool: 是否成功加载用户信息 + + """ + if self._user_info_loaded: + return True + + self.logger.info("开始加载用户信息...") + self._user_info = await self.user_manager.get_user_info() + + if self._user_info is not None: + self._user_info_loaded = True + self.logger.info( + "用户信息加载成功 - ID: %s, 用户名: %s", + self._user_info.get("id"), + self._user_info.get("userName"), + ) + return True + + self.logger.warning("用户信息加载失败") + return False + + def get_user_id(self) -> int | None: + """获取用户ID(从内存缓存)""" + if self._user_info is None: + return None + return self._user_info.get("id") + + def get_user_name(self) -> str: + """获取用户名(从内存缓存)""" + if self._user_info is None: + return "" + return self._user_info.get("userName", "") + + def get_auto_execute_status(self) -> bool: + """获取自动执行状态(从内存缓存)""" + if self._user_info is None: + return False + return self._user_info.get("autoExecute", False) + + def is_admin(self) -> bool: + """获取管理员状态(从内存缓存)""" + if self._user_info is None: + return False + return self._user_info.get("isAdmin", False) + + async def update_user_info(self, *, user_name: str, auto_execute: bool) -> bool: + """ + 更新用户信息 + + 更新成功后会自动更新内存中的缓存。 + + Args: + user_name: 用户名 + auto_execute: 是否启用自动执行 + + Returns: + bool: 更新是否成功 + + """ + success = await self.user_manager.update_user_info( + user_name=user_name, + auto_execute=auto_execute, + ) + + if success and self._user_info is not None: + # 更新内存缓存 + self._user_info["userName"] = user_name + self._user_info["autoExecute"] = auto_execute + self.logger.info("已更新内存中的用户信息缓存") + + return success + def reset_conversation(self) -> None: """重置会话,下次聊天时会创建新的会话""" if self._conversation_manager is not None: @@ -250,46 +334,6 @@ class HermesChatClient(LLMClientBase): log_exception(self.logger, "MCP 响应请求失败", e) raise - async def get_auto_execute_status(self) -> bool: - """ - 获取用户自动执行状态 - - 这是一个便捷方法,从用户信息中提取自动执行状态。 - 默认情况下返回 False。 - - Returns: - bool: 自动执行状态,默认为 False - - """ - user_info = await self.user_manager.get_user_info() - if user_info is None: - self.logger.warning("无法获取用户信息,自动执行状态默认为 False") - return False - - auto_execute = user_info.get("auto_execute", False) - self.logger.debug("当前自动执行状态: %s", auto_execute) - return auto_execute - - async def enable_auto_execute(self) -> None: - """ - 启用自动执行 - - Returns: - bool: 更新是否成功 - - """ - await self.user_manager.update_auto_execute(auto_execute=True) - - async def disable_auto_execute(self) -> None: - """ - 禁用自动执行 - - Returns: - bool: 更新是否成功 - - """ - await self.user_manager.update_auto_execute(auto_execute=False) - async def interrupt(self) -> None: """ 中断当前正在进行的请求 diff --git a/src/backend/hermes/services/user.py b/src/backend/hermes/services/user.py index 0db67b5..6d20c43 100644 --- a/src/backend/hermes/services/user.py +++ b/src/backend/hermes/services/user.py @@ -30,17 +30,17 @@ class HermesUserManager: """ 获取用户信息 - 通过调用 GET /api/auth/user 接口获取当前用户信息, - 包括用户标识、权限、自动执行设置等。 + 通过调用 GET /api/user 接口获取当前用户信息, + 包括用户标识、用户名、权限、自动执行设置等。 Returns: dict[str, Any] | None: 用户信息字典,如果请求失败返回 None 返回数据格式: { - "user_sub": str, # 用户标识 - "revision": bool, # 权限标识 - "is_admin": bool, # 是否管理员 - "auto_execute": bool # 是否自动执行 + "id": int, # 用户ID + "userName": str, # 用户名 + "isAdmin": bool, # 是否管理员 + "autoExecute": bool # 是否自动执行 } """ @@ -49,7 +49,7 @@ class HermesUserManager: try: client = await self.http_manager.get_client() - user_url = urljoin(self.http_manager.base_url, "/api/auth/user") + user_url = urljoin(self.http_manager.base_url, "/api/user") headers = self.http_manager.build_headers() response = await client.get(user_url, headers=headers) @@ -83,10 +83,11 @@ class HermesUserManager: user_info = data["result"] self.logger.info( - "获取用户信息成功 - 用户: %s, 自动执行: %s, 管理员: %s", - user_info.get("user_sub", "未知"), - user_info.get("auto_execute", False), - user_info.get("is_admin", False), + "获取用户信息成功 - 用户ID: %s, 用户名: %s, 自动执行: %s, 管理员: %s", + user_info.get("id", "未知"), + user_info.get("userName", "未知"), + user_info.get("autoExecute", False), + user_info.get("isAdmin", False), ) except (httpx.HTTPError, httpx.InvalidURL) as e: @@ -96,7 +97,7 @@ class HermesUserManager: log_api_request( self.logger, "GET", - f"{self.http_manager.base_url}/api/auth/user", + f"{self.http_manager.base_url}/api/user", 500, duration, error=str(e), @@ -106,13 +107,15 @@ class HermesUserManager: else: return user_info - async def update_auto_execute(self, *, auto_execute: bool) -> None: + async def update_user_info(self, *, user_name: str, auto_execute: bool = False) -> bool: """ - 更新用户自动执行设置 + 更新用户信息 - 通过调用 POST /api/user 接口更新当前用户的自动执行设置。 + 通过调用 POST /api/user 接口更新当前用户的信息。 + 同时更新用户名和自动执行设置。 Args: + user_name: 用户名 auto_execute: 是否启用自动执行 Returns: @@ -120,7 +123,11 @@ class HermesUserManager: """ start_time = time.time() - self.logger.info("开始请求 Hermes 用户设置更新 API - auto_execute: %s", auto_execute) + self.logger.info( + "开始请求 Hermes 用户信息更新 API - user_name: %s, auto_execute: %s", + user_name, + auto_execute, + ) try: client = await self.http_manager.get_client() @@ -132,7 +139,8 @@ class HermesUserManager: ) # 构建请求体 - request_data = { + request_data: dict[str, Any] = { + "userName": user_name, "autoExecute": auto_execute, } @@ -150,15 +158,13 @@ class HermesUserManager: # 处理HTTP错误状态 if response.status_code != HTTP_OK: error_msg = f"API 调用失败,状态码: {response.status_code}" - self.logger.warning("更新用户设置失败: %s", error_msg) - return - - self.logger.info("更新用户设置成功") + self.logger.warning("更新用户信息失败: %s", error_msg) + return False except (httpx.HTTPError, httpx.InvalidURL) as e: # 网络请求异常 duration = time.time() - start_time - log_exception(self.logger, "Hermes 用户设置更新 API 请求异常", e) + log_exception(self.logger, "Hermes 用户信息更新 API 请求异常", e) log_api_request( self.logger, "POST", @@ -167,8 +173,11 @@ class HermesUserManager: duration, error=str(e), ) - self.logger.warning("Hermes 用户设置更新 API 请求异常") - return + self.logger.warning("Hermes 用户信息更新 API 请求异常") + return False + else: + self.logger.info("更新用户信息成功") + return True def _validate_user_response(self, data: dict[str, Any]) -> bool: """验证用户信息 API 响应结构""" @@ -189,7 +198,7 @@ class HermesUserManager: return False # 检查必要字段是否存在 - required_fields = ["user_sub", "auto_execute"] + required_fields = ["id", "userName", "isAdmin", "autoExecute"] for field in required_fields: if field not in result: self.logger.warning("用户信息缺少必要字段: %s", field) -- Gitee From 27bf5d316233d401e0e69917567bb4ec76374e4d Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Fri, 24 Oct 2025 16:20:51 +0800 Subject: [PATCH 3/9] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Client=20?= =?UTF-8?q?=E5=AE=9E=E4=BE=8B=E5=88=B7=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/settings.py | 259 +++++++++++++++++++--------------- src/app/tui.py | 16 ++- src/backend/openai.py | 30 ++-- src/tool/command_processor.py | 2 +- src/tool/oi_select_agent.py | 2 +- 5 files changed, 176 insertions(+), 133 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index a62ec2f..7bd4c77 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -12,8 +12,7 @@ from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Static from app.dialogs import ExitDialog, UserConfigDialog -from backend.hermes import HermesChatClient -from backend.openai import OpenAIClient +from backend import HermesChatClient, OpenAIClient from config import Backend, ConfigManager from i18n.manager import _ from log import get_logger @@ -23,7 +22,7 @@ if TYPE_CHECKING: from textual.app import ComposeResult from textual.events import Key - from backend.base import LLMClientBase + from backend import LLMClientBase class SettingsScreen(ModalScreen): @@ -81,6 +80,100 @@ class SettingsScreen(ModalScreen): # 确保操作按钮始终可见 self._ensure_buttons_visible() + @on(Input.Changed, "#base-url, #api-key, #model-input") + def on_config_changed(self) -> None: + """当 Base URL、API Key 或模型改变时更新客户端并验证配置""" + if self.backend == Backend.OPENAI: + # 获取当前模型输入值 + try: + model_input = self.query_one("#model-input", Input) + self.selected_model = model_input.value + except NoMatches: + # 如果模型输入框不存在,跳过 + pass + + self._update_llm_client() + else: # EULERINTELLI + self._update_llm_client() + + # 重新验证配置 + self._schedule_validation() + + @on(Button.Pressed, "#backend-btn") + def toggle_backend(self) -> None: + """切换后端""" + # 切换后端类型 + self.backend = ( + Backend.EULERINTELLI if self.backend == Backend.OPENAI else Backend.OPENAI + ) + + # 更新后端按钮文本 + backend_btn = self.query_one("#backend-btn", Button) + backend_btn.label = self.backend.get_display_name() + + # 更新 URL 和 API Key + self._load_config_inputs() + + # 更新 LLM 客户端 + self._update_llm_client() + + # 替换后端特定的 UI 组件 + self._replace_backend_widgets() + + # 确保按钮可见 + self._ensure_buttons_visible() + + # 切换后端后重新验证配置 + self._schedule_validation() + + @on(Button.Pressed, "#user-config-btn") + def open_user_config(self) -> None: + """打开用户配置对话框""" + dialog = UserConfigDialog(self.config_manager, self.llm_client) + self.app.push_screen(dialog) + + @on(Button.Pressed, "#save-btn") + def save_settings(self) -> None: + """保存设置""" + # 取消所有后台任务 + self._cancel_background_tasks() + + # 检查验证状态 + if not self.is_validated: + return + + # 获取旧配置 + old_backend, old_base, old_key = self._get_old_config() + + # 保存新配置 + base_url, api_key = self._save_new_config() + + # 判断是否需要刷新客户端 + need_refresh = self._should_refresh_client(old_backend, old_base, old_key, base_url, api_key) + + # 刷新客户端 + if need_refresh: + self._refresh_app_client() + + self.app.pop_screen() + + @on(Button.Pressed, "#cancel-btn") + def cancel_settings(self) -> None: + """取消设置""" + self._cancel_background_tasks() + self.app.pop_screen() + + def on_key(self, event: Key) -> None: + """处理键盘事件""" + if event.key == "escape": + self._cancel_background_tasks() + # ESC 键退出设置页面,等效于取消 + self.app.pop_screen() + if event.key == "ctrl+q": + self.app.push_screen(ExitDialog()) + event.prevent_default() + event.stop() + def _create_common_widgets(self) -> list: """创建通用的 UI 组件(所有后端共享)""" return [ @@ -117,8 +210,7 @@ class SettingsScreen(ModalScreen): id="api-key", placeholder=_("API 访问密钥,可选"), ), - classes="settings-option", - ), + classes="settings-option"), ] def _create_backend_widgets(self) -> list: @@ -152,72 +244,15 @@ class SettingsScreen(ModalScreen): ), ] - @on(Input.Changed, "#base-url, #api-key, #model-input") - def on_config_changed(self) -> None: - """当 Base URL、API Key 或模型改变时更新客户端并验证配置""" - if self.backend == Backend.OPENAI: - # 获取当前模型输入值 - try: - model_input = self.query_one("#model-input", Input) - self.selected_model = model_input.value - except NoMatches: - # 如果模型输入框不存在,跳过 - pass - - self._update_llm_client() - else: # EULERINTELLI - self._update_llm_client() - - # 重新验证配置 - self._schedule_validation() - - @on(Button.Pressed, "#backend-btn") - def toggle_backend(self) -> None: - """切换后端""" - # 切换后端类型 - self.backend = ( - Backend.EULERINTELLI if self.backend == Backend.OPENAI else Backend.OPENAI - ) - - # 更新后端按钮文本 - backend_btn = self.query_one("#backend-btn", Button) - backend_btn.label = self.backend.get_display_name() - - # 更新 URL 和 API Key - self._load_config_inputs() - - # 更新 LLM 客户端 - self._update_llm_client() - - # 替换后端特定的 UI 组件 - self._replace_backend_widgets() - - # 确保按钮可见 - self._ensure_buttons_visible() - - # 切换后端后重新验证配置 - self._schedule_validation() - - @on(Button.Pressed, "#user-config-btn") - def open_user_config(self) -> None: - """打开用户配置对话框""" - dialog = UserConfigDialog(self.config_manager, self.llm_client) - self.app.push_screen(dialog) - - @on(Button.Pressed, "#save-btn") - def save_settings(self) -> None: - """保存设置""" - # 取消所有后台任务 + def _cancel_background_tasks(self) -> None: + """取消所有后台任务""" for task in self.background_tasks: if not task.done(): task.cancel() self.background_tasks.clear() - # 检查验证状态 - if not self.is_validated: - return - - # 仅在真正修改了后端/URL/API Key 时才重建 LLM 客户端 + def _get_old_config(self) -> tuple[Backend, str, str]: + """获取旧配置""" old_backend = self.config_manager.get_backend() if old_backend == Backend.OPENAI: @@ -227,67 +262,67 @@ class SettingsScreen(ModalScreen): old_base = self.config_manager.get_eulerintelli_url() old_key = self.config_manager.get_eulerintelli_key() - # 先保存新的配置到 ConfigManager + return old_backend, old_base, old_key + + def _save_new_config(self) -> tuple[str, str]: + """保存新配置并返回 base_url 和 api_key""" self.config_manager.set_backend(self.backend) base_url = self.query_one("#base-url", Input).value api_key = self.query_one("#api-key", Input).value if self.backend == Backend.OPENAI: - # 获取模型输入值 - try: - model_input = self.query_one("#model-input", Input) - self.selected_model = model_input.value.strip() - except NoMatches: - # 如果模型输入框不存在,保持当前选择的模型 - pass - - self.config_manager.set_base_url(base_url) - self.config_manager.set_api_key(api_key) - self.config_manager.set_model(self.selected_model) + self._save_openai_config(base_url, api_key) else: # eulerintelli self.config_manager.set_eulerintelli_url(base_url) self.config_manager.set_eulerintelli_key(api_key) - # 判断是否需要刷新 LLM 客户端(只有后端、URL 或 API Key 发生变化时) - need_refresh = False - if old_backend != self.backend: - need_refresh = True - elif old_base != base_url or old_key != api_key: - # 同一后端,比较 URL 和 Key - need_refresh = True + return base_url, api_key - refresh_method = getattr(self.app, "refresh_llm_client", None) - if need_refresh and refresh_method: - refresh_method() + def _save_openai_config(self, base_url: str, api_key: str) -> None: + """保存 OpenAI 配置""" + # 获取模型输入值 + try: + model_input = self.query_one("#model-input", Input) + self.selected_model = model_input.value.strip() + except NoMatches: + # 如果模型输入框不存在,保持当前选择的模型 + pass + + self.config_manager.set_base_url(base_url) + self.config_manager.set_api_key(api_key) + self.config_manager.set_model(self.selected_model) + + def _should_refresh_client( + self, + old_backend: Backend, + old_base: str, + old_key: str, + new_base: str, + new_key: str, + ) -> bool: + """判断是否需要刷新客户端""" + # 后端类型变化 + if old_backend != self.backend: + return True - self.app.pop_screen() + # 同一后端,URL 或 Key 变化 + if old_base != new_base or old_key != new_key: + return True - @on(Button.Pressed, "#cancel-btn") - def cancel_settings(self) -> None: - """取消设置""" - # 取消所有后台任务 - for task in self.background_tasks: - if not task.done(): - task.cancel() - self.background_tasks.clear() + # OpenAI 后端,检查模型是否变化 + if self.backend == Backend.OPENAI: + old_model = self.config_manager.get_model() + if old_model != self.selected_model: + return True - self.app.pop_screen() + return False - def on_key(self, event: Key) -> None: - """处理键盘事件""" - if event.key == "escape": - # 取消所有后台任务 - for task in self.background_tasks: - if not task.done(): - task.cancel() - self.background_tasks.clear() - # ESC 键退出设置页面,等效于取消 - self.app.pop_screen() - if event.key == "ctrl+q": - self.app.push_screen(ExitDialog()) - event.prevent_default() - event.stop() + def _refresh_app_client(self) -> None: + """刷新应用客户端""" + refresh_method = getattr(self.app, "refresh_llm_client", None) + if refresh_method: + refresh_method() def _schedule_validation(self) -> None: """调度验证任务,带防抖机制""" diff --git a/src/app/tui.py b/src/app/tui.py index 88cb45a..82c7495 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -19,8 +19,7 @@ from app.mcp_widgets import MCPConfirmResult, MCPConfirmWidget, MCPParameterResu from app.settings import SettingsScreen from app.tui_header import OIHeader from app.tui_mcp_handler import TUIMCPEventHandler -from backend.factory import BackendFactory -from backend.hermes import HermesChatClient +from backend import BackendFactory, HermesChatClient, OpenAIClient from backend.hermes.mcp_helpers import ( MCPTags, extract_mcp_tag, @@ -40,7 +39,7 @@ if TYPE_CHECKING: from textual.events import Mount from textual.visual import VisualType - from backend.base import LLMClientBase + from backend import LLMClientBase class ContentChunkParams(NamedTuple): @@ -422,12 +421,21 @@ class IntelligentTerminal(App): return self._llm_client def refresh_llm_client(self) -> None: - """重新创建 LLM 客户端实例,用于后端/URL/API Key 变更后刷新连接""" + """重新创建 LLM 客户端实例,用于后端/URL/API Key/模型 变更后刷新连接""" # 保存当前智能体状态以便恢复 current_agent_id = self.current_agent[0] if self.current_agent else "" + # 保存 OpenAI 客户端的对话历史 + conversation_history = None + if isinstance(self._llm_client, OpenAIClient): + conversation_history = self._llm_client.conversation_history + self._llm_client = BackendFactory.create_client(self.config_manager) + # 恢复 OpenAI 客户端的对话历史 + if conversation_history is not None and isinstance(self._llm_client, OpenAIClient): + self._llm_client.conversation_history = conversation_history + # 恢复智能体状态到新的客户端 if current_agent_id and isinstance(self._llm_client, HermesChatClient): self._llm_client.set_current_agent(current_agent_id) diff --git a/src/backend/openai.py b/src/backend/openai.py index 486aa08..bd8a061 100644 --- a/src/backend/openai.py +++ b/src/backend/openai.py @@ -48,7 +48,7 @@ class OpenAIClient(LLMClientBase): self.logger.debug("OpenAIClient SSL 验证状态: %s", self.verify_ssl) # 添加历史记录管理 - self._conversation_history: list[ChatCompletionMessageParam] = [] + self.conversation_history: list[ChatCompletionMessageParam] = [] # 用于中断的任务跟踪 self._current_task: asyncio.Task | None = None @@ -67,13 +67,13 @@ class OpenAIClient(LLMClientBase): # 添加用户消息到历史记录 user_message: ChatCompletionMessageParam = {"role": "user", "content": prompt} - self._conversation_history.append(user_message) + self.conversation_history.append(user_message) try: # 使用完整的对话历史记录 response = await self.client.chat.completions.create( model=self.model, - messages=self._conversation_history, + messages=self.conversation_history, stream=True, ) @@ -87,7 +87,7 @@ class OpenAIClient(LLMClientBase): duration, model=self.model, stream=True, - history_length=len(self._conversation_history), + history_length=len(self.conversation_history), ) # 收集助手的完整回复 @@ -102,11 +102,11 @@ class OpenAIClient(LLMClientBase): self.logger.info("OpenAI 流式响应被中断") # 如果被中断,移除刚添加的用户消息 if ( - self._conversation_history - and len(self._conversation_history) > 0 - and self._conversation_history[-1].get("content") == prompt + self.conversation_history + and len(self.conversation_history) > 0 + and self.conversation_history[-1].get("content") == prompt ): - self._conversation_history.pop() + self.conversation_history.pop() raise # 将助手回复添加到历史记录 @@ -115,8 +115,8 @@ class OpenAIClient(LLMClientBase): "role": "assistant", "content": assistant_response, } - self._conversation_history.append(assistant_message) - self.logger.info("对话历史记录已更新,当前消息数: %d", len(self._conversation_history)) + self.conversation_history.append(assistant_message) + self.logger.info("对话历史记录已更新,当前消息数: %d", len(self.conversation_history)) except asyncio.CancelledError: # 重新抛出取消异常 @@ -124,11 +124,11 @@ class OpenAIClient(LLMClientBase): except OpenAIError as e: # 如果请求失败,移除刚添加的用户消息 if ( - self._conversation_history - and len(self._conversation_history) > 0 - and self._conversation_history[-1].get("content") == prompt + self.conversation_history + and len(self.conversation_history) > 0 + and self.conversation_history[-1].get("content") == prompt ): - self._conversation_history.pop() + self.conversation_history.pop() duration = time.time() - start_time log_exception(self.logger, "OpenAI 流式聊天 API 请求失败", e) @@ -174,7 +174,7 @@ class OpenAIClient(LLMClientBase): 清空历史记录,开始新的对话会话。 """ - self._conversation_history.clear() + self.conversation_history.clear() self.logger.info("OpenAI 客户端对话历史记录已重置") async def get_available_models(self) -> list[ModelInfo]: diff --git a/src/tool/command_processor.py b/src/tool/command_processor.py index bfa91be..5c7b8d7 100644 --- a/src/tool/command_processor.py +++ b/src/tool/command_processor.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: import logging from collections.abc import AsyncGenerator - from backend.base import LLMClientBase + from backend import LLMClientBase # 定义危险命令黑名单 BLACKLIST = ["rm", "sudo", "shutdown", "reboot", "mkfs"] diff --git a/src/tool/oi_select_agent.py b/src/tool/oi_select_agent.py index c13023b..70e1cba 100644 --- a/src/tool/oi_select_agent.py +++ b/src/tool/oi_select_agent.py @@ -10,7 +10,7 @@ from textual.app import App, ComposeResult from textual.containers import Container from app.dialogs import AgentSelectionDialog -from backend.factory import BackendFactory +from backend import BackendFactory from config.manager import ConfigManager from config.model import Backend from i18n.manager import _ -- Gitee From 0794e8685f1c8e6fe691839b4272d56e7eaff303 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Fri, 24 Oct 2025 17:29:50 +0800 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=BE=BD=E7=AB=A0=E5=92=8C=E7=94=A8=E6=88=B7=E5=90=8D?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/css/styles.tcss | 40 +++++++++++++- src/app/dialogs/user.py | 82 ++++++++++++++--------------- src/backend/hermes/client.py | 2 +- src/backend/hermes/services/user.py | 2 +- 4 files changed, 80 insertions(+), 46 deletions(-) diff --git a/src/app/css/styles.tcss b/src/app/css/styles.tcss index c85b5d2..5c85105 100644 --- a/src/app/css/styles.tcss +++ b/src/app/css/styles.tcss @@ -60,7 +60,7 @@ SettingsScreen { #settings-screen { align: center middle; - width: 80%; + width: 85%; height: 95%; color: #ffffff; } @@ -95,12 +95,48 @@ SettingsScreen { margin-top: 1; } -/* 设置值样式 */ +/* 设置值样式 (可编辑类型) */ .settings-input { width: 80%; content-align: left middle; } +/* 设置值容器(用于包含值和徽章) */ +.settings-value-container { + margin-left: 1; + background: $surface; + width: 80%; + height: 3; + align: left middle; +} + +/* 设置值样式 (只读类型) */ +.settings-value { + margin-left: 1; + width: auto; + height: 3; +} + +/* 用户类型徽章样式 */ +.user-type-badge { + width: 20%; + height: 3; + text-style: bold; + margin-top: 1; + padding: 0 1; + dock: right; +} + +/* 管理员徽章颜色 */ +.user-type-admin { + color: $warning; +} + +/* 普通用户徽章颜色 */ +.user-type-user { + color: $primary; +} + /* 设置按钮样式 */ .settings-button { content-align: left middle; diff --git a/src/app/dialogs/user.py b/src/app/dialogs/user.py index cdcadbf..8a99da7 100644 --- a/src/app/dialogs/user.py +++ b/src/app/dialogs/user.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, ClassVar from textual import on from textual.containers import Container, Horizontal from textual.screen import ModalScreen -from textual.widgets import Button, Input, Label, Static, TabbedContent, TabPane +from textual.widgets import Button, Label, Static, TabbedContent, TabPane from backend import HermesChatClient from backend.models import LLMType, ModelInfo @@ -132,20 +132,38 @@ class UserConfigDialog(ModalScreen): # 先挂载表单容器到 tab_pane tab_pane.mount(form_container) - # 用户名输入 - form_container.mount( - Horizontal( - Label(_("用户名:"), classes="settings-label"), - Input( - placeholder=_("请输入用户名"), - value=self.username, - id="username-input", - classes="settings-input", - ), - classes="settings-option", + # 用户名和用户类型显示(用户类型在右侧) + user_row = Horizontal(classes="settings-option") + form_container.mount(user_row) + + # 左侧:标签 + user_row.mount(Label(_("用户名:"), classes="settings-label")) + + # 右侧:值容器(包含用户名和徽章) + value_container = Horizontal(classes="settings-value-container") + user_row.mount(value_container) + + # 用户名 + value_container.mount( + Static( + self.username if self.username else _("未登录"), + id="username-display", + classes="settings-value", ), ) + # 用户类型徽章 + if self.username: + user_type_text = _("管理员") if self.is_admin else _("普通用户") + user_type_class = "user-type-admin" if self.is_admin else "user-type-user" + value_container.mount( + Static( + user_type_text, + id="user-type-display", + classes=f"user-type-badge {user_type_class}", + ), + ) + # MCP 工具授权设置(仅当支持时显示) if self.mcp_status_loaded: form_container.mount( @@ -165,10 +183,7 @@ class UserConfigDialog(ModalScreen): tab_pane.mount(button_container) # 创建按钮并挂载到按钮容器 - save_btn = Button(_("保存"), id="save-user-settings-btn", variant="primary") - # 如果用户名为空,禁用保存按钮 - save_btn.disabled = not self.username - button_container.mount(save_btn) + button_container.mount(Button(_("保存"), id="save-user-settings-btn", variant="primary")) button_container.mount(Button(_("取消"), id="cancel-general-btn", variant="default")) def _render_chat_llm_tab(self) -> None: @@ -310,35 +325,18 @@ class UserConfigDialog(ModalScreen): elif tab_id == "llm-tab": self.current_tab = "llm" - @on(Input.Changed, "#username-input") - def on_username_changed(self, event: Input.Changed) -> None: - """用户名输入框内容变化时,更新保存按钮的状态""" - username = event.value.strip() - # 同步更新本地状态 - self.username = username - # 更新保存按钮的禁用状态 - save_btn = self.query_one("#save-user-settings-btn", Button) - save_btn.disabled = not username - @on(Button.Pressed, "#save-user-settings-btn") async def on_save_user_settings(self) -> None: - """保存用户设置(用户名和自动执行状态)""" - username_input = self.query_one("#username-input", Input) - new_username = username_input.value.strip() - - if not new_username: - return - - # 调用后端 API 保存用户信息 - if hasattr(self.llm_client, "update_user_info"): - success = await self.llm_client.update_user_info( # type: ignore[attr-defined] - user_name=new_username, + """保存用户设置(MCP 自动执行状态)""" + # 调用后端 API 保存 MCP 自动执行状态 + if isinstance(self.llm_client, HermesChatClient): + await self.llm_client.update_user_info( + user_name=self.username, auto_execute=self.auto_execute_status, ) - if success: - # 更新本地状态 - self.username = new_username + # 保存成功后关闭对话框 + self.app.pop_screen() @on(Button.Pressed, "#cancel-general-btn") def on_cancel_general(self) -> None: @@ -403,8 +401,8 @@ class UserConfigDialog(ModalScreen): # 保存已激活的模型到配置 self.config_manager.set_llm_chat_model(self.activated_chat_model) self.saved_chat_model = self.activated_chat_model - # 刷新显示以更新已保存状态 - self._render_chat_llm_tab() + # 关闭对话框 + self.app.pop_screen() def action_cancel(self) -> None: """取消并关闭对话框""" diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index ca70820..89e1326 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -143,7 +143,7 @@ class HermesChatClient(LLMClientBase): self.logger.warning("用户信息加载失败") return False - def get_user_id(self) -> int | None: + def get_user_id(self) -> int | str | None: """获取用户ID(从内存缓存)""" if self._user_info is None: return None diff --git a/src/backend/hermes/services/user.py b/src/backend/hermes/services/user.py index 6d20c43..77fdf03 100644 --- a/src/backend/hermes/services/user.py +++ b/src/backend/hermes/services/user.py @@ -37,7 +37,7 @@ class HermesUserManager: dict[str, Any] | None: 用户信息字典,如果请求失败返回 None 返回数据格式: { - "id": int, # 用户ID + "id": int | str, # 用户ID "userName": str, # 用户名 "isAdmin": bool, # 是否管理员 "autoExecute": bool # 是否自动执行 -- Gitee From 67b169635bc119bb4d723253316665ceecea741b Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Sat, 25 Oct 2025 11:19:42 +0800 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/dialogs/user.py | 1 - src/backend/hermes/client.py | 15 +++++---------- src/backend/hermes/services/user.py | 25 +++++++++++-------------- 3 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/app/dialogs/user.py b/src/app/dialogs/user.py index 8a99da7..fcda486 100644 --- a/src/app/dialogs/user.py +++ b/src/app/dialogs/user.py @@ -331,7 +331,6 @@ class UserConfigDialog(ModalScreen): # 调用后端 API 保存 MCP 自动执行状态 if isinstance(self.llm_client, HermesChatClient): await self.llm_client.update_user_info( - user_name=self.username, auto_execute=self.auto_execute_status, ) diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index 89e1326..74911bb 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -60,7 +60,6 @@ class HermesChatClient(LLMClientBase): # 用户信息缓存(在初始化时加载) self._user_info: dict[str, Any] | None = None - self._user_info_loaded: bool = False self.logger.info("Hermes 客户端初始化成功 - URL: %s", base_url) @@ -125,17 +124,16 @@ class HermesChatClient(LLMClientBase): bool: 是否成功加载用户信息 """ - if self._user_info_loaded: + if self._user_info is not None: return True self.logger.info("开始加载用户信息...") self._user_info = await self.user_manager.get_user_info() if self._user_info is not None: - self._user_info_loaded = True self.logger.info( "用户信息加载成功 - ID: %s, 用户名: %s", - self._user_info.get("id"), + self._user_info.get("userId"), self._user_info.get("userName"), ) return True @@ -143,11 +141,11 @@ class HermesChatClient(LLMClientBase): self.logger.warning("用户信息加载失败") return False - def get_user_id(self) -> int | str | None: + def get_user_id(self) -> str | None: """获取用户ID(从内存缓存)""" if self._user_info is None: return None - return self._user_info.get("id") + return self._user_info.get("userId") def get_user_name(self) -> str: """获取用户名(从内存缓存)""" @@ -167,14 +165,13 @@ class HermesChatClient(LLMClientBase): return False return self._user_info.get("isAdmin", False) - async def update_user_info(self, *, user_name: str, auto_execute: bool) -> bool: + async def update_user_info(self, *, auto_execute: bool) -> bool: """ 更新用户信息 更新成功后会自动更新内存中的缓存。 Args: - user_name: 用户名 auto_execute: 是否启用自动执行 Returns: @@ -182,13 +179,11 @@ class HermesChatClient(LLMClientBase): """ success = await self.user_manager.update_user_info( - user_name=user_name, auto_execute=auto_execute, ) if success and self._user_info is not None: # 更新内存缓存 - self._user_info["userName"] = user_name self._user_info["autoExecute"] = auto_execute self.logger.info("已更新内存中的用户信息缓存") diff --git a/src/backend/hermes/services/user.py b/src/backend/hermes/services/user.py index 77fdf03..9690180 100644 --- a/src/backend/hermes/services/user.py +++ b/src/backend/hermes/services/user.py @@ -31,16 +31,17 @@ class HermesUserManager: 获取用户信息 通过调用 GET /api/user 接口获取当前用户信息, - 包括用户标识、用户名、权限、自动执行设置等。 + 包括用户标识、用户名、权限、个人令牌、自动执行设置等。 Returns: dict[str, Any] | None: 用户信息字典,如果请求失败返回 None 返回数据格式: { - "id": int | str, # 用户ID - "userName": str, # 用户名 - "isAdmin": bool, # 是否管理员 - "autoExecute": bool # 是否自动执行 + "userId": str, # 用户ID + "userName": str, # 用户名 + "isAdmin": bool, # 是否管理员 + "personalToken": str, # 个人令牌 + "autoExecute": bool # 是否自动执行 } """ @@ -84,7 +85,7 @@ class HermesUserManager: user_info = data["result"] self.logger.info( "获取用户信息成功 - 用户ID: %s, 用户名: %s, 自动执行: %s, 管理员: %s", - user_info.get("id", "未知"), + user_info.get("userId", "未知"), user_info.get("userName", "未知"), user_info.get("autoExecute", False), user_info.get("isAdmin", False), @@ -107,15 +108,13 @@ class HermesUserManager: else: return user_info - async def update_user_info(self, *, user_name: str, auto_execute: bool = False) -> bool: + async def update_user_info(self, *, auto_execute: bool = False) -> bool: """ 更新用户信息 - 通过调用 POST /api/user 接口更新当前用户的信息。 - 同时更新用户名和自动执行设置。 + 通过调用 POST /api/user 接口更新当前用户的自动执行设置。 Args: - user_name: 用户名 auto_execute: 是否启用自动执行 Returns: @@ -124,8 +123,7 @@ class HermesUserManager: """ start_time = time.time() self.logger.info( - "开始请求 Hermes 用户信息更新 API - user_name: %s, auto_execute: %s", - user_name, + "开始请求 Hermes 用户信息更新 API - auto_execute: %s", auto_execute, ) @@ -140,7 +138,6 @@ class HermesUserManager: # 构建请求体 request_data: dict[str, Any] = { - "userName": user_name, "autoExecute": auto_execute, } @@ -198,7 +195,7 @@ class HermesUserManager: return False # 检查必要字段是否存在 - required_fields = ["id", "userName", "isAdmin", "autoExecute"] + required_fields = ["userId", "userName", "isAdmin", "autoExecute"] for field in required_fields: if field not in result: self.logger.warning("用户信息缺少必要字段: %s", field) -- Gitee From cdff4ad29d8274da0ec16e4a8b174bcc8860a680 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Sat, 25 Oct 2025 11:22:58 +0800 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=E8=87=AA=E5=8A=A8=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E4=B8=AD=20person?= =?UTF-8?q?alToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/tui.py | 35 ++++++++++++++++++++++++++++++++++- src/backend/hermes/client.py | 12 ++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/app/tui.py b/src/app/tui.py index 82c7495..dfed36b 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -445,7 +445,8 @@ class IntelligentTerminal(App): mcp_handler = TUIMCPEventHandler(self, self._llm_client) self._llm_client.set_mcp_handler(mcp_handler) - task = asyncio.create_task(self._llm_client.ensure_user_info_loaded()) + # 创建异步任务加载用户信息并同步 token + task = asyncio.create_task(self._load_user_info_and_sync_token()) self.background_tasks.add(task) task.add_done_callback(self.background_tasks.discard) @@ -1027,6 +1028,38 @@ class IntelligentTerminal(App): # 等待一个小的延迟,确保UI有时间更新 await asyncio.sleep(0.01) + async def _load_user_info_and_sync_token(self) -> None: + """加载用户信息并同步 personalToken 到配置""" + if not isinstance(self._llm_client, HermesChatClient): + return + + try: + # 加载用户信息 + success = await self._llm_client.ensure_user_info_loaded() + if not success: + self.logger.warning("加载用户信息失败,无法同步 personalToken") + return + + # 获取 personalToken + personal_token = self._llm_client.get_personal_token() + if not personal_token: + self.logger.info("服务器未返回 personalToken,跳过同步") + return + + # 获取当前配置中的 personalToken + current_token = self.config_manager.get_eulerintelli_key() + + # 如果 personalToken 不一致,更新配置 + if personal_token != current_token: + self.logger.info("检测到 personalToken 变更,正在同步到配置...") + self.config_manager.set_eulerintelli_key(personal_token) + self.logger.info("PersonalToken 已同步到配置文件") + else: + self.logger.info("PersonalToken 与配置一致,无需同步") + + except (OSError, ValueError, RuntimeError) as e: + log_exception(self.logger, "同步 personalToken 时发生错误", e) + async def _cleanup_llm_client(self) -> None: """异步清理 LLM 客户端""" if self._llm_client is not None: diff --git a/src/backend/hermes/client.py b/src/backend/hermes/client.py index 74911bb..7672aec 100644 --- a/src/backend/hermes/client.py +++ b/src/backend/hermes/client.py @@ -141,6 +141,18 @@ class HermesChatClient(LLMClientBase): self.logger.warning("用户信息加载失败") return False + def get_personal_token(self) -> str: + """ + 获取个人令牌(从内存缓存) + + Returns: + str: 个人令牌,如果未加载则返回空字符串 + + """ + if self._user_info is None: + return "" + return self._user_info.get("personalToken", "") + def get_user_id(self) -> str | None: """获取用户ID(从内存缓存)""" if self._user_info is None: -- Gitee From 1ebdebaf225c0174e926e841713acc3cf9848f63 Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Sat, 25 Oct 2025 11:49:04 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E6=9B=B4=E6=96=B0=E6=99=BA=E8=83=BD?= =?UTF-8?q?=E4=BD=93=E5=90=8D=E7=A7=B0=E4=B8=BA=E5=8F=AF=E6=9C=AC=E5=9C=B0?= =?UTF-8?q?=E5=8C=96=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/tui.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/tui.py b/src/app/tui.py index dfed36b..9e7af56 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -1086,7 +1086,7 @@ class IntelligentTerminal(App): llm_client = self.get_llm_client() # 构建智能体列表 - 默认第一项为"智能问答"(无智能体) - agent_list = [("", "智能问答")] + agent_list = [("", _("智能问答"))] # 尝试获取可用智能体 if hasattr(llm_client, "get_available_agents"): @@ -1112,7 +1112,7 @@ class IntelligentTerminal(App): except (OSError, ValueError, RuntimeError) as e: log_exception(self.logger, "显示智能体选择对话框失败", e) # 即使出错也显示默认选项 - agent_list = [("", "智能问答")] + agent_list = [("", _("智能问答"))] try: llm_client = self.get_llm_client() await self._display_agent_dialog(agent_list, llm_client) @@ -1306,7 +1306,7 @@ class IntelligentTerminal(App): # 这里先返回 ID 和 ID 作为临时方案,后续在智能体列表加载后更新名称 return (default_app, default_app) # 如果没有配置默认智能体,使用智能问答 - return ("", "智能问答") + return ("", _("智能问答")) def _reinitialize_agent_state(self) -> None: """重新初始化智能体状态,用于后端切换时""" @@ -1450,7 +1450,7 @@ class IntelligentTerminal(App): llm_client = self.get_llm_client() if hasattr(llm_client, "get_available_agents"): available_agents = await llm_client.get_available_agents() # type: ignore[attr-defined] - app_id, _ = self.current_agent + app_id, _name = self.current_agent # 查找匹配的智能体 agent_found = False @@ -1468,7 +1468,7 @@ class IntelligentTerminal(App): if not agent_found and app_id: self.logger.warning("配置的默认智能体 '%s' 不存在,回退到智能问答并清理配置", app_id) # 回退到智能问答 - self.current_agent = ("", "智能问答") + self.current_agent = ("", _("智能问答")) # 清理配置中的无效ID self.config_manager.set_default_app("") # 确保客户端也切换到智能问答 -- Gitee From 4ceb8a9d1f8a749fc84f42459ad3bcc5cedf587f Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Sat, 25 Oct 2025 15:14:48 +0800 Subject: [PATCH 8/9] chore: ignore uv.lock Signed-off-by: Hongyu Shi --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 95f1987..0be3519 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,9 @@ .env +# uv +uv.lock + # ide .idea -- Gitee From 8b46de8b50cc927315f2002e637567ad1c2c0e0a Mon Sep 17 00:00:00 2001 From: Hongyu Shi Date: Mon, 27 Oct 2025 11:01:10 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=BF=85?= =?UTF-8?q?=E8=A6=81=E7=9A=84=20LLM=20=E5=AE=A2=E6=88=B7=E7=AB=AF=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=80=BB=E8=BE=91=EF=BC=8C=E4=BC=98=E5=8C=96=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BF=A1=E6=81=AF=E5=8A=A0=E8=BD=BD=E5=92=8C=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Hongyu Shi --- src/app/settings.py | 44 ++------------------------------------------ src/app/tui.py | 30 +++++++++++++----------------- 2 files changed, 15 insertions(+), 59 deletions(-) diff --git a/src/app/settings.py b/src/app/settings.py index 7bd4c77..0510c6a 100644 --- a/src/app/settings.py +++ b/src/app/settings.py @@ -12,7 +12,6 @@ from textual.screen import ModalScreen from textual.widgets import Button, Input, Label, Static from app.dialogs import ExitDialog, UserConfigDialog -from backend import HermesChatClient, OpenAIClient from config import Backend, ConfigManager from i18n.manager import _ from log import get_logger @@ -82,7 +81,7 @@ class SettingsScreen(ModalScreen): @on(Input.Changed, "#base-url, #api-key, #model-input") def on_config_changed(self) -> None: - """当 Base URL、API Key 或模型改变时更新客户端并验证配置""" + """当 Base URL、API Key 或模型改变时验证配置""" if self.backend == Backend.OPENAI: # 获取当前模型输入值 try: @@ -92,10 +91,6 @@ class SettingsScreen(ModalScreen): # 如果模型输入框不存在,跳过 pass - self._update_llm_client() - else: # EULERINTELLI - self._update_llm_client() - # 重新验证配置 self._schedule_validation() @@ -111,12 +106,9 @@ class SettingsScreen(ModalScreen): backend_btn = self.query_one("#backend-btn", Button) backend_btn.label = self.backend.get_display_name() - # 更新 URL 和 API Key + # 更新配置输入框的值 self._load_config_inputs() - # 更新 LLM 客户端 - self._update_llm_client() - # 替换后端特定的 UI 组件 self._replace_backend_widgets() @@ -463,35 +455,3 @@ class SettingsScreen(ModalScreen): 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) - api_key_input = self.query_one("#api-key", Input) - - # 保存当前智能体状态(如果是Hermes客户端) - current_agent_id = "" - if isinstance(self.llm_client, HermesChatClient): - current_agent_id = getattr(self.llm_client, "current_agent_id", "") - - if self.backend == Backend.OPENAI: - # 获取模型输入值,如果输入框不存在则使用当前选择的模型 - try: - model_input = self.query_one("#model-input", Input) - model = model_input.value.strip() - except NoMatches: - model = self.selected_model - - self.llm_client = OpenAIClient( - base_url=base_url_input.value, - model=model, - api_key=api_key_input.value, - ) - else: # EULERINTELLI - self.llm_client = HermesChatClient( - base_url=base_url_input.value, - auth_token=api_key_input.value, - ) - # 恢复智能体状态 - if current_agent_id: - self.llm_client.set_current_agent(current_agent_id) diff --git a/src/app/tui.py b/src/app/tui.py index 9e7af56..dbc6ba4 100644 --- a/src/app/tui.py +++ b/src/app/tui.py @@ -32,7 +32,7 @@ from config.model import Backend from i18n.manager import _ from log.manager import get_logger, log_exception from tool.command_processor import process_command -from tool.validators import APIValidator, validate_oi_connection +from tool.validators import APIValidator if TYPE_CHECKING: from textual.events import Key as KeyEvent @@ -445,14 +445,11 @@ class IntelligentTerminal(App): mcp_handler = TUIMCPEventHandler(self, self._llm_client) self._llm_client.set_mcp_handler(mcp_handler) - # 创建异步任务加载用户信息并同步 token - task = asyncio.create_task(self._load_user_info_and_sync_token()) + # 创建异步任务加载用户信息并同步 personalToken + task = asyncio.create_task(self._ensure_hermes_user_info()) self.background_tasks.add(task) task.add_done_callback(self.background_tasks.discard) - # 后端切换时重新初始化智能体状态 - self._reinitialize_agent_state() - def exit(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 """退出应用前取消所有后台任务""" # 取消所有正在运行的后台任务 @@ -1028,8 +1025,8 @@ class IntelligentTerminal(App): # 等待一个小的延迟,确保UI有时间更新 await asyncio.sleep(0.01) - async def _load_user_info_and_sync_token(self) -> None: - """加载用户信息并同步 personalToken 到配置""" + async def _ensure_hermes_user_info(self) -> None: + """确保 Hermes 用户信息已加载并同步 personalToken 到配置""" if not isinstance(self._llm_client, HermesChatClient): return @@ -1037,7 +1034,7 @@ class IntelligentTerminal(App): # 加载用户信息 success = await self._llm_client.ensure_user_info_loaded() if not success: - self.logger.warning("加载用户信息失败,无法同步 personalToken") + self.logger.warning("加载用户信息失败") return # 获取 personalToken @@ -1058,7 +1055,7 @@ class IntelligentTerminal(App): self.logger.info("PersonalToken 与配置一致,无需同步") except (OSError, ValueError, RuntimeError) as e: - log_exception(self.logger, "同步 personalToken 时发生错误", e) + log_exception(self.logger, "加载用户信息或同步 personalToken 时发生错误", e) async def _cleanup_llm_client(self) -> None: """异步清理 LLM 客户端""" @@ -1346,10 +1343,9 @@ class IntelligentTerminal(App): async def _validate_backend_configuration(self, backend: Backend) -> bool: """验证后端配置""" try: - validator = APIValidator() - if backend == Backend.OPENAI: # 验证 OpenAI 配置 + validator = APIValidator() base_url = self.config_manager.get_base_url() api_key = self.config_manager.get_api_key() model = self.config_manager.get_model() @@ -1362,11 +1358,11 @@ class IntelligentTerminal(App): 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 + # 验证 Hermes 配置 + llm_client = self.get_llm_client() + if isinstance(llm_client, HermesChatClient): + return await llm_client.ensure_user_info_loaded() + return False except Exception: self.logger.exception("验证后端配置时发生错误") -- Gitee