mirror of
https://gitcode.com/JianFeeeee/TrulyMEM-TrueHumanMEM.git
synced 2026-09-21 17:38:18 +00:00
多用户系统 + 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. 静态页面模板(登录/设置/首次引导)
This commit is contained in:
155
ui/app.py
155
ui/app.py
@ -1,9 +1,13 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
from pathlib import Path
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.binding import Binding
|
||||
|
||||
from core import BackendServer, BackendClient
|
||||
from core import BackendServer
|
||||
from core.client import BackendClient
|
||||
from .models.message import Message
|
||||
|
||||
|
||||
@ -27,6 +31,10 @@ class GraphMemoryApp(App):
|
||||
self._backend_server = backend_server
|
||||
self._backend_client = BackendClient(backend_server) if backend_server else None
|
||||
self._api_configured = False
|
||||
self._web_process: subprocess.Popen | None = None
|
||||
self._web_running = False
|
||||
self.login_user = None # 当前登录用户
|
||||
self.login_user_info = None # 当前登录用户信息
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
from .widgets.left_panel import LeftPanel
|
||||
@ -56,18 +64,59 @@ class GraphMemoryApp(App):
|
||||
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
|
||||
|
||||
# 检查是否需要登录
|
||||
migrated = is_migrated()
|
||||
need_login = migrated or not 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
|
||||
# 更新 config section 的 admin 权限
|
||||
from .widgets.config_section import ConfigSection
|
||||
try:
|
||||
is_admin = user_info.get('role') == 'admin'
|
||||
config_section = self.query_one(ConfigSection)
|
||||
config_section.set_admin(is_admin)
|
||||
except Exception:
|
||||
pass
|
||||
# 重新初始化后端
|
||||
self._init_after_login()
|
||||
|
||||
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:
|
||||
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
|
||||
|
||||
# 如果已登录,重新初始化后端服务器以使用用户的数据库
|
||||
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", "") != ""
|
||||
@ -82,13 +131,80 @@ class GraphMemoryApp(App):
|
||||
message = Message(role=msg["role"], content=msg["content"])
|
||||
history.add_message(message)
|
||||
|
||||
welcome = Message(
|
||||
role="assistant",
|
||||
content=f"系统就绪\nAPI Key: {'已配置' if self._api_configured else '未配置'}\n\n输入消息开始对话"
|
||||
)
|
||||
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)
|
||||
|
||||
def _start_web_server(self, port: int = 4096) -> None:
|
||||
"""启动 Web 服务器子进程(支持打包和开发模式)"""
|
||||
if self._web_process and self._web_process.poll() is None:
|
||||
self.notify("Web 服务已在运行", title="提示")
|
||||
return
|
||||
|
||||
def _find_web_binary() -> str:
|
||||
"""查找 Web 二进制或脚本路径"""
|
||||
# 1. PyInstaller 打包环境下查找同目录的 trulymem-web 二进制
|
||||
if getattr(sys, 'frozen', False):
|
||||
base = Path(sys._MEIPASS).parent
|
||||
for name in ['trulymem-web', 'trulymem-web.exe']:
|
||||
candidate = base / name
|
||||
if candidate.exists():
|
||||
return str(candidate)
|
||||
# 2. 开发模式:同目录下的 web_api.py
|
||||
web_script = Path(__file__).parent.parent / "web_api.py"
|
||||
if web_script.exists():
|
||||
return str(web_script)
|
||||
# 3. 打包环境回退:从 MEIPASS 读取 web_api.py 数据文件
|
||||
if getattr(sys, 'frozen', False):
|
||||
bundled = Path(sys._MEIPASS) / "web_api.py"
|
||||
if bundled.exists():
|
||||
return str(bundled)
|
||||
return ""
|
||||
|
||||
target = _find_web_binary()
|
||||
if not target:
|
||||
self.notify("找不到 Web 服务文件(web_api.py)", severity="error")
|
||||
return
|
||||
|
||||
try:
|
||||
if target.endswith('.py'):
|
||||
cmd = [sys.executable, target, "--port", str(port)]
|
||||
else:
|
||||
cmd = [target, "--port", str(port)]
|
||||
self._web_process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
self._web_running = True
|
||||
if self._backend_client:
|
||||
self._backend_client.report_web_status(True, port)
|
||||
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_process:
|
||||
try:
|
||||
self._web_process.terminate()
|
||||
self._web_process.wait(timeout=5)
|
||||
except:
|
||||
try:
|
||||
self._web_process.kill()
|
||||
except:
|
||||
pass
|
||||
self._web_process = None
|
||||
self._web_running = False
|
||||
if self._backend_client:
|
||||
self._backend_client.report_web_status(False, 0)
|
||||
self.notify("Web 服务已停止", title="Web 服务")
|
||||
|
||||
def on_unmount(self) -> None:
|
||||
# 停止 Web 服务
|
||||
self._stop_web_server()
|
||||
if self._backend_client:
|
||||
self._backend_client.shutdown()
|
||||
|
||||
@ -226,6 +342,16 @@ class GraphMemoryApp(App):
|
||||
"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,
|
||||
@ -234,17 +360,17 @@ class GraphMemoryApp(App):
|
||||
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", {})
|
||||
@ -257,10 +383,19 @@ class GraphMemoryApp(App):
|
||||
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()
|
||||
|
||||
self.notify("✅ 配置已保存并生效", title="配置成功", severity="information")
|
||||
else:
|
||||
error = result.get("error", "未知错误")
|
||||
|
||||
142
ui/login_screen.py
Normal file
142
ui/login_screen.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""
|
||||
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)}")
|
||||
@ -17,9 +17,25 @@ class AppConfig:
|
||||
task_update_max: int = 5
|
||||
memory_query_max: int = 20
|
||||
memory_update_max: int = 10
|
||||
web_username: str = ""
|
||||
web_password: str = ""
|
||||
enable_web: bool = False
|
||||
web_port: int = 4096
|
||||
enable_tui: bool = True
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "AppConfig":
|
||||
def from_env(cls, username: str = "") -> "AppConfig":
|
||||
"""
|
||||
从环境变量加载配置。
|
||||
如果指定了 username,尝试从用户的配置文件加载。
|
||||
"""
|
||||
# 如果指定了用户名,尝试从用户的配置文件加载
|
||||
if username:
|
||||
from pathlib import Path
|
||||
user_config_path = Path.home() / ".trulymem" / username / "config.json"
|
||||
if user_config_path.exists():
|
||||
return cls.from_file(user_config_path)
|
||||
|
||||
return cls(
|
||||
api_key=os.getenv("DEEPSEEK_API_KEY", ""),
|
||||
model=os.getenv("MODEL_NAME", "deepseek-chat"),
|
||||
@ -28,6 +44,11 @@ class AppConfig:
|
||||
task_update_max=int(os.getenv("TASK_UPDATE_MAX", 5)),
|
||||
memory_query_max=int(os.getenv("MEMORY_QUERY_MAX", 20)),
|
||||
memory_update_max=int(os.getenv("MEMORY_UPDATE_MAX", 10)),
|
||||
web_username=os.getenv("WEB_USERNAME", ""),
|
||||
web_password=os.getenv("WEB_PASSWORD", ""),
|
||||
enable_web=os.getenv("ENABLE_WEB", "false").lower() == "true",
|
||||
web_port=int(os.getenv("WEB_PORT", 4096)),
|
||||
enable_tui=os.getenv("ENABLE_TUI", "true").lower() == "true",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@ -46,6 +67,11 @@ class AppConfig:
|
||||
task_update_max=data.get("task_update_max", 5),
|
||||
memory_query_max=data.get("memory_query_max", 20),
|
||||
memory_update_max=data.get("memory_update_max", 10),
|
||||
web_username=data.get("web_username", ""),
|
||||
web_password=data.get("web_password", ""),
|
||||
enable_web=data.get("enable_web", False),
|
||||
web_port=data.get("web_port", 4096),
|
||||
enable_tui=data.get("enable_tui", True),
|
||||
)
|
||||
|
||||
def save(self, path: Path) -> None:
|
||||
|
||||
@ -15,9 +15,10 @@ class ConfigSection(Vertical):
|
||||
self.is_tool_limits = is_tool_limits
|
||||
super().__init__()
|
||||
|
||||
def __init__(self, config: AppConfig | None = None, **kwargs):
|
||||
def __init__(self, config: AppConfig | None = None, is_admin: bool = True, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._config = config or AppConfig()
|
||||
self._is_admin = is_admin
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
title = Static("━━ 配置 ━━", classes="config-title")
|
||||
@ -83,26 +84,104 @@ class ConfigSection(Vertical):
|
||||
yield l6
|
||||
yield Input(value=str(self._config.memory_update_max), placeholder="10", id="memory-update-max")
|
||||
|
||||
hint = Static("按Enter保存配置", classes="config-hint")
|
||||
hint.can_focus = False
|
||||
yield hint
|
||||
sep2 = Static("", classes="config-sep")
|
||||
sep2.can_focus = False
|
||||
yield sep2
|
||||
|
||||
from textual.containers import Vertical
|
||||
|
||||
# Web 登录 — 仅 admin 可见
|
||||
with Vertical(id="admin-web-login-section"):
|
||||
web_title = Static("━━ Web 登录 ━━", classes="config-title")
|
||||
web_title.can_focus = False
|
||||
yield web_title
|
||||
|
||||
label_web_user = Static("用户名:", classes="config-label")
|
||||
label_web_user.can_focus = False
|
||||
yield label_web_user
|
||||
yield Input(
|
||||
value=self._config.web_username,
|
||||
placeholder="admin",
|
||||
id="web-username-input"
|
||||
)
|
||||
|
||||
label_web_pwd = Static("密码:", classes="config-label")
|
||||
label_web_pwd.can_focus = False
|
||||
yield label_web_pwd
|
||||
yield Input(
|
||||
value=self._config.web_password,
|
||||
placeholder="修改密码",
|
||||
id="web-password-input",
|
||||
password=True
|
||||
)
|
||||
|
||||
hint = Static("按Enter保存配置", classes="config-hint")
|
||||
hint.can_focus = False
|
||||
yield hint
|
||||
|
||||
sep3 = Static("", classes="config-sep")
|
||||
sep3.can_focus = False
|
||||
yield sep3
|
||||
|
||||
# Web 服务 — 仅 admin 可见
|
||||
with Vertical(id="admin-web-service-section"):
|
||||
ws_title = Static("━━ Web 服务 ━━", classes="config-title")
|
||||
ws_title.can_focus = False
|
||||
yield ws_title
|
||||
|
||||
ws_hint = Static("在侧边栏启用后将自动启动 Web 管理界面", classes="config-hint")
|
||||
ws_hint.can_focus = False
|
||||
yield ws_hint
|
||||
|
||||
from textual.widgets import Checkbox
|
||||
yield Checkbox(
|
||||
"启用 Web 服务",
|
||||
value=self._config.enable_web,
|
||||
id="enable-web-checkbox"
|
||||
)
|
||||
|
||||
label_web_port = Static("端口:", classes="config-label")
|
||||
label_web_port.can_focus = False
|
||||
yield label_web_port
|
||||
yield Input(
|
||||
value=str(self._config.web_port),
|
||||
placeholder="4096",
|
||||
id="web-port-input",
|
||||
type="integer"
|
||||
)
|
||||
|
||||
# 默认隐藏 admin 区域,等 login 后决定是否显示
|
||||
self._apply_admin_visibility()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
try:
|
||||
api_key = self.query_one("#api-key-input", Input)
|
||||
model = self.query_one("#model-input", Input)
|
||||
base_url = self.query_one("#base-url-input", Input)
|
||||
|
||||
|
||||
api_key.tab_index = 0
|
||||
model.tab_index = 1
|
||||
base_url.tab_index = 2
|
||||
|
||||
|
||||
if self._config.api_key:
|
||||
api_key.value = self._config.api_key
|
||||
if self._config.model:
|
||||
model.value = self._config.model
|
||||
if self._config.base_url:
|
||||
base_url.value = self._config.base_url
|
||||
|
||||
web_user = self.query_one("#web-username-input", Input)
|
||||
web_pwd = self.query_one("#web-password-input", Input)
|
||||
web_user.tab_index = 7
|
||||
web_pwd.tab_index = 8
|
||||
|
||||
from textual.widgets import Checkbox
|
||||
web_port = self.query_one("#web-port-input", Input)
|
||||
try:
|
||||
web_checkbox = self.query_one("#enable-web-checkbox", Checkbox)
|
||||
except:
|
||||
pass
|
||||
web_port.tab_index = 9
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@ -111,12 +190,19 @@ class ConfigSection(Vertical):
|
||||
api_key_input = self.query_one("#api-key-input", Input)
|
||||
model_input = self.query_one("#model-input", Input)
|
||||
base_url_input = self.query_one("#base-url-input", Input)
|
||||
|
||||
|
||||
persona_update = self.query_one("#persona-update-max", Input)
|
||||
task_update = self.query_one("#task-update-max", Input)
|
||||
memory_query = self.query_one("#memory-query-max", Input)
|
||||
memory_update = self.query_one("#memory-update-max", Input)
|
||||
|
||||
web_username = self.query_one("#web-username-input", Input)
|
||||
web_password = self.query_one("#web-password-input", Input)
|
||||
|
||||
from textual.widgets import Checkbox
|
||||
web_checkbox = self.query_one("#enable-web-checkbox", Checkbox)
|
||||
web_port_input = self.query_one("#web-port-input", Input)
|
||||
|
||||
self._config = AppConfig(
|
||||
api_key=api_key_input.value,
|
||||
model=model_input.value,
|
||||
@ -125,6 +211,10 @@ class ConfigSection(Vertical):
|
||||
task_update_max=int(task_update.value or 5),
|
||||
memory_query_max=int(memory_query.value or 20),
|
||||
memory_update_max=int(memory_update.value or 10),
|
||||
web_username=web_username.value,
|
||||
web_password=web_password.value,
|
||||
enable_web=web_checkbox.value,
|
||||
web_port=int(web_port_input.value) if web_port_input.value else 4096,
|
||||
)
|
||||
|
||||
# 先发送 API 配置更新(is_tool_limits=False)
|
||||
@ -134,6 +224,24 @@ class ConfigSection(Vertical):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def set_admin(self, is_admin: bool) -> None:
|
||||
"""设置是否 admin 模式,动态显示/隐藏 admin 区域"""
|
||||
self._is_admin = is_admin
|
||||
self._apply_admin_visibility()
|
||||
|
||||
def _apply_admin_visibility(self) -> None:
|
||||
"""根据 _is_admin 显示/隐藏 admin 专用区域"""
|
||||
try:
|
||||
login_section = self.query_one("#admin-web-login-section")
|
||||
login_section.styles.display = "block" if self._is_admin else "none"
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
service_section = self.query_one("#admin-web-service-section")
|
||||
service_section.styles.display = "block" if self._is_admin else "none"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get_config(self) -> AppConfig:
|
||||
return self._config
|
||||
|
||||
@ -147,10 +255,20 @@ class ConfigSection(Vertical):
|
||||
api_key_input.value = config.api_key
|
||||
model_input.value = config.model
|
||||
base_url_input.value = config.base_url
|
||||
|
||||
|
||||
self.query_one("#persona-update-max", Input).value = str(config.persona_update_max)
|
||||
self.query_one("#task-update-max", Input).value = str(config.task_update_max)
|
||||
self.query_one("#memory-query-max", Input).value = str(config.memory_query_max)
|
||||
self.query_one("#memory-update-max", Input).value = str(config.memory_update_max)
|
||||
|
||||
self.query_one("#web-username-input", Input).value = config.web_username
|
||||
self.query_one("#web-password-input", Input).value = config.web_password
|
||||
|
||||
from textual.widgets import Checkbox
|
||||
try:
|
||||
self.query_one("#enable-web-checkbox", Checkbox).value = config.enable_web
|
||||
except:
|
||||
pass
|
||||
self.query_one("#web-port-input", Input).value = str(config.web_port)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user