Files
TrulyMEM-TrueHumanMEM/ui/login_screen.py
root b4456a9c5b 多用户系统 + Admin 角色权限 + Web/TUI 同步 + 构建集成
本轮实现功能:
1. 多用户隔离:每个用户独立 config.json + graph.db
2. TUI 登录页 + 旧版自动迁移(core/migrate.py)
3. Admin/User 角色体系(core/embedded_db.py)
4. Web 后台管理 API(userinfo + admin CRUD)
5. Web 设置页用户管理区(仅 admin 可见)
6. TUI 侧栏配置区权限同步(非 admin 隐藏 Web 服务设置)
7. Web 服务打包为独立二进制(trulymem-web)
8. 双入口 PyInstaller 构建脚本(TUI + Web)
9. 活动记录器(core/activity_recorder.py)
10. 静态页面模板(登录/设置/首次引导)
2026-04-28 10:37:57 +08:00

143 lines
5.9 KiB
Python

"""
TUI 登录页面
"""
import asyncio
from pathlib import Path
from textual.app import ComposeResult
from textual.containers import Center, Middle, Vertical
from textual.widgets import Input, Button, Static, Label
from textual.screen import Screen
from core.embedded_db import EmbeddedGraphDB
from core.migrate import need_migration, run_migration, is_migrated
class LoginScreen(Screen):
"""登录界面"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._migrating = False
self._migration_username = ""
self._migration_password = ""
def compose(self) -> ComposeResult:
# 检测是否需要迁移
migrating = need_migration()
migrated = is_migrated()
if migrating and not migrated:
yield from self._compose_migration()
else:
yield from self._compose_login()
def _compose_login(self) -> ComposeResult:
with Center():
with Middle():
with Vertical(id="login_container"):
yield Static("🔐 TrulyMEM 登录", id="login_title")
yield Label("用户名:")
yield Input(placeholder="请输入用户名", id="username_input")
yield Label("密码:")
yield Input(placeholder="请输入密码", password=True, id="password_input")
yield Button("登录", id="login_button", variant="primary")
yield Static("", id="login_message")
def _compose_migration(self) -> ComposeResult:
with Center():
with Middle():
with Vertical(id="migration_container"):
yield Static("🔄 检测到旧版数据,需要迁移", id="migration_title")
yield Static("请设置管理员账号以完成迁移", id="migration_subtitle")
yield Label("用户名:")
yield Input(placeholder="请输入管理员用户名", id="mig_username_input")
yield Label("密码:")
yield Input(placeholder="请输入管理员密码", password=True, id="mig_password_input")
yield Button("开始迁移", id="migrate_button", variant="primary")
yield Static("", id="migration_message")
def on_button_pressed(self, event: Button.Pressed) -> None:
if event.button.id == "login_button":
self._handle_login()
elif event.button.id == "migrate_button":
self._handle_migration()
def on_input_submitted(self, event: Input.Submitted) -> None:
if event.input.id == "username_input":
self.query_one("#password_input", Input).focus()
elif event.input.id == "password_input":
self._handle_login()
elif event.input.id == "mig_username_input":
self.query_one("#mig_password_input", Input).focus()
elif event.input.id == "mig_password_input":
self._handle_migration()
def _handle_login(self) -> None:
username = self.query_one("#username_input", Input).value.strip()
password = self.query_one("#password_input", Input).value
if not username or not password:
self.query_one("#login_message", Static).update("❌ 用户名和密码不能为空")
return
# 验证用户
try:
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
if not global_db_path.exists():
self.query_one("#login_message", Static).update("❌ 全局数据库不存在,请先完成迁移")
return
db = EmbeddedGraphDB(db_path=str(global_db_path))
user_info = db.get_web_user(username)
if not user_info:
self.query_one("#login_message", Static).update("❌ 用户不存在")
db.close()
return
# 验证密码
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
if db.verify_web_user(username, password):
db.close()
# 登录成功,通知应用
self.app.on_login_success(username, user_info)
else:
self.query_one("#login_message", Static).update("❌ 密码错误")
db.close()
except Exception as e:
self.query_one("#login_message", Static).update(f"❌ 登录失败: {str(e)}")
def _handle_migration(self) -> None:
username = self.query_one("#mig_username_input", Input).value.strip()
password = self.query_one("#mig_password_input", Input).value
if not username or not password:
self.query_one("#migration_message", Static).update("❌ 用户名和密码不能为空")
return
self.query_one("#migration_message", Static).update("⏳ 正在迁移...")
# 执行迁移(异步)
asyncio.create_task(self._do_migration(username, password))
async def _do_migration(self, username: str, password: str) -> None:
try:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(
None, run_migration, username, password
)
if result.get("success"):
self.query_one("#migration_message", Static).update("✅ 迁移成功!请登录")
# 重新加载界面为登录界面
await asyncio.sleep(1)
self.app.pop_screen()
self.app.push_screen(LoginScreen())
else:
self.query_one("#migration_message", Static).update(f"❌ 迁移失败: {result.get('error')}")
except Exception as e:
self.query_one("#migration_message", Static).update(f"❌ 迁移异常: {str(e)}")