多用户系统 + 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:
41
core/activity_recorder.py
Normal file
41
core/activity_recorder.py
Normal file
@ -0,0 +1,41 @@
|
||||
import sqlite3
|
||||
import time
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
|
||||
class ActivityRecorder:
|
||||
"""记录 AI 对图数据库的操作到内存 SQLite"""
|
||||
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
|
||||
self.conn.execute("CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, action TEXT, tool_name TEXT, entity TEXT, detail TEXT)")
|
||||
self.conn.commit()
|
||||
|
||||
def record(self, action: str, tool_name: str, entity: str, detail: str = "") -> None:
|
||||
self.conn.execute("INSERT INTO activities (timestamp, action, tool_name, entity, detail) VALUES (?, ?, ?, ?, ?)",
|
||||
(time.time(), action, tool_name, entity, detail))
|
||||
self.conn.commit()
|
||||
|
||||
def get_all(self) -> List[Dict]:
|
||||
cursor = self.conn.execute("SELECT id, timestamp, action, tool_name, entity, detail FROM activities ORDER BY id")
|
||||
rows = cursor.fetchall()
|
||||
return [{"id": r[0], "timestamp": r[1], "action": r[2], "tool_name": r[3], "entity": r[4], "detail": r[5]} for r in rows]
|
||||
|
||||
def clear(self) -> None:
|
||||
self.conn.execute("DELETE FROM activities")
|
||||
self.conn.commit()
|
||||
|
||||
def get_summary(self) -> Dict[str, int]:
|
||||
cursor = self.conn.execute("SELECT action, COUNT(*) FROM activities GROUP BY action")
|
||||
rows = cursor.fetchall()
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
_recorder: Optional[ActivityRecorder] = None
|
||||
|
||||
|
||||
def get_recorder() -> ActivityRecorder:
|
||||
global _recorder
|
||||
if _recorder is None:
|
||||
_recorder = ActivityRecorder()
|
||||
return _recorder
|
||||
@ -85,6 +85,44 @@ class BackendClient:
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body.get("data", {})
|
||||
|
||||
def get_web_users(self) -> list:
|
||||
"""获取 Web 用户列表"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_USERS,
|
||||
body={}
|
||||
)
|
||||
return self._server.send(packet).body.get("users", [])
|
||||
|
||||
def set_web_user(self, username: str, password: str) -> Dict:
|
||||
"""设置 Web 用户"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.SET_WEB_USER,
|
||||
body={"username": username, "password": password}
|
||||
)
|
||||
return self._server.send(packet).body.get("data", {"success": False})
|
||||
|
||||
def get_full_config(self) -> Dict:
|
||||
"""获取完整配置"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_CONFIG,
|
||||
body={}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"api_config": {}, "tool_limits": {}}
|
||||
|
||||
def report_web_status(self, running: bool, port: int = 4096) -> Dict:
|
||||
"""向后端报告 Web 服务运行状态"""
|
||||
packet = Packet(
|
||||
id=self._next_id(),
|
||||
type=PacketType.GET_WEB_SERVICE_STATUS,
|
||||
body={"running": running, "port": port}
|
||||
)
|
||||
response = self._server.send(packet)
|
||||
return response.body if response.body else {"success": False}
|
||||
|
||||
def shutdown(self) -> None:
|
||||
self._server.shutdown()
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import hashlib
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@ -86,6 +87,30 @@ class EmbeddedGraphDB:
|
||||
""")
|
||||
cursor.execute("CREATE INDEX idx_chat_created ON chat_records(created_at)")
|
||||
|
||||
# 创建 Web 用户表(支持多用户隔离)
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS web_users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
config_path TEXT,
|
||||
db_path TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
# 检查并添加新字段(用于旧数据库迁移)
|
||||
cursor.execute("PRAGMA table_info(web_users)")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
if 'config_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN config_path TEXT")
|
||||
if 'db_path' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN db_path TEXT")
|
||||
if 'role' not in columns:
|
||||
cursor.execute("ALTER TABLE web_users ADD COLUMN role TEXT NOT NULL DEFAULT 'user'")
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
def ensure_constraints(self):
|
||||
@ -519,6 +544,122 @@ class EmbeddedGraphDB:
|
||||
cursor.execute("DELETE FROM chat_records")
|
||||
self.conn.commit()
|
||||
return {"cleared": True}
|
||||
|
||||
def set_web_user(self, username: str, password: str, base_dir: str = None, role: str = 'user') -> Dict:
|
||||
"""设置或更新 Web 登录用户。password 是明文,自动哈希存储。
|
||||
自动创建用户目录并设置 config_path 和 db_path。
|
||||
role: 'admin' 或 'user',默认 'user'"""
|
||||
if not username or not password:
|
||||
return {"success": False, "error": "用户名和密码不能为空"}
|
||||
if role not in ('admin', 'user'):
|
||||
return {"success": False, "error": "角色无效 (admin/user)"}
|
||||
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
|
||||
# 确定基础目录
|
||||
if base_dir is None:
|
||||
base_dir = Path.home() / ".trulymem"
|
||||
else:
|
||||
base_dir = Path(base_dir)
|
||||
|
||||
# 创建用户目录
|
||||
user_dir = base_dir / username
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 设置用户文件路径
|
||||
config_path = str(user_dir / "config.json")
|
||||
db_path = str(user_dir / f"{username}_graph.db")
|
||||
|
||||
cursor = self.conn.cursor()
|
||||
# 如果是第一个用户,强制设为 admin
|
||||
if self.get_web_users_count() == 0:
|
||||
role = 'admin'
|
||||
cursor.execute("""
|
||||
INSERT INTO web_users (username, password_hash, role, config_path, db_path)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(username) DO UPDATE SET
|
||||
password_hash = excluded.password_hash,
|
||||
role = CASE WHEN web_users.role = 'admin' THEN 'admin' ELSE excluded.role END,
|
||||
config_path = COALESCE(web_users.config_path, excluded.config_path),
|
||||
db_path = COALESCE(web_users.db_path, excluded.db_path),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
""", (username, password_hash, role, config_path, db_path))
|
||||
self.conn.commit()
|
||||
return {"success": True, "username": username, "role": role, "config_path": config_path, "db_path": db_path}
|
||||
|
||||
def get_web_users(self) -> List[Dict]:
|
||||
"""获取所有 Web 用户列表"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT id, username, role, config_path, db_path, created_at, updated_at FROM web_users ORDER BY created_at ASC")
|
||||
users = []
|
||||
for row in cursor.fetchall():
|
||||
users.append({
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
})
|
||||
return users
|
||||
|
||||
def get_web_user(self, username: str) -> Optional[Dict]:
|
||||
"""获取单个 Web 用户信息"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id, username, role, config_path, db_path, created_at, updated_at
|
||||
FROM web_users WHERE username = ?
|
||||
""", (username,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return {
|
||||
"id": row['id'],
|
||||
"username": row['username'],
|
||||
"role": row['role'],
|
||||
"config_path": row['config_path'],
|
||||
"db_path": row['db_path'],
|
||||
"created_at": row['created_at'],
|
||||
"updated_at": row['updated_at']
|
||||
}
|
||||
return None
|
||||
|
||||
def is_admin(self, username: str) -> bool:
|
||||
"""检查用户是否为管理员"""
|
||||
user = self.get_web_user(username)
|
||||
return user is not None and user.get('role') == 'admin'
|
||||
|
||||
def delete_web_user(self, username: str) -> Dict:
|
||||
"""删除 Web 用户(同时保留文件目录)"""
|
||||
if not username:
|
||||
return {"success": False, "error": "用户名不能为空"}
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("DELETE FROM web_users WHERE username = ?", (username,))
|
||||
self.conn.commit()
|
||||
if cursor.rowcount > 0:
|
||||
return {"success": True, "username": username}
|
||||
return {"success": False, "error": "用户不存在"}
|
||||
|
||||
def get_web_users_count(self) -> int:
|
||||
"""获取 Web 用户数量 (用于判断是否需要首次设置)"""
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("SELECT COUNT(*) as cnt FROM web_users")
|
||||
row = cursor.fetchone()
|
||||
return row['cnt'] if row else 0
|
||||
|
||||
def verify_web_user(self, username: str, password: str) -> bool:
|
||||
"""验证 Web 用户登录"""
|
||||
import hashlib
|
||||
password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
cursor = self.conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT id FROM web_users
|
||||
WHERE username = ? AND password_hash = ?
|
||||
""", (username, password_hash))
|
||||
return cursor.fetchone() is not None
|
||||
|
||||
def close(self):
|
||||
"""关闭数据库连接"""
|
||||
|
||||
135
core/migrate.py
Normal file
135
core/migrate.py
Normal file
@ -0,0 +1,135 @@
|
||||
"""
|
||||
自动迁移模块 - 从旧版单用户架构迁移到多用户隔离架构
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def _trulymem_dir() -> Path:
|
||||
return Path.home() / ".trulymem"
|
||||
|
||||
def _old_config_path() -> Path:
|
||||
return _trulymem_dir() / "config.json"
|
||||
|
||||
def _old_db_path() -> Path:
|
||||
return _trulymem_dir() / "graph_memory.db"
|
||||
|
||||
def _new_global_db_path() -> Path:
|
||||
return _trulymem_dir() / "trulymem.db"
|
||||
|
||||
def _migrated_flag() -> Path:
|
||||
return _trulymem_dir() / ".migrated"
|
||||
|
||||
|
||||
def need_migration() -> bool:
|
||||
"""检测是否需要迁移"""
|
||||
# 如果已经迁移过,不需要再迁移
|
||||
if is_migrated():
|
||||
return False
|
||||
|
||||
old_config_exists = _old_config_path().exists()
|
||||
old_db_exists = _old_db_path().exists()
|
||||
new_db_exists = _new_global_db_path().exists()
|
||||
if (old_config_exists or old_db_exists) and not new_db_exists:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def is_migrated() -> bool:
|
||||
"""检查是否已完成迁移"""
|
||||
return _migrated_flag().exists()
|
||||
|
||||
|
||||
def _mark_migrated():
|
||||
"""标记迁移完成"""
|
||||
_trulymem_dir().mkdir(parents=True, exist_ok=True)
|
||||
with open(_migrated_flag(), 'w') as f:
|
||||
f.write(datetime.now().isoformat())
|
||||
|
||||
|
||||
def run_migration(username: str, password: str) -> Dict:
|
||||
"""
|
||||
执行迁移
|
||||
|
||||
Args:
|
||||
username: 新用户名
|
||||
password: 新用户密码
|
||||
|
||||
Returns:
|
||||
迁移结果字典
|
||||
"""
|
||||
try:
|
||||
# 1. 创建用户目录
|
||||
user_dir = _trulymem_dir() / username
|
||||
user_dir.mkdir(parents=True, exist_ok=True)
|
||||
new_config_path = user_dir / "config.json"
|
||||
if _old_config_path().exists():
|
||||
shutil.copy2(_old_config_path(), new_config_path)
|
||||
new_db_path = user_dir / f"{username}_graph.db"
|
||||
if _old_db_path().exists():
|
||||
shutil.copy2(_old_db_path(), new_db_path)
|
||||
|
||||
# 4. 创建全局数据库并写入 web_users 表
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
|
||||
global_db = EmbeddedGraphDB(db_path=str(_new_global_db_path()))
|
||||
|
||||
# 设置用户(会自动创建记录)
|
||||
result = global_db.set_web_user(username, password)
|
||||
if not result.get("success"):
|
||||
return {"success": False, "error": f"创建用户失败: {result.get('error')}"}
|
||||
|
||||
# 如果用户目录已存在,更新路径(确保正确)
|
||||
cursor = global_db.conn.cursor()
|
||||
config_path = str(new_config_path)
|
||||
db_path = str(new_db_path)
|
||||
cursor.execute("""
|
||||
UPDATE web_users
|
||||
SET config_path = ?, db_path = ?
|
||||
WHERE username = ?
|
||||
""", (config_path, db_path, username))
|
||||
global_db.conn.commit()
|
||||
|
||||
# 5. 标记迁移完成
|
||||
_mark_migrated()
|
||||
|
||||
global_db.close()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"username": username,
|
||||
"config_path": config_path,
|
||||
"db_path": db_path,
|
||||
"message": "迁移完成"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
def rollback_migration():
|
||||
"""回滚迁移(用于失败恢复)"""
|
||||
try:
|
||||
# 删除全局数据库
|
||||
if _new_global_db_path().exists():
|
||||
_new_global_db_path().unlink()
|
||||
if _migrated_flag().exists():
|
||||
_migrated_flag().unlink()
|
||||
|
||||
return {"success": True, "message": "回滚完成"}
|
||||
except Exception as e:
|
||||
return {"success": False, "error": str(e)}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试
|
||||
print("Migration module test")
|
||||
print(f"Need migration: {need_migration()}")
|
||||
print(f"Is migrated: {is_migrated()}")
|
||||
@ -9,6 +9,7 @@ from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
class PacketType(Enum):
|
||||
@ -17,6 +18,10 @@ class PacketType(Enum):
|
||||
GET_STATUS = "get_status"
|
||||
GET_SETTINGS = "get_settings" # 合并:获取 api_config + tool_limits
|
||||
SET_SETTINGS = "set_settings" # 合并:设置 api_config + tool_limits
|
||||
GET_WEB_USERS = "get_web_users" # 获取 Web 用户列表
|
||||
SET_WEB_USER = "set_web_user" # 设置 Web 用户(用户名+密码)
|
||||
GET_WEB_SERVICE_STATUS = "get_web_service_status" # 获取 Web 服务运行状态
|
||||
GET_CONFIG = "get_config" # 获取完整配置
|
||||
GET_HISTORY = "get_history"
|
||||
SAVE_HISTORY = "save_history"
|
||||
SHUTDOWN = "shutdown"
|
||||
@ -43,10 +48,11 @@ class BackendServer:
|
||||
|
||||
DEFAULT_CONFIG_PATH = Path.home() / ".trulymem" / "config.json"
|
||||
|
||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True, config_file: str = None):
|
||||
def __init__(self, db_path: str = "graph_memory.db", use_embedded_db: bool = True, config_file: str = None, username: str = ""):
|
||||
self._db_path = db_path
|
||||
self._use_embedded_db = use_embedded_db
|
||||
self._config_file = Path(config_file) if config_file else self.DEFAULT_CONFIG_PATH
|
||||
self._username = username
|
||||
|
||||
self._graph = None
|
||||
self._client = None
|
||||
@ -97,9 +103,26 @@ class BackendServer:
|
||||
self._thread.start()
|
||||
|
||||
def _load_config(self) -> None:
|
||||
if self._config_file.exists():
|
||||
"""加载配置。如果指定了用户名,从用户的 config_path 加载。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
with open(self._config_file, 'r') as f:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
saved = json.load(f)
|
||||
self._config.update(saved)
|
||||
for key in self._tool_limits:
|
||||
@ -109,9 +132,26 @@ class BackendServer:
|
||||
pass
|
||||
|
||||
def _save_config(self) -> None:
|
||||
self._config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
"""保存配置。如果指定了用户名,保存到用户的 config_path。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
if self._username:
|
||||
try:
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('config_path'):
|
||||
config_file = Path(user_info['config_path'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
saved_data = {**self._config, **self._tool_limits}
|
||||
with open(self._config_file, 'w') as f:
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(saved_data, f, indent=2)
|
||||
|
||||
def _create_tool_limiter(self):
|
||||
@ -125,8 +165,25 @@ class BackendServer:
|
||||
return ToolLimiter(limits)
|
||||
|
||||
def _init_graph(self) -> None:
|
||||
"""初始化图数据库。如果指定了用户名,从全局数据库获取用户的 db_path。"""
|
||||
db_path = self._db_path
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的数据库路径
|
||||
if self._username:
|
||||
try:
|
||||
# 临时连接全局数据库获取用户信息
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
if global_db_path.exists():
|
||||
temp_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
user_info = temp_db.get_web_user(self._username)
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('db_path'):
|
||||
db_path = user_info['db_path']
|
||||
except Exception:
|
||||
pass # 如果获取失败,使用默认路径
|
||||
|
||||
if self._use_embedded_db:
|
||||
self._graph = EmbeddedGraphDB(db_path=self._db_path)
|
||||
self._graph = EmbeddedGraphDB(db_path=db_path)
|
||||
else:
|
||||
from .graph_client import Neo4jGraph
|
||||
self._graph = Neo4jGraph(
|
||||
@ -158,6 +215,25 @@ class BackendServer:
|
||||
response_body = self._handle_get_settings()
|
||||
elif packet.type == PacketType.SET_SETTINGS:
|
||||
response_body = self._handle_set_settings(packet.body)
|
||||
elif packet.type == PacketType.GET_WEB_USERS:
|
||||
response_body = {"users": self._graph.get_web_users()}
|
||||
elif packet.type == PacketType.SET_WEB_USER:
|
||||
username = packet.body.get("username", "")
|
||||
password = packet.body.get("password", "")
|
||||
if not username or not password:
|
||||
response_body = {"success": False, "error": "用户名和密码不能为空"}
|
||||
else:
|
||||
# 使用全局数据库(trulymem.db)来管理用户
|
||||
global_db_path = Path.home() / ".trulymem" / "trulymem.db"
|
||||
from .embedded_db import EmbeddedGraphDB
|
||||
global_db = EmbeddedGraphDB(db_path=str(global_db_path))
|
||||
response_body = global_db.set_web_user(username, password)
|
||||
global_db.close()
|
||||
elif packet.type == PacketType.GET_WEB_SERVICE_STATUS:
|
||||
body = packet.body
|
||||
response_body = {"running": body.get("running", False), "port": body.get("port", 4096)}
|
||||
elif packet.type == PacketType.GET_CONFIG:
|
||||
response_body = self._get_full_config()
|
||||
elif packet.type == PacketType.GET_HISTORY:
|
||||
response_body = self._handle_get_history()
|
||||
elif packet.type == PacketType.SAVE_HISTORY:
|
||||
@ -182,6 +258,8 @@ class BackendServer:
|
||||
def _handle_process_message(self, body: Dict) -> Dict:
|
||||
from .tool_executor import execute_tool
|
||||
|
||||
get_recorder().clear()
|
||||
|
||||
user_input = body.get("user_input", "")
|
||||
|
||||
if not self._client:
|
||||
@ -332,6 +410,12 @@ class BackendServer:
|
||||
"tool_limits": self._tool_limits.copy()
|
||||
}
|
||||
|
||||
def _get_full_config(self) -> Dict:
|
||||
return {
|
||||
"api_config": self._config.copy(),
|
||||
"tool_limits": self._tool_limits.copy(),
|
||||
}
|
||||
|
||||
def _handle_set_settings(self, body: Dict) -> Dict:
|
||||
api_config = body.get("api_config", {})
|
||||
tool_limits = body.get("tool_limits", {})
|
||||
|
||||
@ -4,6 +4,8 @@
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
|
||||
from .activity_recorder import get_recorder
|
||||
|
||||
|
||||
def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
"""执行工具调用"""
|
||||
@ -11,8 +13,12 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
print(f"[参数] {json.dumps(arguments, ensure_ascii=False, indent=2)}")
|
||||
|
||||
try:
|
||||
recorder = get_recorder()
|
||||
|
||||
# 基础记忆工具
|
||||
if tool_name == "memory_recall":
|
||||
entity = arguments.get("query_intent", "") or str(arguments.get("seed_entities", ""))
|
||||
recorder.record("query", tool_name, entity)
|
||||
result = graph.recall(
|
||||
query_intent=arguments.get("query_intent", ""),
|
||||
seed_entities=arguments.get("seed_entities"),
|
||||
@ -23,30 +29,39 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
return format_recall_result(result)
|
||||
|
||||
elif tool_name == "memory_commit":
|
||||
triplets = arguments.get("triplets", [])
|
||||
entity = triplets[0].get("subject", "") if triplets else ""
|
||||
recorder.record("create", tool_name, entity, f"{len(triplets)} triplets")
|
||||
result = graph.commit(
|
||||
triplets=arguments.get("triplets", []),
|
||||
triplets=triplets,
|
||||
entity_types=arguments.get("entity_types"),
|
||||
temporal_tag=arguments.get("temporal_tag")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_purge":
|
||||
criteria = arguments.get("criteria", {})
|
||||
entity = criteria.get("subject_contains", str(criteria))
|
||||
recorder.record("delete", tool_name, entity)
|
||||
result = graph.purge(
|
||||
criteria=arguments.get("criteria", {}),
|
||||
criteria=criteria,
|
||||
mode=arguments.get("mode", "soft"),
|
||||
new_relation=arguments.get("new_relation")
|
||||
)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_introspect":
|
||||
recorder.record("query", tool_name, "数据库统计")
|
||||
result = graph.introspect(session_id=arguments.get("session_id"))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_archive":
|
||||
recorder.record("archive", tool_name, "旧记忆")
|
||||
result = graph.archive(days=arguments.get("days", 30))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_cleanup":
|
||||
recorder.record("cleanup", tool_name, "已删除数据")
|
||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
@ -56,27 +71,37 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
|
||||
# 人设图管理工具
|
||||
elif tool_name == "persona_update":
|
||||
recorder.record("update", tool_name, "人设属性")
|
||||
result = execute_persona_update(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_clear":
|
||||
recorder.record("delete", tool_name, "所有人设")
|
||||
result = execute_persona_clear(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
# 工作记忆链管理工具
|
||||
elif tool_name == "task_create":
|
||||
desc = arguments.get("description", "")
|
||||
recorder.record("create", tool_name, desc)
|
||||
result = execute_task_create(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_set_state":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_set_state(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_delete":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("delete", tool_name, desc)
|
||||
result = execute_task_delete(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_link_info":
|
||||
desc = arguments.get("task_id", "")
|
||||
recorder.record("update", tool_name, desc)
|
||||
result = execute_task_link_info(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user