refactor: packet-based IPC with core/ui separation

- All logic in core layer (BackendServer runs independently)
- Packet protocol: {id, type, body} for all communications
- UI simplified: just input→backend→display
- Config, history, tools all handled by core
- BackendServer can run standalone without UI
This commit is contained in:
root
2026-04-12 20:47:07 +08:00
parent a820afada9
commit 5c76b19624
4 changed files with 294 additions and 247 deletions

266
ui/app.py
View File

@ -2,17 +2,12 @@ import asyncio
from pathlib import Path
from textual.app import App, ComposeResult
from textual.binding import Binding
from datetime import datetime
from core import BackendServer, BackendClient
from .models.message import Message, ToolCall, ToolResult
from .models.config import AppConfig
from .models.log_entry import LogEntry
from .services.config_manager import ConfigManager
from core import BackendClient
from .models.message import Message
class GraphMemoryApp(App[None]):
class GraphMemoryApp(App):
CSS_PATH = [
Path(__file__).parent / "styles" / "app.css",
Path(__file__).parent / "styles" / "messages.css",
@ -23,257 +18,108 @@ class GraphMemoryApp(App[None]):
Binding("f1", "show_help", "帮助"),
Binding("f2", "toggle_sidebar", "侧边栏"),
Binding("f3", "toggle_tool_details", "工具详情"),
Binding("f4", "focus_query", "查询"),
Binding("f5", "clear_history", "清屏"),
Binding("f6", "quit", "退出"),
]
def __init__(self, config: AppConfig | None = None, backend_server: BackendServer | None = None, **kwargs):
def __init__(self, backend_server: BackendServer = None, **kwargs):
super().__init__(**kwargs)
self._config_manager = ConfigManager()
if config:
self._config = config
elif self._config_manager.exists():
self._config = self._config_manager.load()
else:
self._config = AppConfig.from_env()
if backend_server:
self._backend_server = backend_server
self._backend_client = BackendClient(backend_server)
else:
self._backend_server: BackendServer | None = None
self._backend_client: BackendClient | None = None
self._backend_server = backend_server
self._backend_client = BackendClient(backend_server) if backend_server else None
def compose(self) -> ComposeResult:
from .widgets.left_panel import LeftPanel
from .widgets.right_panel import RightPanel
from .widgets.status_bar import StatusBar
yield LeftPanel()
yield RightPanel(self._config, use_embedded_db=True)
yield RightPanel()
yield StatusBar()
def on_mount(self) -> None:
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)
return
status = self._backend_client.get_status()
api_configured = status.get("config", {}).get("api_key", "") != ""
from .widgets.message_history import MessageHistory
history = self.query_one(MessageHistory)
try:
self._backend_server = BackendServer(
db_path="graph_memory.db",
use_embedded_db=True
)
self._backend_server.start(
api_key=self._config.api_key,
base_url=self._config.base_url
)
self._backend_client = BackendClient(self._backend_server)
welcome = Message(
role="assistant",
content="系统初始化成功!\n\n"
f"数据库: 内嵌SQLite (graph_memory.db)\n"
f"API Key: {'已配置' if self._config.api_key else '未配置'}\n\n"
"现在可以开始对话了!",
)
history.add_message(welcome)
except Exception as e:
error = Message(
role="assistant",
content=f"初始化失败: {str(e)}\n\n"
"请检查:\n"
"1. API Key 是否配置\n"
"2. 网络连接是否正常\n\n"
"按F2打开侧边栏配置API Key",
)
history.add_message(error)
welcome = Message(
role="assistant",
content=f"系统就绪\nAPI Key: {'已配置' if api_configured else '未配置'}\n\n输入消息开始对话"
)
history.add_message(welcome)
def on_unmount(self) -> None:
if self._backend_server:
self._backend_server.shutdown()
if self._backend_client:
self._backend_client.shutdown()
def action_show_help(self) -> None:
help_text = """
快捷键:
F1 - 帮助
F2 - 切换侧边栏
F3 - 工具详情
F4 - 查询框
F5 - 清屏
F6 - 退出
输入消<EFBFBD><EFBFBD>后按 Enter 发送
"""
self.notify(help_text, title="帮助", timeout=10)
self.notify("F1-帮助 F2-侧边栏 F3-工具详情 F5-清屏 F6-退出", title="快捷键", timeout=10)
def action_toggle_sidebar(self) -> None:
from .widgets.right_panel import RightPanel
sidebar = self.query_one(RightPanel)
sidebar.toggle()
sidebar.update_title()
def action_toggle_tool_details(self) -> None:
from .widgets.message_history import MessageHistory
history = self.query_one(MessageHistory)
history.toggle_latest_tool_details()
def action_focus_query(self) -> None:
from .widgets.right_panel import RightPanel
sidebar = self.query_one(RightPanel)
if not sidebar.has_cypher_query_box():
self.notify("查询框仅在 Neo4j 模式下可用", title="提示", timeout=3)
return
if sidebar.is_collapsed():
sidebar.toggle()
sidebar.update_title()
query_box = sidebar.get_cypher_query_box()
if query_box:
query_box.focus()
def action_clear_history(self) -> None:
from .widgets.message_history import MessageHistory
history = self.query_one(MessageHistory)
history.clear_messages()
def on_input_box_send_message(self, event) -> None:
from .widgets.input_box import InputBox
from .widgets.message_history import MessageHistory
from .widgets.right_panel import RightPanel
if not self._backend_client:
return
try:
history = self.query_one(MessageHistory)
user_message = Message(role="user", content=event.content)
history.add_message(user_message)
if not self._config.api_key:
response_msg = Message(
role="assistant",
content="请先配置API Key。\n\n按F2打开侧边栏输入API Key后按Enter保存。",
)
history.add_message(response_msg)
return
processing_msg = Message(role="assistant", content="正在处理...")
history.add_message(processing_msg)
asyncio.create_task(self._process_message_async(event.content))
except Exception as e:
error_msg = Message(role="assistant", content=f"错误: {str(e)}")
history.add_message(error_msg)
async def _process_message_async(self, user_input: str) -> None:
user_input = event.content
from .widgets.message_history import MessageHistory
from .widgets.right_panel import RightPanel
history = self.query_one(MessageHistory)
log = self.query_one(RightPanel).get_operation_log()
history.add_message(Message(role="user", content=user_input))
history.add_message(Message(role="assistant", content="处理中..."))
asyncio.create_task(self._process(user_input))
async def _process(self, user_input: str) -> None:
try:
if not self._backend_client:
raise Exception("后端未初始化")
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._backend_client.process_message(user_input)
lambda: self._backend_client.send_message(user_input)
)
content = result.get("content", "(无回复)")
tool_calls_data = result.get("tool_calls", [])
rejected_tools = result.get("rejected_tools", [])
tool_calls = []
tool_results = []
for tc in tool_calls_data:
tc_obj = ToolCall(
id=tc.get("id", ""),
name=tc.get("name", ""),
arguments=tc.get("arguments", {})
)
tool_calls.append(tc_obj)
tr = ToolResult(
tool_call_id=tc_obj.id,
name=tc_obj.name,
arguments=tc_obj.arguments,
result=tc.get("result", ""),
success=not tc.get("result", "").startswith("工具执行<EFBFBD><EFBFBD><EFBFBD>")
)
tool_results.append(tr)
log_entry = LogEntry(
tool_name=tc_obj.name,
arguments=tc_obj.arguments,
result=tc.get("result", ""),
)
log.add_log(log_entry)
assistant_message = Message(
role="assistant",
content=content,
tool_calls=tool_calls if tool_calls else None,
tool_results=tool_results if tool_results else None
)
history.add_message(assistant_message)
self.refresh()
except Exception as e:
error_msg = str(e)
from .widgets.message_history import MessageHistory
history = self.query_one(MessageHistory)
history.clear_messages()
if "Connection error" in error_msg or "connection" in error_msg.lower():
help_text = """
网络连接错误!可能的原因:
1. API Key 未配置或无效
2. 网络无法访问 API 服务器
3. API 服务器暂时不可用
解决方法:
按 F2 展开侧边栏,检查并配置 API Key
检查网络连接
"""
elif "API Key" in error_msg:
help_text = """
API Key 未配置!
请按以下步骤配置:
1. 按 F2 展开右侧边栏
2. 点击"配置"展开配置区
3. 在 API Key 输入框输入你的密钥
4. 按 Enter 键保存配置
获取 API Key: https://platform.deepseek.com/
"""
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))
else:
help_text = f"\n详细错误: {error_msg}"
error_message = Message(role="assistant", content=f"错误: {error_msg}\n{help_text}")
history.add_message(error_message)
error = result.get("error", "未知错误")
history.add_message(Message(role="assistant", content=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)}"))
def on_config_section_config_changed(self, event) -> None:
from .widgets.right_panel import RightPanel
def on_config_changed(self, event) -> None:
if not self._backend_client:
return
self._config = event.config
self._config_manager.save(self._config)
api_key = event.api_key
base_url = event.base_url
try:
right_panel = self.query_one(RightPanel)
right_panel._config = self._config
except Exception:
pass
if self._backend_client:
self._backend_client.update_config(
api_key=self._config.api_key,
base_url=self._config.base_url
)
self.notify("配置已保存并应用", title="配置")
else:
self.notify("配置已保存,但后端未初始化", title="警告")
result = self._backend_client.update_config(api_key=api_key, base_url=base_url)
self.notify("配置已保存" if result.get("success") else "配置失败", title="配置")