Files

425 lines
17 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import asyncio
import sys
import threading
import signal
from pathlib import Path
from textual.app import App, ComposeResult
from textual.binding import Binding
from core import BackendServer
from core.client import BackendClient
from .models.message import Message
class GraphMemoryApp(App):
CSS_PATH = [
Path(__file__).parent / "styles" / "app.css",
Path(__file__).parent / "styles" / "messages.css",
Path(__file__).parent / "styles" / "components.css",
]
BINDINGS = [
Binding("f1", "show_help", "帮助"),
Binding("f2", "toggle_sidebar", "侧边栏"),
Binding("f3", "toggle_tool_details", "工具详情"),
Binding("f5", "clear_history", "清屏"),
Binding("f6", "quit", "退出"),
]
def __init__(self, backend_server: BackendServer = None, config_file: str = None, **kwargs):
super().__init__(**kwargs)
self._backend_server = backend_server
self._backend_client = BackendClient(backend_server) if backend_server else None
self._api_configured = False
self._web_running = False
self.login_user = None # 当前登录用户
self.login_user_info = None # 当前登录用户信息
def compose(self) -> ComposeResult:
from .widgets.left_panel import LeftPanel
from .widgets.right_panel import RightPanel
from .widgets.status_bar import StatusBar
from .models.config import AppConfig
initial_config = AppConfig()
if self._backend_client:
settings_result = self._backend_client.get_settings()
settings_data = settings_result.get("data", {})
api_config = settings_data.get("api_config", {})
initial_config.api_key = api_config.get("api_key", "")
initial_config.base_url = api_config.get("base_url", "https://api.deepseek.com")
initial_config.model = api_config.get("model", "deepseek-v4-flash")
initial_config.enable_web = api_config.get("enable_web", False)
initial_config.web_port = api_config.get("web_port", 4096)
initial_config.enable_tui = api_config.get("enable_tui", True)
tool_limits = settings_data.get("tool_limits", {})
initial_config.persona_update_max = tool_limits.get("persona_update_max", 1)
initial_config.task_update_max = tool_limits.get("task_update_max", 5)
initial_config.memory_query_max = tool_limits.get("memory_query_max", 20)
initial_config.memory_update_max = tool_limits.get("memory_update_max", 10)
yield LeftPanel()
yield RightPanel(config=initial_config)
yield StatusBar()
def on_mount(self) -> None:
from .widgets.status_bar import StatusBar
from .widgets.message_history import MessageHistory
from .login_screen import LoginScreen
from core.migrate import need_migration, is_migrated
# 检查是否需要登录
# 如果已迁移,需要登录验证用户
# 如果需要迁移,显示迁移登录界面
need_login = is_migrated() or need_migration()
if need_login and not self.login_user:
# 显示登录界面
self.push_screen(LoginScreen())
return
# 已登录或无需登录,继续初始化
self._init_after_login()
def on_login_success(self, username: str, user_info: dict) -> None:
"""登录成功后调用"""
self.login_user = username
self.login_user_info = user_info
# 关闭登录界面
self.pop_screen()
# 更新 config section 的 admin 权限
from .widgets.config_section import ConfigSection
is_admin = user_info.get('role') == 'admin'
self.call_after_refresh(lambda: self._update_admin(is_admin))
# 重新初始化后端
self._init_after_login()
def _update_admin(self, is_admin: bool) -> None:
from .widgets.config_section import ConfigSection
try:
config_section = self.query_one(ConfigSection)
config_section.set_admin(is_admin)
except Exception as e:
print(f"配置区加载失败: {e}", file=sys.stderr)
def _init_after_login(self) -> None:
"""登录后初始化"""
from .widgets.status_bar import StatusBar
from .widgets.message_history import MessageHistory
status_bar = self.query_one(StatusBar)
if not self._backend_server:
history = self.query_one(MessageHistory)
error = Message(role="assistant", content="后端未初始化")
history.add_message(error)
status_bar.set_api_status(False)
return
# 如果已登录,重新初始化后端服务器以使用用户的数据库
if self.login_user:
from core.server import BackendServer
# 创建新的后端服务器(使用用户的数据库)
self._backend_server = BackendServer(username=self.login_user)
self._backend_client = BackendClient(self._backend_server)
self._backend_server.start()
status = self._backend_client.get_status()
data = status.get("data", {})
self._api_configured = data.get("config", {}).get("api_key", "") != ""
status_bar.set_api_status(self._api_configured)
history = self.query_one(MessageHistory)
if self._api_configured:
chat_history = self._backend_client.get_history()
if chat_history:
for msg in chat_history:
message = Message(role=msg["role"], content=msg["content"])
history.add_message(message)
role_label = "管理员" if self.login_user_info and self.login_user_info.get('role') == 'admin' else "用户"
welcome_msg = f"系统就绪\n用户: {self.login_user or '默认'} ({role_label})\n"
welcome_msg += f"API Key: {'已配置' if self._api_configured else '未配置'}\n\n输入消息开始对话"
welcome = Message(role="assistant", content=welcome_msg)
history.add_message(welcome)
# 自动启动Web服务如果配置中启用了
if self._backend_client:
settings_result = self._backend_client.get_settings()
settings_data = settings_result.get("data", {})
api_config = settings_data.get("api_config", {})
enable_web = api_config.get("enable_web", False)
web_port = api_config.get("web_port", 4096)
# 更新状态栏的Web服务状态
try:
status_bar.set_web_status(self._web_running, web_port if self._web_running else 0)
except:
pass
if enable_web:
self._start_web_server(web_port)
def _start_web_server(self, port: int = 4096) -> None:
"""在当前进程通过线程启动 Web 服务(无需子进程)"""
if self._web_running:
self.notify("Web 服务已在运行", title="提示")
return
try:
from web_api import run_web_server
from .widgets.status_bar import StatusBar
run_web_server(port=port)
self._web_running = True
if self._backend_client:
self._backend_client.report_web_status(True, port)
# 更新状态栏
try:
status_bar = self.query_one(StatusBar)
status_bar.set_web_status(True, port)
except:
pass
self.notify(f"Web 服务已启动 → http://0.0.0.0:{port}", title="Web 服务")
except Exception as e:
self.notify(f"启动 Web 服务失败: {e}", severity="error")
def _stop_web_server(self) -> None:
"""停止 Web 服务线程"""
if self._web_running:
try:
from web_api import stop_web_server
from .widgets.status_bar import StatusBar
stop_web_server()
except Exception:
pass
self._web_running = False
if self._backend_client:
self._backend_client.report_web_status(False, 0)
# 更新状态栏
try:
status_bar = self.query_one(StatusBar)
status_bar.set_web_status(False)
except:
pass
self.notify("Web 服务已停止", title="Web 服务")
def on_unmount(self) -> None:
# 停止 Web 服务
self._stop_web_server()
if self._backend_client:
self._backend_client.shutdown()
def action_show_help(self) -> None:
from pathlib import Path
config_path = Path.home() / ".trulymem" / "config.json"
db_path = Path.home() / ".trulymem" / "graph_memory.db"
help_text = (
"F1-帮助 F2-侧边栏 F3-工具详情 F5-清屏 F6-退出\n\n"
f"配置文件: {config_path}\n"
f"数据库: {db_path}"
)
self.notify(help_text, title="快捷键 & 配置路径", timeout=15)
def action_toggle_sidebar(self) -> None:
from .widgets.right_panel import RightPanel
sidebar = self.query_one(RightPanel)
sidebar.toggle()
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_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:
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="正在处理..."))
status_bar.set_processing(True)
asyncio.create_task(self._process(user_input))
def on_input_box_clear_history(self, event) -> None:
"""处理清空聊天记录事件"""
if not self._backend_client:
self.notify("后端未初始化", title="错误", severity="error")
return
self._backend_client.clear_history()
from .widgets.message_history import MessageHistory
history = self.query_one(MessageHistory)
history.clear_messages()
self.notify("聊天记录已清空AI记忆保持不变", title="提示", severity="information")
async def _process(self, user_input: str) -> None:
from .widgets.message_history import MessageHistory
from .widgets.status_bar import StatusBar
from .widgets.right_panel import RightPanel
from .models.log_entry import LogEntry
from datetime import datetime
history = self.query_one(MessageHistory)
status_bar = self.query_one(StatusBar)
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._backend_client.process_message(user_input)
)
if result.get("success"):
# 响应结构: {"success": True, "data": {"content": "...", "tool_calls": [...], ...}, "error": None}
data = result.get("data", {})
content = data.get("content", "(无回复)")
history.update_latest_message(content)
# 处理工具调用信息,更新操作日志
tool_calls = data.get("tool_calls", [])
if tool_calls:
try:
right_panel = self.query_one(RightPanel)
operation_log = right_panel.get_operation_log()
for tool_call in tool_calls:
entry = LogEntry(
timestamp=datetime.now(),
tool_name=tool_call.get("name", "unknown"),
arguments=tool_call.get("arguments", {}),
result=str(tool_call.get("result", "")),
duration=0.0 # 后端没有返回耗时信息
)
operation_log.add_log(entry)
except Exception:
pass # 忽略操作日志更新失败
else:
error = result.get("error", "未知错误")
history.update_latest_message(f"❌ 错误: {error}")
status_bar.set_processing(False)
def on_config_section_config_changed(self, event) -> None:
if not self._backend_client:
self.notify("后端未初始化,无法保存配置", title="错误", severity="error")
return
asyncio.create_task(self._update_settings_async(event.config))
async def _update_settings_async(self, config) -> None:
from .widgets.status_bar import StatusBar
from .widgets.config_section import ConfigSection
status_bar = self.query_one(StatusBar)
api_config = {
"api_key": config.api_key,
"base_url": config.base_url,
"model": getattr(config, 'model', 'deepseek-v4-flash'),
"enable_web": getattr(config, 'enable_web', False),
"enable_tui": getattr(config, 'enable_tui', True),
"web_port": getattr(config, 'web_port', 4096),
}
tool_limits = {
"persona_update_max": config.persona_update_max,
"task_update_max": config.task_update_max,
"memory_query_max": config.memory_query_max,
"memory_update_max": config.memory_update_max,
}
# 保存 Web 用户(如果用户名和密码都不为空)
if config.web_username and config.web_password:
try:
await asyncio.get_event_loop().run_in_executor(
None, self._backend_client.set_web_user,
config.web_username, config.web_password
)
except Exception as e:
self.notify(f"保存 Web 用户失败: {e}", severity="warning")
try:
result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._backend_client.update_settings(
api_config=api_config,
tool_limits=tool_limits
)
)
if result.get("success"):
self._api_configured = bool(config.api_key)
status_bar.set_api_status(self._api_configured)
settings_result = await asyncio.get_event_loop().run_in_executor(
None,
lambda: self._backend_client.get_settings()
)
settings_data = settings_result.get("data", {})
try:
config_section = self.query_one(ConfigSection)
api_cfg = settings_data.get("api_config", {})
tool_lmts = settings_data.get("tool_limits", {})
config_section.set_config(AppConfig(
api_key=api_cfg.get("api_key", ""),
base_url=api_cfg.get("base_url", "https://api.deepseek.com"),
model=api_cfg.get("model", "deepseek-v4-flash"),
persona_update_max=tool_lmts.get("persona_update_max", 1),
task_update_max=tool_lmts.get("task_update_max", 5),
memory_query_max=tool_lmts.get("memory_query_max", 20),
memory_update_max=tool_lmts.get("memory_update_max", 10),
enable_web=api_cfg.get("enable_web", False),
web_port=api_cfg.get("web_port", 4096),
enable_tui=api_cfg.get("enable_tui", True),
))
except Exception:
pass
# 管理 Web 服务
if config.enable_web:
self._start_web_server(config.web_port)
else:
self._stop_web_server()
# 更新状态栏的Web服务状态
try:
status_bar.set_web_status(self._web_running, config.web_port if self._web_running else 0)
except:
pass
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")