From 82903a3fa76124fad7513ecf6c0720a727345f62 Mon Sep 17 00:00:00 2001 From: JianFeeeee <109188060+JianFeeeee@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:41:19 +0800 Subject: [PATCH] fix: multiple UI and API issues - Fix tool calls format for DeepSeek API (add type field) - Fix config event handler name - Fix message history display (preserve all messages) - Fix right panel width and scrolling - Add async config update to prevent UI freeze - Add prompt caching with singleton pattern - Update build scripts with complete hidden imports --- build/build_macos.sh | 49 +++++++++++++++++++-- build/build_windows.bat | 44 +++++++++++++++++-- core/__init__.py | 55 +++++++++++++++++------- core/prompts/prompt_manager.py | 24 +++++++++-- ui/app.py | 78 +++++++++++++++++++++++++--------- ui/styles/app.css | 7 ++- ui/widgets/right_panel.py | 2 +- ui/widgets/status_bar.py | 18 +++++++- 8 files changed, 223 insertions(+), 54 deletions(-) diff --git a/build/build_macos.sh b/build/build_macos.sh index 475e1e6..34c0381 100644 --- a/build/build_macos.sh +++ b/build/build_macos.sh @@ -1,22 +1,63 @@ #!/bin/bash set -e -echo "Building macOS binary..." +echo "===== Building TrulyMEM for macOS =====" + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" +cd "$PROJECT_ROOT" + +echo "Project root: $PROJECT_ROOT" if ! command -v python3 &> /dev/null; then echo "Error: python3 not found" exit 1 fi +echo "Installing dependencies..." pip3 install -r requirements.txt -python3 -m PyInstaller --clean --onefile --console \ +echo "Cleaning previous builds..." +rm -rf build/dist build/__pycache__ 2>/dev/null || true + +echo "Running PyInstaller..." +python3 -m PyInstaller trulymem_entry.py \ + --clean \ + --onefile \ + --console \ + --name TrulyMEM \ --add-data "ui/styles:ui/styles" \ --add-data "core/prompts/templates:core/prompts/templates" \ --hidden-import textual \ + --hidden-import textual.app \ + --hidden-import textual.widgets \ + --hidden-import textual.css \ --hidden-import openai \ + --hidden-import openai._client \ --hidden-import neo4j \ + --hidden-import sqlite3 \ + --hidden-import core \ + --hidden-import core.embedded_db \ + --hidden-import core.graph_client \ + --hidden-import core.tool_executor \ + --hidden-import core.tool_limiter \ + --hidden-import core.tools \ + --hidden-import core.tools.memory_tools \ + --hidden-import core.prompts \ + --hidden-import core.prompts.prompt_manager \ + --hidden-import ui \ + --hidden-import ui.app \ + --hidden-import ui.models \ + --hidden-import ui.models.message \ + --hidden-import ui.models.config \ + --hidden-import ui.models.log_entry \ + --hidden-import ui.widgets \ + --hidden-import ui.handlers \ + --hidden-import ui.services \ + --hidden-import ui.services.config_manager \ --collect-all textual \ - trulymem_entry.py + --noconfirm -echo "Done! Binary: dist/TrulyMEM" \ No newline at end of file +echo "===== Build Complete =====" +echo "Binary: dist/TrulyMEM" +ls -la dist/ \ No newline at end of file diff --git a/build/build_windows.bat b/build/build_windows.bat index 13657dc..40733a2 100644 --- a/build/build_windows.bat +++ b/build/build_windows.bat @@ -1,5 +1,9 @@ @echo off -echo Building Windows binary... +echo ===== Building TrulyMEM for Windows ===== + +REM 切换到脚本所在目录的上一级目录(项目根目录) +cd /d "%~dp0.." +echo Project root: %CD% python --version >nul 2>&1 if errorlevel 1 ( @@ -7,16 +11,48 @@ if errorlevel 1 ( exit /b 1 ) +echo Installing dependencies... pip install -r requirements.txt -python -m PyInstaller --clean --onefile --console ^ +echo Running PyInstaller... +python -m PyInstaller trulymem_entry.py ^ + --clean ^ + --onefile ^ + --console ^ + --name TrulyMEM ^ --add-data "ui/styles;ui/styles" ^ --add-data "core/prompts/templates;core/prompts/templates" ^ --hidden-import textual ^ + --hidden-import textual.app ^ + --hidden-import textual.widgets ^ + --hidden-import textual.css ^ --hidden-import openai ^ + --hidden-import openai._client ^ --hidden-import neo4j ^ + --hidden-import sqlite3 ^ + --hidden-import core ^ + --hidden-import core.embedded_db ^ + --hidden-import core.graph_client ^ + --hidden-import core.tool_executor ^ + --hidden-import core.tool_limiter ^ + --hidden-import core.tools ^ + --hidden-import core.tools.memory_tools ^ + --hidden-import core.prompts ^ + --hidden-import core.prompts.prompt_manager ^ + --hidden-import ui ^ + --hidden-import ui.app ^ + --hidden-import ui.models ^ + --hidden-import ui.models.message ^ + --hidden-import ui.models.config ^ + --hidden-import ui.models.log_entry ^ + --hidden-import ui.widgets ^ + --hidden-import ui.handlers ^ + --hidden-import ui.services ^ + --hidden-import ui.services.config_manager ^ --collect-all textual ^ - trulymem_entry.py + --noconfirm -echo Done! Binary: dist\TrulyMEM.exe +echo ===== Build Complete ===== +echo Binary: dist\TrulyMEM.exe +dir dist\TrulyMEM.exe pause \ No newline at end of file diff --git a/core/__init__.py b/core/__init__.py index 7e7979d..9f6dee2 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -96,27 +96,50 @@ class BackendServer: messages = [{"role": "user", "content": user_input}] response = self._client.send_message_with_history(messages) message = response.choices[0].message - content = message.content or "(无��复)" + content = message.content or "(无回复)" tool_calls = [] while message.tool_calls: - args = {} - try: - import json - args = json.loads(message.tool_calls[0].function.arguments) - except: - pass + # 处理所有工具调用 + tool_results = [] - allowed, reason = limiter.can_call(message.tool_calls[0].function.name, args) - if not allowed: - tool_calls.append({"name": message.tool_calls[0].function.name, "result": f"工具调用被拒绝: {reason}"}) - continue + for tool_call in message.tool_calls: + args = {} + try: + import json + args = json.loads(tool_call.function.arguments) + except: + pass + + allowed, reason = limiter.can_call(tool_call.function.name, args) + if not allowed: + tool_calls.append({"name": tool_call.function.name, "result": f"工具调用被拒绝: {reason}"}) + tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": f"工具调用被拒绝: {reason}"}) + continue + + limiter.record_call(tool_call.function.name, args) + result = execute_tool(self._graph, tool_call.function.name, args) + tool_calls.append({"name": tool_call.function.name, "result": result}) + tool_results.append({"role": "tool", "tool_call_id": tool_call.id, "content": result}) + + # 添加 assistant 消息(包含完整的 tool_calls,必须有 type 字段) + assistant_msg = { + "role": "assistant", + "content": message.content or "", + "tool_calls": [{ + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments + } + } for tc in message.tool_calls] + } + messages.append(assistant_msg) + + # 添加工具结果 + messages.extend(tool_results) - limiter.record_call(message.tool_calls[0].function.name, args) - result = execute_tool(self._graph, message.tool_calls[0].function.name, args) - tool_calls.append({"name": message.tool_calls[0].function.name, "result": result}) - messages.append({"role": "assistant", "tool_calls": [{"id": "1", "function": {"name": message.tool_calls[0].function.name, "arguments": message.tool_calls[0].function.arguments}}]}) - messages.append({"role": "tool", "tool_call_id": "1", "content": result}) response = self._client.send_message_with_history(messages) message = response.choices[0].message content = message.content or content diff --git a/core/prompts/prompt_manager.py b/core/prompts/prompt_manager.py index c225bff..fb19377 100644 --- a/core/prompts/prompt_manager.py +++ b/core/prompts/prompt_manager.py @@ -7,17 +7,33 @@ from pathlib import Path class PromptManager: """提示词管理器""" + _instance = None + _cached_prompt = None + + def __new__(cls): + """单例模式,避免重复加载""" + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + def __init__(self): - self.prompts_dir = Path(__file__).parent / "templates" + if not hasattr(self, '_initialized'): + self.prompts_dir = Path(__file__).parent / "templates" + self._initialized = True def get_system_prompt(self) -> str: - """获取系统提示词""" + """获取系统提示词(带缓存)""" + if PromptManager._cached_prompt is not None: + return PromptManager._cached_prompt + prompt_file = self.prompts_dir / "system_prompt.md" if prompt_file.exists(): with open(prompt_file, "r", encoding="utf-8") as f: - return f.read() + PromptManager._cached_prompt = f.read() else: - return self._build_default_prompt() + PromptManager._cached_prompt = self._build_default_prompt() + + return PromptManager._cached_prompt def _build_default_prompt(self) -> str: """构建默认提示词(精简版)""" diff --git a/ui/app.py b/ui/app.py index 29b0c1c..77e92f5 100644 --- a/ui/app.py +++ b/ui/app.py @@ -26,6 +26,7 @@ class GraphMemoryApp(App): super().__init__(**kwargs) self._backend_server = backend_server self._backend_client = BackendClient(backend_server) if backend_server else None + self._api_configured = False def compose(self) -> ComposeResult: from .widgets.left_panel import LeftPanel @@ -36,21 +37,26 @@ class GraphMemoryApp(App): yield StatusBar() def on_mount(self) -> None: + from .widgets.status_bar import StatusBar + status_bar = self.query_one(StatusBar) + if not self._backend_server: from .widgets.message_history import MessageHistory history = self.query_one(MessageHistory) error = Message(role="assistant", content="后端未初始化") history.add_message(error) + status_bar.set_api_status(False) return status = self._backend_client.get_status() - api_configured = status.get("config", {}).get("api_key", "") != "" + self._api_configured = status.get("config", {}).get("api_key", "") != "" + status_bar.set_api_status(self._api_configured) from .widgets.message_history import MessageHistory history = self.query_one(MessageHistory) welcome = Message( role="assistant", - content=f"系统就绪\nAPI Key: {'已配置' if api_configured else '未配置'}\n\n输入消息开始对话" + content=f"系统就绪\nAPI Key: {'已配置' if self._api_configured else '未配置'}\n\n输入消息开始对话" ) history.add_message(welcome) @@ -78,48 +84,82 @@ class GraphMemoryApp(App): def on_input_box_send_message(self, event) -> None: if not self._backend_client: + self.notify("后端未初始化", title="错误", severity="error") + return + + if not self._api_configured: + self.notify("请先配置 API Key (按 F2 打开侧边栏)", title="提示", severity="warning") return user_input = event.content from .widgets.message_history import MessageHistory + from .widgets.status_bar import StatusBar + history = self.query_one(MessageHistory) + status_bar = self.query_one(StatusBar) history.add_message(Message(role="user", content=user_input)) - history.add_message(Message(role="assistant", content="处理中...")) + history.add_message(Message(role="assistant", content="⏳ 正在处理...")) + status_bar.set_processing(True) asyncio.create_task(self._process(user_input)) async def _process(self, user_input: str) -> None: + from .widgets.message_history import MessageHistory + from .widgets.status_bar import StatusBar + + history = self.query_one(MessageHistory) + status_bar = self.query_one(StatusBar) + try: result = await asyncio.get_event_loop().run_in_executor( None, lambda: self._backend_client.send_message(user_input) ) - from .widgets.message_history import MessageHistory - history = self.query_one(MessageHistory) - history.clear_messages() - - history.add_message(Message(role="user", content=user_input)) - + # 更新"处理中"消息为实际回复 if result.get("success"): content = result.get("content", "(无回复)") - history.add_message(Message(role="assistant", content=content)) + history.update_latest_message(content) else: error = result.get("error", "未知错误") - history.add_message(Message(role="assistant", content=f"错误: {error}")) + history.update_latest_message(f"❌ 错误: {error}") except Exception as e: - from .widgets.message_history import MessageHistory - history = self.query_one(MessageHistory) - history.add_message(Message(role="assistant", content=f"错误: {str(e)}")) + history.update_latest_message(f"❌ 异常: {str(e)}") + finally: + status_bar.set_processing(False) - def on_config_changed(self, event) -> None: + def on_config_section_config_changed(self, event) -> None: + """处理配置变更事件""" if not self._backend_client: + self.notify("后端未初始化,无法保存配置", title="错误", severity="error") return - api_key = event.api_key - base_url = event.base_url + config = event.config + api_key = config.api_key + base_url = config.base_url - result = self._backend_client.update_config(api_key=api_key, base_url=base_url) - self.notify("配置已保存" if result.get("success") else "配置失败", title="配置") \ No newline at end of file + # 异步更新配置,避免阻塞UI + asyncio.create_task(self._update_config_async(api_key, base_url)) + + async def _update_config_async(self, api_key: str, base_url: str) -> None: + """异步更新配置""" + from .widgets.status_bar import StatusBar + status_bar = self.query_one(StatusBar) + + try: + result = await asyncio.get_event_loop().run_in_executor( + None, + lambda: self._backend_client.update_config(api_key=api_key, base_url=base_url) + ) + + if result.get("success"): + self._api_configured = bool(api_key) + status_bar.set_api_status(self._api_configured) + self.notify("✅ 配置已保存并生效", title="配置成功", severity="information") + else: + error = result.get("error", "未知错误") + self.notify(f"❌ 配置失败: {error}", title="配置失败", severity="error") + except Exception as e: + self.notify(f"❌ 配置异常: {str(e)}", title="配置失败", severity="error") \ No newline at end of file diff --git a/ui/styles/app.css b/ui/styles/app.css index 6c04465..f94d904 100644 --- a/ui/styles/app.css +++ b/ui/styles/app.css @@ -22,15 +22,14 @@ LeftPanel { } RightPanel { - width: 70; + width: 35; dock: right; background: $panel; - overflow-y: auto; } RightPanel ScrollableContainer { - height: 1fr; - overflow-y: auto; + height: 100%; + overflow-y: scroll; } StatusBar { diff --git a/ui/widgets/right_panel.py b/ui/widgets/right_panel.py index d2a9591..6692e14 100644 --- a/ui/widgets/right_panel.py +++ b/ui/widgets/right_panel.py @@ -35,7 +35,7 @@ class RightPanel(Container): self.styles.width = 0 self.styles.display = "none" else: - self.styles.width = 70 + self.styles.width = 35 self.styles.display = "block" def is_collapsed(self) -> bool: diff --git a/ui/widgets/status_bar.py b/ui/widgets/status_bar.py index bd29dcb..f7168be 100644 --- a/ui/widgets/status_bar.py +++ b/ui/widgets/status_bar.py @@ -15,8 +15,10 @@ class StatusBar(Static): def __init__(self, **kwargs): super().__init__(**kwargs) - self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F4:查询 F5:清屏 F6:退出" + self._shortcuts = "F1:帮助 F2:侧边栏 F3:工具详情 F5:清屏 F6:退出" self._license_info = "本项目由jianf设计,以GPLv3形式开源" + self._api_status = "未配置" + self._processing = False def on_mount(self) -> None: """组件挂载时""" @@ -24,4 +26,16 @@ class StatusBar(Static): def _update_display(self) -> None: """更新显示""" - self.update(f"{self._license_info} | {self._shortcuts}") + status_icon = "●" if self._api_status == "已配置" else "○" + processing_indicator = " [处理中...]" if self._processing else "" + self.update(f"{status_icon} API: {self._api_status}{processing_indicator} | {self._license_info} | {self._shortcuts}") + + def set_api_status(self, configured: bool) -> None: + """设置API状态""" + self._api_status = "已配置" if configured else "未配置" + self._update_display() + + def set_processing(self, processing: bool) -> None: + """设置处理状态""" + self._processing = processing + self._update_display()