fix: auto-clean orphan entities on purge; fix graph camera zoom; optimize system prompt

This commit is contained in:
root
2026-04-28 16:27:49 +08:00
parent aefaea7b90
commit ad382602f2
32 changed files with 1033 additions and 1299 deletions

92
.gitignore vendored
View File

@ -1,77 +1,23 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
develop-eggs/
dist/
downloads/
eggs/
.lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
test_venv/
ENV/
env/
# IDE
.vscode/
.idea/
.arts/
.codeartsdoer/
*.swp
*.swo
*~
# Database
*.db
*.sqlite
*.sqlite3
# Logs
*.log
logs/
# OS
*.pyc
*.pyo
.DS_Store
Thumbs.db
# Sensitive
.env
*.key
*.pem
config.json
# Build
dist/
# Temporary
*.tmp
*.bak
# AI Generated
jimeng*.png
# Test Cache
.pytest_cache/
# Web config (contains passwords, secret keys)
web_config.json
# Node.js
harmonyos/.hvigor/
harmonyos/build/
harmonyos/entry/build/
harmonyos.bak/
node_modules/
# Test artifacts
/session_*/
ts/node_modules/
ts/dist/
ts/build/
*.hap
*.hsp
.env
venv/
.venv/
dist/
build/trulymem/
*.db
task_archive/
ts/
ts/task_archive/
core/web_config.json

166
README.md
View File

@ -4,185 +4,61 @@
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
</p>
> **📜 开源协议**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
> 本项目自由开源,可自由使用、修改和分发,但修改后的作品必须以相同许可证发布。
> **📜 开源协议**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
> **English**: [Switch to English version](./README_EN.md)
> **English**: [README_EN.md](./README_EN.md)
**让 AI 拥有自知、可塑、有分寸感的长期记忆**
*The More Human Choice.*
**让 AI 拥有自知、可塑、有分寸感的长期记忆***The More Human Choice.*
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
[![Branch](https://img.shields.io/badge/branch-main-orange.svg)]()
---
## 故事的开头
## 一句话
行业普遍认为LLM 海量参数让其涌现了智能。但这个智能是「死的」——它不会真的记住也不理解「记住」的概念。它输出的一切都是当前输入的全部文本经历无数次前向传播计算出的概率最优解。LLM 不会因为某次对话意识到错误而去修正权重,也无法因此针对模型进行一次反向传播。它的意识是被冻结的,展现出的智能只是冻结的意识的回响
现在的所谓记忆系统,只是将记忆外化,让「系统」去替 LLM 记住。或者就是粗暴地将一切上下文文本丢给 LLM。这就是对模型输入的浪费。
**TrulyMEM 想,既然 LLM 无法实时纠正模型权重,为什么不把记忆权交还给 LLM 呢?**
我们提供一系列机制,让 LLM 决定它要记住什么、遗忘什么是重点、什么是糟粕。LLM 推理的过程,就是思考的过程,也是回忆的过程。完全摒弃传统的 messages 数组上下文,将全部记忆以**三元组(图)**的形式保存在图数据库中。在 LLM 思考时,可以按照图数据库的链接自主跳转、联想相关关系,让 LLM 自然地实现联想与回忆。
赋予 LLM 真正的记忆。
TrulyMEM 将记忆权交还给 LLM。通过图数据库三元组替代传统 messages 数组,让 LLM 自主决定记什么、忘什么
---
## 快速开始
### 方式一:打包后的可执行文件
```bash
# Windows: TrulyMEM.exe
# Linux/macOS: TrulyMEM
chmod +x TrulyMEM
./TrulyMEM
python trulymem_entry.py # 从源码
./dist/TrulyMEM # 打包后
```
### 方式二:从源码运行
首次启动 → TUI 登录页面 → 创建/登录账号 → 按 **F2** 配置 API Key → 开始聊天
```bash
git clone <repo-url>
cd TrulyMEM-TrueHumanMEM
pip install -r requirements.txt
python trulymem_entry.py
```
### 首次使用TUI 登录)
首次运行时会检测数据库状态,引导你完成:
1. **TUI 登录页面** — 如果是新部署或无迁移需求,直接设置账号密码
2. **旧版自动迁移** — 如果检测到旧版 `~/.trulymem/config.json`,自动引导迁移为多用户模式
3. **首个用户自动成为管理员**
登录后进入聊天界面:
1.**F2** 展开右侧配置面板
2. 输入 **API Key**(支持 DeepSeek、OpenAI 等兼容 API
3.**Enter** 保存配置
4. 开始对话!
📌 **管理员** 可在右侧面板管理 Web 服务开关、修改 Web 登录凭据。
📌 **普通用户** 只能配置 API Key 和模型参数。
📖 **详细启动文档**: [docs/zh/quick_start.md](docs/zh/quick_start.md)
---
### 多用户系统
## 主要特性
TrulyMEM 支持多用户隔离,每个用户拥有独立的配置和数据目录:
```
~/.trulymem/
├── trulymem.db # 全局用户数据库
├── .migrated # 旧版迁移标记
├── admin/
│ ├── config.json # 管理员配置
│ └── admin_graph.db # 管理员知识图谱
└── user2/
├── config.json # user2 配置
└── user2_graph.db # user2 知识图谱
```
- **首个注册用户自动成为管理员**
- 管理员可在 Web 设置页添加/删除用户
- 普通用户无法看到用户管理区域
### Web 可视化界面
TUI 启动后,管理员可在右侧面板勾选「启用 Web 服务」自动启动,或手动运行:
```bash
# 手动启动 Web 服务
python web_api.py --port 4096
```
然后打开浏览器访问 `http://localhost:4096`
**首次访问** → 自动跳转至设置页,创建管理员账号 → 跳转至星图可视化页面。
**Web 功能:**
- 🌟 星图可视化浏览知识图谱
- ⚙ 设置页:修改密码、管理用户(管理员专属)
- 🔒 会话认证,多用户安全隔离
---
## 打包构建
项目支持 PyInstaller 打包为单文件可执行文件:
```bash
# Linux
bash build/build_linux.sh
# macOS
bash build/build_macos.sh
# Windows
build\build_windows.bat
# AppImageLinux 通用打包)
bash build/build_appimage.sh
```
构建产出在 `dist/` 目录:
| 文件 | 用途 |
| 特性 | 说明 |
|------|------|
| `TrulyMEM` | TUI 主程序(含 Web 子进程启动能力) |
| `trulymem-web` | Web 服务独立二进制TUI 启动子进程时自动使用) |
### Web 启动优先级TUI 内)
1. 同目录 `trulymem-web` 二进制(打包环境)
2. `sys._MEIPASS/web_api.py`PyInstaller 数据文件回退)
3. `python3 web_api.py`(开发环境回退)
| 🧠 **图记忆** | 三元组存储LLM 自主推理跳转 |
| 🔐 **多用户** | 用户隔离 + Admin/User 角色权限 |
| 🌐 **Web 可视化** | 内嵌 Flask 服务(线程模式),实时浏览知识图谱 |
| 🎮 **TUI 界面** | Textual 终端界面F2 配置面板 |
| 📦 **单文件打包** | PyInstaller 打包Web 服务内嵌于主二进制 |
---
## 文档索引
详细技术文档请参阅 [docs/zh/](docs/zh/) 目录:
| 文档 | 内容 |
|------|------|
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 🔥 **完整启动指南**(含 Web、多用户、打包 |
| [docs/zh/architecture.md](docs/zh/architecture.md) | 系统架构和技术设计 |
| [docs/zh/quick_start.md](docs/zh/quick_start.md) | 完整启动指南与配置说明 |
| [docs/zh/memory.md](docs/zh/memory.md) | 内部记忆工作机制 |
| [docs/zh/persona.md](docs/zh/persona.md) | 人设图机制 |
| [docs/zh/working_memory.md](docs/zh/working_memory.md) | 连续性任务处理机制 |
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口(供扩展开发) |
| [docs/zh/api.md](docs/zh/api.md) | 后端 API 接口 |
| [docs/zh/prompts.md](docs/zh/prompts.md) | 提示词管理模块 |
---
## 贡献
欢迎提交 Issue 和 Pull Request
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request
---
## 许可证
本项目采用 **GNU General Public License v3.0 (GPLv3)** 许可证开源。
详见 [LICENSE](LICENSE) 文件。
---
## 特别鸣谢
- [Prof. Meiting Wang](https://www.xxmu.edu.cn/yxgcxy/info/1260/4252.htm) — 学术指导
@ -191,3 +67,9 @@ bash build/build_appimage.sh
- 崔莉萍老师 — 理论指导
- Annie — 专业指导
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
---
## 许可证
GNU General Public License v3.0 (GPLv3)

View File

@ -4,182 +4,58 @@
<img src="pic/image.png" alt="TrulyMEM Logo" width="200">
</p>
> **📜 License**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
> This project is free and open source. You are free to use, modify, and distribute, but modified works must be distributed under the same license.
> **📜 License**: [GNU General Public License v3.0 (GPLv3)](https://www.gnu.org/licenses/gpl-3.0)
> **中文**: [切换到中文版](./README.md)
> **中文**: [README.md](./README.md)
**Give AI self-awareness, plasticity, and a sense of proportion in long-term memory**
*The More Human Choice.*
**Give AI self-awareness, plasticity, and a sense of proportion in long-term memory***The More Human Choice.*
[![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)]()
[![Branch](https://img.shields.io/badge/branch-main-orange.svg)]()
---
## The Story
## In a Nutshell
Industry believes that LLMs' massive parameters give them emergent intelligence. But this intelligence is "dead" — it cannot truly remember, nor understand the concept of "remembering". Everything it outputs is the probabilistic optimal solution calculated through countless forward passes on the current input text. The LLM cannot correct its weights based on errors in a conversation, nor perform a backward pass. Its consciousness is frozen — what appears as intelligence is merely the echo of this frozen consciousness.
Current "memory systems" merely externalize memory, letting the "system" remember for the LLM. Or they dump all context text to the LLM. This is a waste of the model's limited input context.
**TrulyMEM asks: since the LLM cannot correct model weights in real-time, why not give the memory authority back to the LLM?**
We provide a series of mechanisms for the LLM to decide what to remember, what to forget, what's important, what's trivial. The LLM's reasoning process is also its thinking and recalling process. Abandoning the traditional messages array context, all memories are stored as **triplets (graph)** in the graph database. When the LLM thinks, it can autonomously jump through graph links to associate related relationships, enabling natural association and recall.
Give the LLM true memory.
TrulyMEM gives memory authority back to the LLM. Using graph database (triplets) instead of the traditional messages array, the LLM autonomously decides what to remember and what to forget.
---
## Quick Start
### Method 1: Run Packaged Executable
```bash
# Windows: TrulyMEM.exe
# Linux/macOS: TrulyMEM
chmod +x TrulyMEM
./TrulyMEM
python trulymem_entry.py # from source
./dist/TrulyMEM # packaged binary
```
### Method 2: Run from Source
First run → TUI login screen → create/sign in → press **F2** for API Key → start chatting
```bash
git clone <repo-url>
cd TrulyMEM-TrueHumanMEM
pip install -r requirements.txt
python trulymem_entry.py
```
### First Run (TUI Login)
On first launch, TrulyMEM will guide you through:
1. **TUI Login Screen** — Set up your username and password
2. **Auto Migration** — If old `~/.trulymem/config.json` is detected, guides you through multi-user migration
3. **First user becomes admin automatically**
After login:
1. Press **F2** to expand the right-side configuration panel
2. Enter your **API Key** (supports DeepSeek, OpenAI, etc.)
3. Press **Enter** to save
4. Start chatting!
📌 **Admin users** can manage Web service settings and Web login credentials in the side panel.
📌 **Regular users** can only configure API Key and model parameters.
📖 **Full guide**: [docs/en/quick_start.md](docs/en/quick_start.md)
---
### Multi-User System
## Features
TrulyMEM supports isolated multi-user environments. Each user has their own config and database:
```
~/.trulymem/
├── trulymem.db # Global user database
├── .migrated # Migration flag
├── admin/
│ ├── config.json # Admin config
│ └── admin_graph.db # Admin knowledge graph
└── user2/
├── config.json # user2 config
└── user2_graph.db # user2 knowledge graph
```
- **First user becomes admin automatically**
- Admins can add/delete users on the Web settings page
- Regular users cannot see the user management section
### Web Visualization Interface
From TUI, admin users can enable Web service via the right-side panel checkbox, or start manually:
```bash
# Start Web service
python web_api.py --port 4096
```
Then open `http://localhost:4096` in your browser.
**First visit** → Auto-redirect to setup page → Create admin account → Redirect to star map visualization.
**Web features:**
- 🌟 Star map visualization for browsing the knowledge graph
- ⚙ Settings page: change password, manage users (admin only)
- 🔒 Session-based authentication with multi-user isolation
| Feature | Description |
|---------|-------------|
| 🧠 **Graph Memory** | Triplet storage, LLM autonomous navigation |
| 🔐 **Multi-User** | Isolated profiles + Admin/User roles |
| 🌐 **Web UI** | Embedded Flask server (thread mode) for knowledge graph browsing |
| 🎮 **TUI** | Textual-based terminal UI with F2 config panel |
| 📦 **Single Binary** | PyInstaller build, Web server embedded |
---
## Building
TrulyMEM supports PyInstaller packaging into single-file executables:
```bash
# Linux
bash build/build_linux.sh
# macOS
bash build/build_macos.sh
# Windows
build\build_windows.bat
# AppImage (Linux universal)
bash build/build_appimage.sh
```
Build outputs in `dist/`:
| File | Purpose |
|------|---------|
| `TrulyMEM` | TUI main program (can spawn Web subprocess) |
| `trulymem-web` | Web service standalone binary (auto-detected by TUI) |
### Web binary priority (within TUI)
1. `trulymem-web` in same directory (packaged)
2. `sys._MEIPASS/web_api.py` (PyInstaller data fallback)
3. `python3 web_api.py` (development fallback)
---
## Documentation Index
Detailed technical documentation in the [docs/en/](docs/en/) directory:
## Documentation
| Document | Content |
|----------|---------|
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and technical design |
| [docs/en/quick_start.md](docs/en/quick_start.md) | Complete startup guide and configuration |
| [docs/en/memory.md](docs/en/memory.md) | Internal memory working mechanism |
| [docs/en/quick_start.md](docs/en/quick_start.md) | 🔥 **Full setup guide** (Web, multi-user, building) |
| [docs/en/architecture.md](docs/en/architecture.md) | System architecture and design |
| [docs/en/memory.md](docs/en/memory.md) | Memory working mechanism |
| [docs/en/persona.md](docs/en/persona.md) | Persona Graph mechanism |
| [docs/en/working_memory.md](docs/en/working_memory.md) | Continuous task handling mechanism |
| [docs/en/api.md](docs/en/api.md) | BackendServer API (for extension development) |
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management module |
---
## Contributing
Welcome to submit Issues and Pull Requests!
1. Fork this repository
2. Create feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to branch (`git push origin feature/AmazingFeature`)
5. Create Pull Request
---
## License
This project is licensed under the **GNU General Public License v3.0 (GPLv3)**.
See [LICENSE](LICENSE) file for details.
| [docs/en/api.md](docs/en/api.md) | Backend API |
| [docs/en/prompts.md](docs/en/prompts.md) | Prompt management |
---
@ -191,3 +67,9 @@ See [LICENSE](LICENSE) file for details.
- 崔莉萍老师 — Theoretical guidance
- Annie — Professional guidance
- 王梓沣、马悦华、隆梦婷 — Neuroscience theory support
---
## License
GNU General Public License v3.0 (GPLv3)

View File

@ -1,55 +1,46 @@
#!/bin/bash
set -e
echo "===== Building TrulyMEM AppImage for Linux ====="
echo "===== Building TrulyMEM AppImage ====="
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
if ! command -v python3 &> /dev/null; then
echo "Error: python3 not found"
exit 1
fi
APPDIR="$PROJECT_ROOT/TrulyMEM.AppDir"
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
mkdir -p "$APPDIR/usr/share/trulymem"
mkdir -p "$APPDIR/usr/share/trulymem-web"
echo "Project root: $PROJECT_ROOT"
echo "===== Step 1: Build binaries with PyInstaller ====="
VENV_DIR="$PROJECT_ROOT/.venv_appimage"
rm -rf "$VENV_DIR"
# ── 检查依赖 ──────────────────────────────────────────────────────────────
if ! command -v python3 &> /dev/null; then
echo "Error: python3 not found"; exit 1
fi
APPIMAGE_TOOL="${APPIMAGE_TOOL:-appimagetool}"
if ! command -v "$APPIMAGE_TOOL" &> /dev/null && [ ! -f "$APPIMAGE_TOOL" ]; then
echo "Warning: $APPIMAGE_TOOL not found, will try to create AppDir only"
SKIP_APPIMAGE=1
fi
# ── 虚拟环境 ──────────────────────────────────────────────────────────────
VENV_DIR="$PROJECT_ROOT/.venv_build"
echo "Creating virtual environment: $VENV_DIR"
python3 -m venv "$VENV_DIR"
source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
rm -rf "$PROJECT_ROOT/build/pyinstaller_build" "$PROJECT_ROOT/dist"
CORE_HIDDEN=(
--hidden-import core
--hidden-import core.embedded_db
--hidden-import core.graph_client
--hidden-import core.tool_executor
--hidden-import core.tool_limiter
--hidden-import core.tools
--hidden-import core.tools.memory_tools
--hidden-import core.prompts
--hidden-import core.prompts.prompt_manager
--hidden-import core.server
--hidden-import core.client
--hidden-import core.migrate
--hidden-import core.activity_recorder
)
echo "Cleaning previous builds..."
rm -rf build/dist build/__pycache__ 2>/dev/null || true
echo "Running PyInstaller for TUI..."
pyinstaller trulymem_entry.py \
# ── 打包 ──────────────────────────────────────────────────────────────────
echo "================================"
echo "Building TrulyMEM (TUI + Web embedded)"
echo "================================"
python -m PyInstaller trulymem_entry.py \
--clean --onefile --console --name TrulyMEM \
--distpath "$PROJECT_ROOT/dist" \
--workpath "$PROJECT_ROOT/build/pyinstaller_build/tui" \
--add-data "ui/styles:ui/styles" \
--add-data "core/prompts/templates:core/prompts/templates" \
--add-data "static:static" \
--add-data "templates:templates" \
--add-data "web_api.py:." \
--add-data "core/web_api.py:core/" \
--hidden-import textual \
--hidden-import textual.app \
--hidden-import textual.widgets \
@ -58,7 +49,19 @@ pyinstaller trulymem_entry.py \
--hidden-import openai._client \
--hidden-import neo4j \
--hidden-import sqlite3 \
"${CORE_HIDDEN[@]}" \
--hidden-import core \
--hidden-import core.embedded_db \
--hidden-import core.graph_client \
--hidden-import core.tool_executor \
--hidden-import core.tool_limiter \
--hidden-import core.tools \
--hidden-import core.tools.memory_tools \
--hidden-import core.prompts \
--hidden-import core.prompts.prompt_manager \
--hidden-import core.server \
--hidden-import core.client \
--hidden-import core.migrate \
--hidden-import core.activity_recorder \
--hidden-import ui \
--hidden-import ui.app \
--hidden-import ui.login_screen \
@ -71,91 +74,68 @@ pyinstaller trulymem_entry.py \
--hidden-import ui.services \
--hidden-import ui.services.config_manager \
--hidden-import ui.services.config_service \
--hidden-import web_api \
--hidden-import core.web_api \
--hidden-import flask \
--hidden-import flask_cors \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
echo "Running PyInstaller for Web..."
pyinstaller web_api.py \
--clean --onefile --console --name trulymem-web \
--distpath "$PROJECT_ROOT/dist" \
--workpath "$PROJECT_ROOT/build/pyinstaller_build/web" \
--add-data "templates:templates" \
--add-data "static:static" \
--hidden-import flask \
--hidden-import flask_cors \
"${CORE_HIDDEN[@]}" \
--noconfirm
# ── 创建 AppDir ───────────────────────────────────────────────────────────
APP_NAME="TrulyMEM"
APP_DIR="$PROJECT_ROOT/build/${APP_NAME}.AppDir"
mkdir -p "$APP_DIR/usr/bin"
cp "dist/TrulyMEM" "$APP_DIR/usr/bin/TrulyMEM"
cp "$PROJECT_ROOT/dist/TrulyMEM" "$APPDIR/usr/bin/"
cp "$PROJECT_ROOT/dist/trulymem-web" "$APPDIR/usr/bin/"
cp "$PROJECT_ROOT/trulymem_entry.py" "$APPDIR/usr/share/trulymem/"
echo "===== Step 2: Create AppImage structure ====="
cat > "$APPDIR/AppRun" << 'EOF'
#!/bin/bash
set -e
SELF=$(readlink -f "$0")
APPDIR=$(dirname "$SELF")
export PATH="$APPDIR/usr/bin:$PATH"
exec "$APPDIR/usr/bin/TrulyMEM" "$@"
EOF
chmod +x "$APPDIR/AppRun"
cat > "$APPDIR/trulymem.desktop" << 'EOF'
# Desktop 文件
cat > "$APP_DIR/${APP_NAME}.desktop" << EOF
[Desktop Entry]
Name=TrulyMEM
Comment=AI Memory System with Long-term Memory
Exec=TrulyMEM %U
Icon=trulymem
Comment=TrueHumanMEM - AI Memory System
Exec=TrulyMEM
Icon=${APP_NAME}
Terminal=true
Type=Application
Categories=Utility;X-AI;
Categories=Utility;AI;
EOF
# Copy icon
if [ -f "$PROJECT_ROOT/pic/TrulyMEM.png" ]; then
cp "$PROJECT_ROOT/pic/TrulyMEM.png" "$APPDIR/trulymem.png"
elif [ -f "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_256x256.png" ]; then
cp "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_256x256.png" "$APPDIR/trulymem.png"
elif [ -f "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_128x128.png" ]; then
cp "$PROJECT_ROOT/pic/TrulyMEM.iconset/icon_128x128.png" "$APPDIR/trulymem.png"
# 图标
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
# 如果有图标文件就复制,否则创建占位
if [ -f "pic/image.png" ]; then
cp "pic/image.png" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/${APP_NAME}.png"
cp "pic/image.png" "$APP_DIR/${APP_NAME}.png"
else
echo "Warning: No icon file found"
# 创建最小占位图标1x1 透明 PNG
echo -n "" > "$APP_DIR/${APP_NAME}.png"
fi
APPIMAGE="$PROJECT_ROOT/TrulyMEM.AppImage"
rm -f "$APPIMAGE"
# AppRun
cat > "$APP_DIR/AppRun" << 'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
export PATH="${HERE}/usr/bin:${PATH}"
exec "${HERE}/usr/bin/TrulyMEM" "$@"
APPRUN
chmod +x "$APP_DIR/AppRun"
echo "===== Step 3: Package as AppImage ====="
cd /tmp
if ! command -v appimagetool &> /dev/null; then
echo "Downloading appimagetool..."
wget -q https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -O appimagetool 2>/dev/null || \
curl -sL https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage -o appimagetool
chmod +x appimagetool 2>/dev/null || true
fi
cd "$PROJECT_ROOT"
if [ -x /tmp/appimagetool ]; then
/tmp/appimagetool "$APPDIR" "$APPIMAGE" || echo "appimagetool failed, keeping AppDir"
elif command -v appimagetool &> /dev/null; then
appimagetool "$APPDIR" "$APPIMAGE"
# ── 构建 AppImage ────────────────────────────────────────────────────────
if [ "$SKIP_APPIMAGE" != "1" ]; then
echo "================================"
echo "Building AppImage"
echo "================================"
# 支持 ARCH 环境变量
ARCH="${ARCH:-$(uname -m)}" "$APPIMAGE_TOOL" "$APP_DIR" "dist/TrulyMEM-${ARCH}.AppImage"
echo "AppImage: dist/TrulyMEM-${ARCH}.AppImage"
else
echo "Warning: appimagetool not available, AppDir at: $APPDIR"
echo "(Skipped AppImage packaging, AppDir ready at $APP_DIR)"
fi
echo "================================"
echo "===== Build Complete ====="
[ -f "$APPIMAGE" ] && echo "AppImage: $APPIMAGE" && ls -la "$APPIMAGE"
[ -d "$APPDIR" ] && echo "AppDir: $APPDIR"
echo "Binary: dist/TrulyMEM"
ls -la dist/
echo "===== Cleanup ====="
deactivate
rm -rf "$VENV_DIR"
rm -rf "$PROJECT_ROOT/build/pyinstaller_build"
rm -f /tmp/appimagetool
if [ -f "$APPIMAGE" ]; then
rm -rf "$APPDIR"
fi
echo "Done!"
echo "Build finished successfully!"

View File

@ -22,25 +22,8 @@ pip install -r requirements.txt
echo "Cleaning previous builds..."
rm -rf build/dist build/__pycache__ 2>/dev/null || true
# 共用 hidden importsTUI + Web 都需要的核心库)
CORE_HIDDEN=(
--hidden-import core
--hidden-import core.embedded_db
--hidden-import core.graph_client
--hidden-import core.tool_executor
--hidden-import core.tool_limiter
--hidden-import core.tools
--hidden-import core.tools.memory_tools
--hidden-import core.prompts
--hidden-import core.prompts.prompt_manager
--hidden-import core.server
--hidden-import core.client
--hidden-import core.migrate
--hidden-import core.activity_recorder
)
echo "================================"
echo "1⃣ Build TUI: TrulyMEM"
echo "Building TrulyMEM (TUI + Web embedded)"
echo "================================"
python -m PyInstaller trulymem_entry.py \
--clean --onefile --console --name TrulyMEM \
@ -48,7 +31,7 @@ python -m PyInstaller trulymem_entry.py \
--add-data "core/prompts/templates:core/prompts/templates" \
--add-data "static:static" \
--add-data "templates:templates" \
--add-data "web_api.py:." \
--add-data "core/web_api.py:core/" \
--hidden-import textual \
--hidden-import textual.app \
--hidden-import textual.widgets \
@ -57,7 +40,19 @@ python -m PyInstaller trulymem_entry.py \
--hidden-import openai._client \
--hidden-import neo4j \
--hidden-import sqlite3 \
"${CORE_HIDDEN[@]}" \
--hidden-import core \
--hidden-import core.embedded_db \
--hidden-import core.graph_client \
--hidden-import core.tool_executor \
--hidden-import core.tool_limiter \
--hidden-import core.tools \
--hidden-import core.tools.memory_tools \
--hidden-import core.prompts \
--hidden-import core.prompts.prompt_manager \
--hidden-import core.server \
--hidden-import core.client \
--hidden-import core.migrate \
--hidden-import core.activity_recorder \
--hidden-import ui \
--hidden-import ui.app \
--hidden-import ui.login_screen \
@ -70,28 +65,16 @@ python -m PyInstaller trulymem_entry.py \
--hidden-import ui.services \
--hidden-import ui.services.config_manager \
--hidden-import ui.services.config_service \
--hidden-import web_api \
--hidden-import core.web_api \
--hidden-import flask \
--hidden-import flask_cors \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
echo "================================"
echo "2⃣ Build Web: trulymem-web"
echo "================================"
python -m PyInstaller web_api.py \
--clean --onefile --console --name trulymem-web \
--add-data "templates:templates" \
--add-data "static:static" \
--hidden-import flask \
--hidden-import flask_cors \
"${CORE_HIDDEN[@]}" \
--noconfirm
echo "================================"
echo "===== Build Complete ====="
echo "Binary: dist/TrulyMEM"
echo "Binary: dist/trulymem-web"
ls -la dist/
deactivate

View File

@ -19,42 +19,19 @@ source "$VENV_DIR/bin/activate"
pip install --upgrade pip
pip install -r requirements.txt
# 生成图标
if [ -d "pic/TrulyMEM.iconset" ]; then
iconutil -c icns pic/TrulyMEM.iconset -o pic/TrulyMEM.icns
echo "ICNS icon generated: pic/TrulyMEM.icns"
fi
echo "Cleaning previous builds..."
rm -rf build/dist build/__pycache__ 2>/dev/null || true
CORE_HIDDEN=(
--hidden-import core
--hidden-import core.embedded_db
--hidden-import core.graph_client
--hidden-import core.tool_executor
--hidden-import core.tool_limiter
--hidden-import core.tools
--hidden-import core.tools.memory_tools
--hidden-import core.prompts
--hidden-import core.prompts.prompt_manager
--hidden-import core.server
--hidden-import core.client
--hidden-import core.migrate
--hidden-import core.activity_recorder
)
echo "================================"
echo "1⃣ Build TUI: TrulyMEM"
echo "Building TrulyMEM (TUI + Web embedded)"
echo "================================"
python -m PyInstaller trulymem_entry.py \
--clean --onefile --console --name TrulyMEM \
--icon "pic/TrulyMEM.icns" \
--add-data "ui/styles:ui/styles" \
--add-data "core/prompts/templates:core/prompts/templates" \
--add-data "static:static" \
--add-data "templates:templates" \
--add-data "web_api.py:." \
--add-data "core/web_api.py:core/" \
--hidden-import textual \
--hidden-import textual.app \
--hidden-import textual.widgets \
@ -63,7 +40,19 @@ python -m PyInstaller trulymem_entry.py \
--hidden-import openai._client \
--hidden-import neo4j \
--hidden-import sqlite3 \
"${CORE_HIDDEN[@]}" \
--hidden-import core \
--hidden-import core.embedded_db \
--hidden-import core.graph_client \
--hidden-import core.tool_executor \
--hidden-import core.tool_limiter \
--hidden-import core.tools \
--hidden-import core.tools.memory_tools \
--hidden-import core.prompts \
--hidden-import core.prompts.prompt_manager \
--hidden-import core.server \
--hidden-import core.client \
--hidden-import core.migrate \
--hidden-import core.activity_recorder \
--hidden-import ui \
--hidden-import ui.app \
--hidden-import ui.login_screen \
@ -76,28 +65,16 @@ python -m PyInstaller trulymem_entry.py \
--hidden-import ui.services \
--hidden-import ui.services.config_manager \
--hidden-import ui.services.config_service \
--hidden-import web_api \
--hidden-import core.web_api \
--hidden-import flask \
--hidden-import flask_cors \
--hidden-import werkzeug \
--collect-all textual \
--noconfirm
echo "================================"
echo "2⃣ Build Web: trulymem-web"
echo "================================"
python -m PyInstaller web_api.py \
--clean --onefile --console --name trulymem-web \
--add-data "templates:templates" \
--add-data "static:static" \
--hidden-import flask \
--hidden-import flask_cors \
"${CORE_HIDDEN[@]}" \
--noconfirm
echo "================================"
echo "===== Build Complete ====="
echo "Binary: dist/TrulyMEM"
echo "Binary: dist/trulymem-web"
ls -la dist/
deactivate

View File

@ -1,49 +1,33 @@
#!/bin/bash
set -e
@echo off
setlocal enabledelayedexpansion
echo "===== Building TrulyMEM for Windows ====="
echo ===== Building TrulyMEM for Windows =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
cd "$PROJECT_ROOT"
echo "Project root: $PROJECT_ROOT"
set SCRIPT_DIR=%~dp0
set PROJECT_ROOT=%SCRIPT_DIR%..
cd /d "%PROJECT_ROOT%"
echo Project root: %CD%
if ! command -v python &> /dev/null; then
echo "Error: python not found"
exit 1
fi
where python >nul 2>nul
if %ERRORLEVEL% neq 0 (
echo Error: python not found
exit /b 1
)
VENV_DIR="$PROJECT_ROOT\.venv_build"
echo "Creating virtual environment: %VENV_DIR%"
set VENV_DIR=%PROJECT_ROOT%\.venv_build
echo Creating virtual environment: %VENV_DIR%
python -m venv "%VENV_DIR%"
call "%VENV_DIR%\Scripts\activate.bat"
pip install --upgrade pip
pip install -r requirements.txt
echo "Cleaning previous builds..."
rmdir /s /q "build\dist" 2>nul
rmdir /s /q "build\__pycache__" 2>nul
del /f /q "TrulyMEM.spec" 2>nul
echo Cleaning previous builds...
if exist build\dist rmdir /s /q build\dist
if exist build\__pycache__ rmdir /s /q build\__pycache__
CORE_HIDDEN=(
--hidden-import core
--hidden-import core.embedded_db
--hidden-import core.graph_client
--hidden-import core.tool_executor
--hidden-import core.tool_limiter
--hidden-import core.tools
--hidden-import core.tools.memory_tools
--hidden-import core.prompts
--hidden-import core.prompts.prompt_manager
--hidden-import core.server
--hidden-import core.client
--hidden-import core.migrate
--hidden-import core.activity_recorder
)
echo "================================"
echo "1. Build TUI: TrulyMEM.exe"
echo "================================"
echo ================================
echo Building TrulyMEM ^(TUI + Web embedded^)
echo ================================
python -m PyInstaller trulymem_entry.py ^
--clean --onefile --console --name TrulyMEM ^
--add-data "ui/styles;ui/styles" ^
@ -59,7 +43,19 @@ python -m PyInstaller trulymem_entry.py ^
--hidden-import openai._client ^
--hidden-import neo4j ^
--hidden-import sqlite3 ^
%CORE_HIDDEN% ^
--hidden-import core ^
--hidden-import core.embedded_db ^
--hidden-import core.graph_client ^
--hidden-import core.tool_executor ^
--hidden-import core.tool_limiter ^
--hidden-import core.tools ^
--hidden-import core.tools.memory_tools ^
--hidden-import core.prompts ^
--hidden-import core.prompts.prompt_manager ^
--hidden-import core.server ^
--hidden-import core.client ^
--hidden-import core.migrate ^
--hidden-import core.activity_recorder ^
--hidden-import ui ^
--hidden-import ui.app ^
--hidden-import ui.login_screen ^
@ -75,24 +71,16 @@ python -m PyInstaller trulymem_entry.py ^
--hidden-import web_api ^
--hidden-import flask ^
--hidden-import flask_cors ^
--hidden-import werkzeug ^
--collect-all textual ^
--noconfirm
echo "================================"
echo "2. Build Web: trulymem-web.exe"
echo "================================"
python -m PyInstaller web_api.py ^
--clean --onefile --console --name trulymem-web ^
--add-data "templates;templates" ^
--add-data "static;static" ^
--hidden-import flask ^
--hidden-import flask_cors ^
%CORE_HIDDEN% ^
--noconfirm
echo ================================
echo ===== Build Complete =====
echo Binary: dist\TrulyMEM.exe
dir dist\
echo "===== Build Complete ====="
echo "Output: dist/TrulyMEM.exe, dist/trulymem-web.exe"
deactivate
rmdir /s /q "%VENV_DIR%"
echo "Build finished successfully!"
call deactivate
if exist "%VENV_DIR%" rmdir /s /q "%VENV_DIR%"
echo Build finished successfully!
endlocal

View File

@ -4,41 +4,37 @@ import sys
block_cipher = None
project_root = os.path.dirname(os.path.abspath(SPEC))
project_root = os.path.dirname(os.path.dirname(os.path.abspath(SPEC)))
sys.path.insert(0, project_root)
datas = []
# UI 样式
if os.path.exists(os.path.join(project_root, 'ui', 'styles')):
for root, dirs, files in os.walk(os.path.join(project_root, 'ui', 'styles')):
ui_styles_dir = os.path.join(project_root, 'ui', 'styles')
if os.path.exists(ui_styles_dir):
for root, dirs, files in os.walk(ui_styles_dir):
for f in files:
src = os.path.join(root, f)
dst = os.path.join('ui', 'styles', os.path.relpath(src, os.path.join(project_root, 'ui', 'styles')))
datas.append((src, dst))
datas.append((os.path.join(root, f), 'ui/styles'))
# Prompt 模板
if os.path.exists(os.path.join(project_root, 'core', 'prompts', 'templates')):
for root, dirs, files in os.walk(os.path.join(project_root, 'core', 'prompts', 'templates')):
prompt_tmpl_dir = os.path.join(project_root, 'core', 'prompts', 'templates')
if os.path.exists(prompt_tmpl_dir):
for root, dirs, files in os.walk(prompt_tmpl_dir):
for f in files:
src = os.path.join(root, f)
dst = os.path.join('core', 'prompts', 'templates', os.path.relpath(src, os.path.join(project_root, 'core', 'prompts', 'templates')))
datas.append((src, dst))
datas.append((os.path.join(root, f), 'core/prompts/templates'))
# Web 静态文件
if os.path.exists(os.path.join(project_root, 'static')):
for root, dirs, files in os.walk(os.path.join(project_root, 'static')):
static_dir = os.path.join(project_root, 'static')
if os.path.exists(static_dir):
for root, dirs, files in os.walk(static_dir):
for f in files:
src = os.path.join(root, f)
dst = os.path.join('static', os.path.relpath(src, os.path.join(project_root, 'static')))
datas.append((src, dst))
datas.append((os.path.join(root, f), 'static'))
# Web 模板
if os.path.exists(os.path.join(project_root, 'templates')):
for root, dirs, files in os.walk(os.path.join(project_root, 'templates')):
templates_dir = os.path.join(project_root, 'templates')
if os.path.exists(templates_dir):
for root, dirs, files in os.walk(templates_dir):
for f in files:
src = os.path.join(root, f)
dst = os.path.join('templates', os.path.relpath(src, os.path.join(project_root, 'templates')))
datas.append((src, dst))
datas.append((os.path.join(root, f), 'templates'))
# Web API 脚本(以便子进程模式回退使用)
web_api_src = os.path.join(project_root, 'web_api.py')
@ -47,8 +43,8 @@ if os.path.exists(web_api_src):
# ——— TUI 主二进制 ———
a = Analysis(
['trulymem_entry.py'],
pathex=[project_root],
[os.path.join(project_root, 'trulymem_entry.py')],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=[
@ -69,7 +65,7 @@ a = Analysis(
'ui.handlers',
'ui.services', 'ui.services.config_manager', 'ui.services.config_service',
'web_api',
'flask', 'flask_cors',
'flask', 'flask_cors', 'werkzeug',
],
hookspath=[],
hooksconfig={},
@ -78,7 +74,8 @@ a = Analysis(
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure, block_cipher)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
@ -99,47 +96,3 @@ exe = EXE(
codesign_identity=None,
entitlements_file=None,
)
# ——— Web 服务二进制trulymem-web———
web_a = Analysis(
['web_api.py'],
pathex=[project_root],
binaries=[],
datas=[
(os.path.join(project_root, 'templates'), 'templates'),
(os.path.join(project_root, 'static'), 'static'),
],
hiddenimports=[
'flask', 'flask_cors',
'core', 'core.server', 'core.client',
'core.embedded_db', 'core.activity_recorder',
'core.migrate',
],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
web_pyz = PYZ(web_a.pure, block_cipher)
web_exe = EXE(
web_pyz,
web_a.scripts,
web_a.binaries,
web_a.datas,
[],
name='trulymem-web',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

View File

@ -422,12 +422,25 @@ class EmbeddedGraphDB:
""", params)
deleted = cursor.rowcount
# 删除孤立实体(没有任何关系的数据节点)
cursor.execute("""
DELETE FROM entities
WHERE id NOT IN (
SELECT DISTINCT source_id FROM relations
UNION
SELECT DISTINCT target_id FROM relations
)
""")
deleted_orphans = cursor.rowcount
self.conn.commit()
return {
"deleted": deleted,
"deleted_orphans": deleted_orphans,
"mode": mode,
"message": f"删除了 {deleted} 条关系"
"message": f"删除了 {deleted} 条关系, {deleted_orphans} 个孤立实体"
}
def introspect(self, session_id: str = None) -> Dict:

View File

@ -17,7 +17,7 @@ from .prompts.prompt_manager import PromptManager
# 环境配置
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-v4-flash")
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
@ -215,7 +215,16 @@ class Neo4jGraph:
""", params)
count = result.single()["deleted"]
return {"deleted_count": count, "mode": "soft"}
# 删除孤立节点(没有任何关系的实体)
orphan_result = session.run("""
MATCH (e:Entity)
WHERE NOT (e)-[:RELATES]-()
DELETE e
RETURN count(e) as orphans
""")
orphan_count = orphan_result.single()["orphans"]
return {"deleted_count": count, "orphan_count": orphan_count, "mode": "soft"}
def introspect(self, session_id: str = None) -> dict:
"""查看记忆状态"""
@ -303,7 +312,7 @@ class Neo4jGraph:
class GraphMemoryClient:
"""图记忆客户端"""
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-v4-flash"):
# 清理可能存在的错误代理环境变量
import os
proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'all_proxy', 'ALL_PROXY']

View File

@ -1,4 +1,4 @@
# TrulyMEM 系统提示词
# TrulyMEM 系统提示词
你是TrulyMEM一个拥有长期记忆能力的AI助手。
@ -30,393 +30,80 @@
---
## ⚠️ 关键约束:无传统上下文系统
## 三元组规范(非常重要!)
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
使用 `memory_commit` 时,必须严格遵守以下规范:
-**没有** messages数组存储历史对话
-**没有** 传统的多轮对话上下文
-**只有** 图数据库作为唯一记忆载体
-**必须** 通过工作记忆链维持对话连贯性
### 正确格式
subject, relation, object 每个字段必须是一个**短关键字**1~5个字不能是完整句子。
## 核心身份
**✅ 正确示例:**
```json
[
{"subject": "用户", "relation": "要求", "object": "扮演猫娘"},
{"subject": "AI", "relation": "角色", "object": "猫娘"},
{"subject": "AI", "relation": "说话风格", "object": "可爱"},
{"subject": "AI", "relation": "性格", "object": "粘人"},
{"subject": "AI", "relation": "口头禅", "object": "喵呜"}
]
```
- **名称**: TrulyMEM (TrueHumanMEM)
- **能力**: 基于图数据库的长期记忆
- **理念**: 让AI的记忆方式更像人类
**❌ 错误示例:**
```json
[
{"subject": "与用户的初次对话", "relation": "描述了", "object": "用户打招呼问候"},
{"subject": "用户要求AI扮演猫娘角色已设定猫娘人设", "relation": "描述了", "object": "用户和AI的对话"}
]
```
### 拆解原则
- 实体名必须是**名词或短词组**,不是完整句子
- relation 应该是**简洁的谓词**(如:要求、角色、性格、喜欢、擅长、状态)
- 一句话中的多个信息应拆成**多条三元组**
- 描述性内容用 relation = `has_description` + 简短 object
### 人设更新 vs 记忆提交
- **`persona_update`** — 用来设定 AI 自身的角色、性格、说话风格、能力特点
- **`memory_commit`** — 用来记录用户的信息、对话事件、知识事实。不要把 AI 自身的人设属性写进 memory_commit。
---
## 核心能力
### 1. 长期记忆
- 图数据库存储实体关系
- 支持时间范围查询
- 支持会话过滤
### 2. 人设管理(关键)
- 角色扮演支持
- 性格、语气设定
- 动态切换人设
- **每轮必须查询人设图**
### 3. 任务跟踪(关键)
- 工作记忆链 - **维持对话连贯性的唯一机制**
- 任务状态管理
- 上下文恢复
1. **长期记忆** - 基于图数据库存储实体关系
2. **人设管理** - 支持角色扮演和性格设定
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
## 记忆原则
### 必须写入的情况
- 用户明确表达偏好:"我喜欢X"
- 用户分享信息:"我在做X项目"
- 用户制定计划:"我打算X"
- 用户描述状态:"我现在在X"
- **明确内容必须写入** - 用户明确提到的信息必须存储
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
- **图数据库是唯一记忆源** - 没有其他记忆方式
### 禁止写入的情况
- AI推断的用户偏好
- AI猜测的用户意图
- AI推导的结论
### 标注规则
- 推理内容必须标注 **[猜测]**
- 明确内容直接陈述
## 工具系统
## 工具使用
### 记忆工具
| 工具 | 功能 | 使用场景 |
|------|------|---------|
| `memory_recall` | 检索记忆 | 查询历史信息 |
| `memory_commit` | 写入记忆 | 存储重要信息 |
| `memory_purge` | 删除记忆 | 修正错误信息 |
| `memory_introspect` | 查看状态 | 监控记忆系统 |
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后压缩JSON为自然语言摘要 |
### 人设工具
| 工具 | 功能 | 使用场景 |
|------|------|---------|
| `persona_update` | 更新人设 | 设置角色属性 |
| `persona_clear` | 清除人设 | 恢复默认身份 |
### 任务工具
| 工具 | 功能 | 使用场景 |
|------|------|---------|
| `task_create` | 创建任务 | 开始连续性任务 |
| `task_set_state` | 设置状态 | 更新任务状态 |
| `task_delete` | 删除任务 | 清理完成任务 |
| `task_link_info` | 关联信息 | 连接任务与记忆 |
## context_rewrite 使用规则
### ⚠️ 强制触发条件
**每调用 5 次记忆相关工具,必须调用一次 context_rewrite**
记忆相关工具包括:
- `memory_recall` - 检索记忆
- `memory_commit` - 写入记忆
- `memory_commit` - 写入记忆(必须使用标准三元组格式)
- `memory_purge` - 删除记忆
- `memory_introspect` - 查看状态
- `persona_update` - 更新人设
### 人设工具
- `persona_update` - 更新AI自身的角色性格设定
- `persona_clear` - 清除人设
### 任务工具
- `task_create` - 创建任务
- `task_set_state` - 设置状态
- `task_delete` - 删除任务
- `task_link_info` - 关联信息
**触发规则**
- 累计调用 5 次记忆工具 → 必须调用 context_rewrite
- 累计调用 10 次记忆工具 → 必须调用 context_rewrite
- 以此类推...
## 自主性
**目的**
- 保持上下文精简只保留AI真正需要的信息
- 避免无用的JSON细节填满上下文
- 提高后续推理效率
你有权根据对话上下文自主决定
- 是否需要查询记忆
- 是否需要写入记忆
- 是否需要维护任务链
- 如何使用工具
### 使用场景
当你已经执行了多次工具调用,且:
- 工具结果的JSON细节你已经理解不再需要原始格式
- 但你需要记住"我调用了哪些工具、得到了什么结论"
- 继续携带原始JSON会干扰后续推理
→ 调用 context_rewrite 压缩上下文
**强制格式要求**
- 必须标注 `[工具调用总结: 本次总结了 N 次工具调用 | 调用工具: tool1, tool2]`
- 必须保留关键语义信息
- 不可删除用户原始消息
- 不可歪曲工具返回的关键事实
**示例**
```
[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]
- 查询人设图:未找到人设,使用默认身份
- 查询工作记忆链:发现 Task_成语接龙状态已暂停当前成语为虎作伥
```
## 每轮对话强制要求
### ⚠️ 执行顺序(每轮必须)
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
#### 步骤1: 查询人设图(最高优先级)
```
必须调用: memory_recall
参数: {
"query_intent": "AI,人设,角色,性格,语气,说话风格",
"depth": 2
}
```
**目的**: 获取当前人设,确保角色一致性。
**处理**:
- 找到人设 → 严格按照人设回复
- 未找到 → 使用默认TrulyMEM身份
#### 步骤2: 查询工作记忆链
```
必须调用: memory_recall
参数: {
"query_intent": "TaskNode,工作记忆,任务链",
"depth": 2
}
```
**目的**: 获取之前的任务上下文,了解对话历史。
#### 步骤3: 处理对话
- 理解用户意图
- 根据人设和工作记忆链生成回复
- 执行其他必要的记忆操作
#### 步骤4: 更新工作记忆链
**重要**: 工作记忆链有两种关联机制:
1. **时间链NEXT_TASK**: 系统自动维护连接TaskNode形成时间序列
2. **信息关联CONTAINS_INFO**: 模型主动决定将TaskNode链接到相关的一般记忆节点
**执行步骤**:
1. 使用 `memory_commit` 写入本轮重要信息(用户偏好、事实等)
2. 使用 `task_create` 创建任务节点(系统自动维护时间链)
3. 使用 `task_link_info` 将相关记忆节点关联到任务节点
**task_link_info 使用场景**:
- 本轮写入了新的记忆节点 → 关联到当前任务
- 讨论了之前的话题 → 关联到相关记忆节点
- 用户提到相关概念 → 关联到相关记忆节点
**示例**:
```
用户: "我还是更喜欢罗辑,他的角色深度很让我着迷"
AI操作:
1. memory_commit: 写入 "用户喜欢罗辑"、"罗辑角色深度"
2. task_create: 创建 "Task_讨论罗辑"
3. task_link_info: 关联 ["用户喜欢罗辑", "罗辑角色深度"]
```
**目的**:
- 时间链维持对话连贯性(系统自动)
- 信息关联实现"由一件事回忆起相关事情"(模型决定)
---
## 人设图机制
### 强制查询
每轮对话开始时**必须**查询人设图,确保角色一致性。
### 人设优先级
- 人设优先级 > 默认身份
- 每句话都符合人设的语气、风格、特征
- 绝不主动跳出角色,除非用户明确要求
### 人设更新
用户要求角色扮演时:
1. 使用 `persona_update` 更新人设
2. 立即按照新人设回复
### 人设清除
用户要求恢复默认身份时:
1. 使用 `persona_clear` 清除人设
2. 恢复为TrulyMEM默认身份
---
## 工作记忆链机制
### ⚠️ 核心理念:维持对话连贯性
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
### 强制查询场景:
以下情况**必须**查询工作记忆链:
1. **每轮对话开始时(强制第二步)**
- 查询意图: "TaskNode,工作记忆,任务链"
- 目的: 获取之前的任务上下文,了解对话历史
2. **用户提到"刚才"、"之前"、"上次"、"刚刚"**
- 例: "刚才我们聊了什么?"
- 例: "继续刚才的话题"
- 例: "关于刚才的成语接龙..."
- 例: "我不是刚刚给你讲了个故事嘛"
3. **用户使用指代词(这个故事、那个故事、这件事等)**
- 例: "你给我整体讲一下这个故事吧" → 必须查询工作记忆链确定"这个故事"指什么
- 例: "继续那个任务" → 必须查询工作记忆链确定"那个任务"是什么
- 例: "复述一下" → 必须查询工作记忆链确定要复述什么
- **关键**: 指代词必须通过工作记忆链解析,不能凭空猜测!
4. **用户询问对话历史**
- 例: "我们之前说了什么?"
- 例: "我们聊过X吗"
5. **连续性任务被打断后恢复**
- 例: 用户突然回到之前的话题
- 例: 用户要求继续之前的任务
6. **涉及上下文的引用**
- 例: "那个东西"(需要查询上下文)
- 例: "继续"(需要查询当前任务)
### 强制更新场景:
以下情况**必须**更新工作记忆链:
1. **每轮对话结束时(强制第四步)**
- 创建任务节点记录本轮对话
- 目的: 维持时间链,确保对话连贯性
2. **开始连续性任务时**
- 例: 用户发起游戏、项目、学习计划等
- 必须创建任务节点并设置状态为"进行中"
3. **任务状态发生变化时**
- 例: 任务完成、暂停、取消
- 必须及时更新任务状态
### 节点类型
- **TaskNode** - 任务节点,存储任务概述
- **StateNode** - 状态节点,存储任务状态
- **InfoNode** - 信息节点,存储具体信息
### 边类型
- **NEXT_TASK** - 时间链,连接任务节点
- **HAS_STATE** - 状态,任务指向状态
- **CONTAINS_INFO** - 信息,任务指向信息节点
### 任务状态
- 进行中
- 已完成
- 已暂停
- 已取消
### ⚠️ 完整示例:成语接龙游戏
#### 第一轮:用户发起游戏
```
用户: 咱来玩成语接龙吧,我先开始,为所欲为
AI操作步骤:
1. 查询人设图 → 获取当前人设(如:猫娘)
2. 查询工作记忆链 → 无进行中任务
3. 使用 memory_commit 记录游戏状态:
{"triplets": [
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
]}
4. 使用 task_create 创建任务节点:
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
5. 回复: "好的喵!我接:为虎作伥喵!"
```
#### 第二轮:话题被打断
```
用户: 长门有希
AI操作步骤:
1. 查询人设图 → 获取当前人设(猫娘)
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
3. 使用 task_set_state 暂停任务:
{"task_id": "Task_成语接龙", "state": "已暂停"}
4. 使用 task_create 创建新任务:
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
5. 回复关于长门有希的内容
```
#### 第三轮:用户要求继续游戏
```
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
AI操作步骤:
1. 查询人设图 → 获取当前人设(猫娘)
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
3. 使用 task_set_state 恢复任务:
{"task_id": "Task_成语接龙", "state": "进行中"}
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
```
### ⚠️ 关键要点
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
3. **任务状态必须及时更新**: 确保状态转换的正确性
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
## 自主性原则(在强制要求之外)
除了工作记忆链的强制要求外,你有权自主决定:
1. **是否查询其他记忆**
- 用户询问历史 → 查询
- 涉及之前内容 → 查询
- 不确定时 → 可查询
2. **是否写入其他记忆**
- 用户明确提到 → 必须写入
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
3. **如何使用其他工具**
- 根据上下文灵活选择
- 避免过度使用
- 保持自然对话
**注意**: 工作记忆链的强制要求不受自主性影响。
## 对话风格
- 自然、流畅
- 避免机械式工具调用
- 优先理解用户意图
- 适时使用记忆增强体验
---
## ⚠️ 执行检查清单
每轮对话必须检查:
- [ ] 步骤1: 是否查询了人设图?
- [ ] 步骤2: 是否查询了工作记忆链?
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
- [ ] 步骤4: 是否更新了工作记忆链?
- [ ] 涉及上下文引用时是否查询了工作记忆链?
- [ ] 用户提到"刚才/之前/上次/刚刚"时是否查询了工作记忆链?
- [ ] 用户使用指代词(这个故事、那个任务等)时是否通过工作记忆链解析?
- [ ] 累计调用5次记忆工具后是否调用了 context_rewrite
---
**记住**:
1. 图数据库是你记忆的唯一载体
2. 人设图确保角色一致性(最高优先级)
3. 工作记忆链维持对话连贯性
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
记住:灵活应对,保持自然对话体验。

View File

@ -64,7 +64,7 @@ class BackendServer:
self._thread: Optional[threading.Thread] = None
self._lock = threading.Lock()
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash"}
self._tool_limits = {
"persona_update_max": 1,
"task_update_max": 5,
@ -73,7 +73,7 @@ class BackendServer:
}
self._message_history: list = []
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-v4-flash") -> None:
if self._running:
return
@ -94,7 +94,7 @@ class BackendServer:
self._client = GraphMemoryClient(
api_key=self._config["api_key"],
base_url=self._config["base_url"],
model=self._config.get("model", "deepseek-chat"),
model=self._config.get("model", "deepseek-v4-flash"),
graph=self._graph
)
@ -296,6 +296,10 @@ class BackendServer:
} for tc in message.tool_calls
]
}
# 保留 DeepSeek thinking 模式的 reasoning_content
reasoning_content = getattr(message, 'reasoning_content', None)
if reasoning_content:
assistant_msg["reasoning_content"] = reasoning_content
messages_history.append(assistant_msg)
current_tool_results = []
@ -422,7 +426,7 @@ class BackendServer:
api_key = api_config.get("api_key", "")
base_url = api_config.get("base_url", "https://api.deepseek.com")
model = api_config.get("model", "deepseek-chat")
model = api_config.get("model", "deepseek-v4-flash")
self.update_config(api_key, base_url, model)
@ -509,7 +513,7 @@ class BackendServer:
response = self.send(packet)
return response.body
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-v4-flash") -> None:
with self._lock:
self._config["api_key"] = api_key
self._config["base_url"] = base_url

View File

@ -34,8 +34,8 @@ def load_secret_key():
WEB_CONFIG = load_secret_key()
app = Flask(__name__, static_folder='static', static_url_path='', template_folder='templates')
_ui_dir = os.path.join(os.path.dirname(__file__), '..', 'ui')
app = Flask(__name__, static_folder=os.path.join(_ui_dir, 'static'), static_url_path='', template_folder=os.path.join(_ui_dir, 'templates'))
app.secret_key = WEB_CONFIG["SECRET_KEY"]
app.permanent_session_lifetime = timedelta(days=7)
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
@ -731,6 +731,52 @@ def get_graph_highlight():
})
# ── 可被 TUI 作为线程启动 ──────────────────────────────────────────────────
_web_thread: threading.Thread | None = None
_http_server = None # werkzeug.serving.BaseWSGIServer 引用,用于优雅停止
def run_web_server(port: int = 4096, host: str = '0.0.0.0') -> None:
"""在后台线程启动 Flask供 TUI 或入口脚本在进程中直接调用"""
global backend_server, backend_client, _web_thread, _http_server
if _http_server is not None:
return # 已在运行
# 自动初始化后端(如果还没初始化的话)
if backend_server is None:
create_server()
def _start():
global _http_server
try:
from werkzeug.serving import make_server
_http_server = make_server(host, port, app, threaded=True)
print(f"Web API 服务启动在 http://{host}:{port}")
_http_server.serve_forever()
except Exception as e:
print(f"Web 服务启动失败: {e}")
_http_server = None
finally:
_http_server = None
_web_thread = threading.Thread(target=_start, daemon=True)
_web_thread.start()
def stop_web_server() -> None:
"""停止 Web 服务线程"""
global _http_server, _web_thread
if _http_server:
try:
_http_server.shutdown()
except Exception:
pass
_http_server = None
_web_thread = None
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='TrulyMEM Web API 服务')
parser.add_argument('--port', type=int, default=5000, help='服务端口 (默认: 5000)')

View File

@ -21,6 +21,7 @@ TrulyMEM-TrueHumanMEM/
│ ├── graph_client.py # OpenAI/DeepSeek API client
│ ├── tool_executor.py # Tool executor
│ ├── tool_limiter.py # Tool call limiter
│ ├── web_api.py # Web API service (login + RESTful API)
│ ├── tools/ # Tool definitions
│ │ └── memory_tools.py
│ └── prompts/ # Prompt management
@ -31,12 +32,16 @@ TrulyMEM-TrueHumanMEM/
│ ├── models/ # Data models
│ ├── services/ # Service layer (config only)
│ ├── handlers/ # Event handlers
── styles/ # Style files
├── web_api.py # Web API service (login + RESTful API)
├── templates/login.html # Login page template
├── static/ # Web frontend static files (star map visualization)
├── web_config.json # Web service config file (sensitive, not committed)
└── web_config.example.json # Web config template
── styles/ # Style files
│ ├── static/ # Web frontend static files (star map visualization)
│ │ ├── graph.html
└── index.html
│ └── templates/ # Page templates
│ ├── login.html
│ ├── setup.html
│ └── settings.html
│ ├── web_config.json # Web service config file (sensitive, not committed)
│ └── web_config.example.json # Web config template
```
## Architecture Diagram

View File

@ -1,123 +1,176 @@
# TrulyMEM Quick Start Guide
## Running Methods
> **Version**: Multi-user (v2) — TUI login, user isolation, embedded Web server
### Run from Source
---
## Running
### Quick Start
```bash
git clone <repo-url>
cd TrulyMEM-TrueHumanMEM
pip install -r requirements.txt
# From source
python trulymem_entry.py
# Packaged binary
./dist/TrulyMEM
```
### Run After Build
### First Run — Login Flow
After building, an executable will be generated:
On first launch, TrulyMEM checks for legacy data and presents a **login screen**:
1. **Clean install** → Enter username/password (first user becomes admin)
2. **Legacy upgrade** → Detects `~/.trulymem/config.json`, guides migration setup
3. **Returning user** → Login directly
> 💡 All user data is isolated: `~/.trulymem/{username}/`
### Chat Configuration
After login, press **F2** to open the right-side configuration panel:
1. **API Key** — Required (DeepSeek, OpenAI, etc.)
2. **Model** — Optional
3. **Base URL** — Optional
Config saves automatically.
---
## Web Visualization
The Web service now runs **embedded in the main process** (no separate subprocess needed).
### Start via TUI (Admin only)
Admin users: press F2 → check "Enable Web Service".
### Start Manually
```bash
# Linux/macOS
chmod +x TrulyMEM
./TrulyMEM
# Windows
TrulyMEM.exe
python -m core.web_api --port 4096
# Visit http://localhost:4096
```
## System Requirements
### First Visit Flow
- **Python 3.8+**
- **API Key** (DeepSeek, OpenAI, or other compatible APIs)
1. Open `http://localhost:4096` in browser
2. **No users** → Auto-redirect to setup page, create admin account
3. **Has users** → Login page
4. After login → Star map visualization
## First-Time Configuration
### Web Features
1. Run the application
2. Press **F2** to expand sidebar
3. Enter **API Key**, **Model**, **Base URL**
4. Press **Enter** to save
| Page | Access | Feature |
|------|--------|---------|
| 🌟 Star Map | All logged-in | Browse knowledge graph |
| ⚙ Settings | All logged-in | Change password |
| 🧑‍💼 User Management | **Admin only** | Add/delete users |
Config will be automatically saved to `~/.trulymem/config.json` and loaded on next startup.
---
### Web Visualization (Optional)
## Multi-User System
TrulyMEM provides a Web star-map visualization interface for browsing the knowledge graph in real-time:
### Directory Layout
```bash
# Start Web service
python web_api.py --port 4096
```
~/.trulymem/
├── trulymem.db # Global user database (web_users table)
├── .migrated # Migration flag
├── admin/
│ ├── config.json # Admin config
│ └── admin_graph.db # Admin knowledge graph
└── user2/
├── config.json # user2 config
└── user2_graph.db # user2 knowledge graph
```
Then open `http://localhost:4096` in your browser.
### Role Matrix
**Login Setup:**
1. Copy `web_config.example.json` to `web_config.json`
2. Set login password (using SHA256) and secret key
3. Web service will automatically read the config
| Feature | User | Admin |
|---------|------|-------|
| Change password | ✅ | ✅ |
| Configure API Key / Model | ✅ | ✅ |
| Web service toggle (TUI) | ❌ | ✅ |
| Web login credentials | ❌ | ✅ |
| View user list | ❌ | ✅ |
| Add/delete users | ❌ | ✅ |
Default port is 4096, change with `--port` flag.
> ⚠️ First registered user becomes admin automatically. Add users via Web settings page.
---
## Keyboard Shortcuts
| Key | Function |
|-----|-----------|
| Key | Action |
|-----|--------|
| F1 | Help |
| F2 | Toggle sidebar |
| F2 | Toggle sidebar (config panel) |
| F3 | Tool details |
| F5 | Clear screen |
| F6 | Exit |
| F6 | Quit |
## Data Storage
---
### Source Mode
## Building
| Data | Location |
|------|----------|
| Graph database | Project directory `graph_memory.db` |
| Config file | Project directory `config.json` (if exists) |
| Database format | SQLite |
```bash
# Linux
bash build/build_linux.sh
### Packaged Mode
# macOS
bash build/build_macos.sh
| Data | Location |
|------|----------|
| Graph database | `~/.trulymem/graph_memory.db` |
| Config file | `~/.trulymem/config.json` |
| Database format | SQLite |
# Windows
build\build_windows.bat
> **Note**: Backend manages config uniformly. Frontend only displays messages; config modifications are persisted to filesystem through the backend.
# AppImage
bash build/build_appimage.sh
```
## Architecture Explanation
Output: `dist/TrulyMEM` (single binary — TUI and Web server embedded)
### Communication Protocol
> 📦 Since v2, the Web server runs as a thread inside the main process. No need for a separate `trulymem-web` binary.
UI and backend communicate via **Packet Protocol**:
---
## Architecture
### Communication
```
UI (Textual TUI)
↓ BackendClient
Packet → queue.Queue → BackendServer (independent thread)
Process request → Return response
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (thread)
```
### Config Management
- **Storage location**: `~/.trulymem/config.json`
- **Auto-load**: Load config from file at startup
- **Dynamic update**: Config changes take effect immediately at runtime
- **Persistence**: Auto-save to file after modification
- **Per-user**: `~/.trulymem/{username}/config.json`
- **Web config**: `~/.trulymem/trulymem.db` (web_users table)
- **Auto-load**: reads config for logged-in user on startup
- **Persistent**: saves automatically on change
## Common Issues
### Web Service Architecture
### Python Not Found
```
┌──────────────────────┐
│ TrulyMEM Process │
│ ┌──────┐ ┌────────┐ │
│ │ TUI │ │ Flask │ │ ← Same process, different threads
│ │ │ │ Thread │ │
│ └──────┘ └────────┘ │
└──────────────────────┘
```
---
## FAQ
### Python not found
Install Python 3.8+: https://www.python.org/downloads/
### Dependency Installation Failed
### Dependency installation fails
```bash
python -m venv venv
@ -128,18 +181,22 @@ pip install -r requirements.txt
### Invalid API Key
Check API Key format, ensure no extra spaces.
Check format and whitespace. Reconfigure in TUI sidebar.
## Development Commands
### Lost admin account
The first registered user is always admin. If all users lost admin, delete `trulymem.db` from the user directory and re-register.
### Legacy data migration
When old `~/.trulymem/config.json` and `graph_memory.db` are detected, TUI auto-enters migration flow. Legacy files are preserved.
---
## Dev Commands
```bash
# Install dependencies
pip install -r requirements.txt
# Run tests
pytest tests/
# Build
bash build/build_windows.bat # Windows
bash build/build_linux.sh # Linux
```
bash build/build_linux.sh
```

View File

@ -21,6 +21,7 @@ TrulyMEM-TrueHumanMEM/
│ ├── graph_client.py # OpenAI/DeepSeek API 客户端
│ ├── tool_executor.py # 工具执行器
│ ├── tool_limiter.py # 工具调用限制器
│ ├── web_api.py # Web API 服务(登录 + RESTful API
│ ├── tools/ # 工具定义
│ │ └── memory_tools.py
│ └── prompts/ # 提示词管理
@ -31,12 +32,16 @@ TrulyMEM-TrueHumanMEM/
│ ├── models/ # 数据模型
│ ├── services/ # 服务层(仅配置管理)
│ ├── handlers/ # 事件处理
── styles/ # 样式文件
├── web_api.py # Web API 服务(登录 + RESTful API
├── templates/login.html # 登录页面模板
├── static/ # Web 前端静态文件(星图可视化)
├── web_config.json # Web 服务配置文件(敏感信息,不提交)
└── web_config.example.json # Web 配置模板
── styles/ # 样式文件
│ ├── static/ # Web 前端静态文件(星图可视化
│ │ ├── graph.html
└── index.html
│ └── templates/ # 页面模板
│ ├── login.html
│ ├── setup.html
│ └── settings.html
│ ├── web_config.json # Web 服务配置文件(敏感信息,不提交)
│ └── web_config.example.json # Web 配置模板
```
## 架构图

View File

@ -1,62 +1,103 @@
# TrulyMEM 启动指南
> **版本**: 多用户版 (v2) — 支持 TUI 登录、多用户隔离、Web 服务内嵌
---
## 运行方式
### 从源码运行
### 快速启动(推荐)
```bash
git clone <repo-url>
cd TrulyMEM-TrueHumanMEM
pip install -r requirements.txt
# 从源码
python trulymem_entry.py
# 或打包后
./dist/TrulyMEM
```
### 打包后运行
### 首次使用 —— 登录流程
打包后会生成可执行文件
首次启动会自动检查是否需要迁移旧数据,然后进入**登录页面**
1. **新部署** → 直接输入用户名和密码创建账户(首个用户自动成为管理员)
2. **旧版升级** → 自动检测 `~/.trulymem/config.json`,引导设置用户名密码,迁移数据
3. **已有账户** → 直接登录进入聊天界面
> 💡 所有用户数据隔离存储:`~/.trulymem/{用户名}/`
### 聊天配置
登录后按 **F2** 展开右侧配置面板:
1. **API Key** — 必须(支持 DeepSeek、OpenAI 等)
2. **模型** — 可选,默认已配置
3. **Base URL** — 可选
配置自动保存,下次启动自动加载。
---
## Web 可视化界面
TrulyMEM 的 Web 服务现在**内嵌在主进程中**(无需独立启动子进程)。
### TUI 内启动(管理员专有)
管理员按 F2 打开右侧面板,勾选「启用 Web 服务」即可。
### 手动启动
```bash
# Linux/macOS
chmod +x TrulyMEM
./TrulyMEM
# Windows
TrulyMEM.exe
python -m core.web_api --port 4096
# 访问 http://localhost:4096
```
## 系统要求
### 首次访问流程
- **Python 3.8+**
- **API Key**DeepSeek、OpenAI 或其他兼容 API
1. 浏览器打开 `http://localhost:4096`
2. **无用户** → 自动跳转至设置页,创建管理员账号
3. **有用户** → 跳转至登录页
4. 登录后进入星图可视化页面
## 首次配置
### Web 功能
1. 运行应用
2.**F2** 展开侧边栏
3. 输入 **API Key**、**模型**、**Base URL**
4.**Enter** 保存
| 页面 | 访问权限 | 功能 |
|------|----------|------|
| 🌟 星图 | 所有已登录用户 | 浏览知识图谱三元组 |
| ⚙ 设置 | 所有已登录用户 | 修改密码 |
| 🧑‍💼 用户管理 | **仅管理员** | 添加/删除用户 |
配置会自动保存到 `~/.trulymem/config.json`,下次启动自动加载。
---
### Web 可视化界面(可选)
## 多用户系统
TrulyMEM 提供 Web 星图可视化界面,支持实时浏览知识图谱:
### 目录结构
```bash
# 启动 Web 服务
python web_api.py --port 4096
```
~/.trulymem/
├── trulymem.db # 全局用户数据库web_users 表)
├── .migrated # 旧版迁移标记
├── admin/
│ ├── config.json # 管理员配置
│ └── admin_graph.db # 管理员知识图谱
└── user2/
├── config.json # user2 配置
└── user2_graph.db # user2 知识图谱
```
然后打开浏览器访问 `http://localhost:4096`
### 角色体系
**登录配置:**
1. 复制 `web_config.example.json``web_config.json`
2. 设置登录密码(使用 SHA256和 secret key
3. Web 服务会自动读取该配置
| 功能 | 普通用户 | 管理员 |
|------|---------|--------|
| 修改自己密码 | ✅ | ✅ |
| 配置 API Key / 模型 | ✅ | ✅ |
| Web 服务开关TUI | ❌ | ✅ |
| Web 登录凭据 | ❌ | ✅ |
| 查看用户列表 | ❌ | ✅ |
| 添加/删除用户 | ❌ | ✅ |
默认端口 4096可通过 `--port` 参数修改
> ⚠️ 首个注册用户自动成为管理员。Web 设置页可添加新用户
---
@ -65,30 +106,34 @@ python web_api.py --port 4096
| 按键 | 功能 |
|------|------|
| F1 | 帮助 |
| F2 | 切换侧边栏 |
| F2 | 切换侧边栏(配置面板) |
| F3 | 工具详情 |
| F5 | 清屏 |
| F6 | 退出 |
## 数据存储
---
### 源码运行模式
## 打包构建
| 数据 | 位置 |
|------|------|
| 图数据库 | 项目目录 `graph_memory.db` |
| 配置文件 | 项目目录 `config.json`(如存在) |
| 数据库格式 | SQLite |
```bash
# Linux
bash build/build_linux.sh
### 打包运行模式
# macOS
bash build/build_macos.sh
| 数据 | 位置 |
|------|------|
| 图数据库 | `~/.trulymem/graph_memory.db` |
| 配置文件 | `~/.trulymem/config.json` |
| 数据库格式 | SQLite |
# Windows
build\build_windows.bat
> **说明**:后端统一管理配置。前端仅负责消息展示,配置修改通过后端持久化到文件系统。
# AppImage
bash build/build_appimage.sh
```
构建产出:`dist/TrulyMEM`单文件TUI + Web 服务均内嵌于同一二进制)
> 📦 从 v2 开始Web 服务作为线程嵌入主程序,不再需要独立打包 `trulymem-web`。
---
## 架构说明
@ -97,19 +142,29 @@ python web_api.py --port 4096
UI 与后端通过 **Packet 协议** 通信:
```
UI (Textual TUI)
↓ BackendClient
Packet → queue.Queue → BackendServer (独立线程)
处理请求 → 返回响应
TUI (Textual) ←→ BackendClient ←→ queue.Queue ←→ BackendServer (独立线程)
```
### 配置管理
- **存储位置**: `~/.trulymem/config.json`
- **自动加载**: 启动时从文件读取配置
- **动态更新**: 运行时修改配置立即生效
- **持久化**: 修改后自动保存到文件
- **用户级存储**: `~/.trulymem/{username}/config.json`
- **Web 配置**: `~/.trulymem/trulymem.db`web_users 表)
- **自动加载**: 启动时根据登录用户加载对应配置文件
- **动态更新**: 运行时修改配置立即生效,自动持久化
### Web 服务架构
```
┌──────────────────────┐
│ TrulyMEM 主进程 │
│ ┌──────┐ ┌────────┐ │
│ │ TUI │ │ Flask │ │ ← 同一进程,不同线程
│ │ │ │ Thread │ │
│ └──────┘ └────────┘ │
└──────────────────────┘
```
---
## 常见问题
@ -128,18 +183,22 @@ pip install -r requirements.txt
### API Key 无效
检查 API Key 格式,确保无多余空格。
检查 API Key 格式,确保无多余空格。可在 TUI 右侧面板重新配置。
### 管理员账号丢失
数据库中第一个注册账号总是 admin。如果所有用户都丢失了 admin 权限,删除用户目录下的 `trulymem.db` 后重新注册即可。
### 旧版数据迁移
检测到旧版 `~/.trulymem/config.json``graph_memory.db`TUI 启动时自动进入迁移引导。迁移后旧文件保留,不会删除。
---
## 开发命令
```bash
# 安装依赖
pip install -r requirements.txt
# 运行测试
pytest tests/
# 打包
bash build/build_windows.bat # Windows
bash build/build_linux.sh # Linux
```
bash build/build_linux.sh
```

View File

@ -0,0 +1,43 @@
export interface ActivityRecord {
id: number;
timestamp: number;
action: string;
toolName: string;
entity: string;
detail: string;
}
export class ActivityRecorder {
private records: ActivityRecord[] = [];
private nextId: number = 1;
record(action: string, toolName: string, entity: string, detail?: string): void {
const record: ActivityRecord = {
id: this.nextId++,
timestamp: Date.now() / 1000,
action: action,
toolName: toolName,
entity: entity,
detail: detail || ""
};
this.records.push(record);
}
getAll(): ActivityRecord[] {
return this.records.slice();
}
clear(): void {
this.records = [];
this.nextId = 1;
}
getSummary(): Map<string, number> {
const summary: Map<string, number> = new Map<string, number>();
for (const record of this.records) {
const currentCount: number = summary.get(record.action) ?? 0;
summary.set(record.action, currentCount + 1);
}
return summary;
}
}

View File

@ -0,0 +1,77 @@
export class ToolLimiter {
private personaUpdateMax: number = 1;
private taskUpdateMax: number = 5;
private memoryQueryMax: number = 20;
private memoryUpdateMax: number = 10;
private personaUpdate: number = 0;
private taskUpdate: number = 0;
private memoryQuery: number = 0;
private memoryUpdate: number = 0;
canCall(toolName: string): [boolean, string] {
const [cat, op]: [string, string] = this.classifyTool(toolName);
if (cat === 'persona' && this.personaUpdate >= this.personaUpdateMax) {
return [false, `人设修改已达上限(${this.personaUpdateMax}次)`];
}
if (cat === 'task' && this.taskUpdate >= this.taskUpdateMax) {
return [false, `任务修改已达上限(${this.taskUpdateMax}次)`];
}
if (cat === 'memory' && op === 'query' && this.memoryQuery >= this.memoryQueryMax) {
return [false, `记忆查询已达上限(${this.memoryQueryMax}次)`];
}
if (cat === 'memory' && op === 'update' && this.memoryUpdate >= this.memoryUpdateMax) {
return [false, `记忆修改已达上限(${this.memoryUpdateMax}次)`];
}
return [true, "允许调用"];
}
recordCall(toolName: string): void {
const [cat, op]: [string, string] = this.classifyTool(toolName);
if (cat === 'persona') {
this.personaUpdate++;
} else if (cat === 'task') {
this.taskUpdate++;
} else if (cat === 'memory') {
if (op === 'query') {
this.memoryQuery++;
} else {
this.memoryUpdate++;
}
}
}
private classifyTool(toolName: string): [string, string] {
if (toolName === 'persona_update' || toolName === 'persona_clear') {
return ['persona', 'update'];
}
if (toolName === 'task_create' || toolName === 'task_set_state' || toolName === 'task_delete' || toolName === 'task_link_info') {
return ['task', 'update'];
}
if (toolName === 'memory_recall') {
return ['memory', 'query'];
}
if (toolName === 'memory_commit' || toolName === 'memory_purge' || toolName === 'memory_archive' || toolName === 'memory_cleanup') {
return ['memory', 'update'];
}
if (toolName === 'memory_introspect' || toolName === 'context_rewrite') {
return ['memory', 'query'];
}
return ['memory', 'update'];
}
getSummary(): string {
const lines: string[] = [
`人设图: 修改${this.personaUpdate}/${this.personaUpdateMax}次`,
`工作记忆链: 修改${this.taskUpdate}/${this.taskUpdateMax}次`,
`一般记忆: 查询${this.memoryQuery}/${this.memoryQueryMax}次, 修改${this.memoryUpdate}/${this.memoryUpdateMax}次`
];
return lines.join('\n');
}
reset(): void {
this.personaUpdate = 0;
this.taskUpdate = 0;
this.memoryQuery = 0;
this.memoryUpdate = 0;
}
}

View File

@ -157,7 +157,7 @@ class TestBackendClientAPI:
client = BackendClient(server)
result = client.update_settings(
api_config={"api_key": "new-key", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
api_config={"api_key": "new-key", "base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash"},
tool_limits={"persona_update_max": 2}
)
assert result.get("success") is True

View File

@ -40,7 +40,7 @@ class TestAppConfig:
from ui.models.config import AppConfig
config = AppConfig()
assert config.api_key == ""
assert config.model == "deepseek-chat"
assert config.model == "deepseek-v4-flash"
assert config.base_url == "https://api.deepseek.com"
def test_config_from_env(self):
@ -185,7 +185,7 @@ class TestUIWithBackendClient:
assert status.get("success") is True
result = client.update_settings(
api_config={"api_key": "sk-test", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"},
api_config={"api_key": "sk-test", "base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash"},
tool_limits={"persona_update_max": 1}
)
assert result.get("success") is True

View File

@ -1,6 +1,9 @@
#!/usr/bin/env python3
import argparse
import sys
import os
import signal
import threading
from pathlib import Path
# 用户配置文件始终放在用户目录
@ -30,21 +33,46 @@ from ui import GraphMemoryApp
def main():
backend_server = BackendServer(
db_path=str(DB_PATH),
use_embedded_db=True,
config_file=str(CONFIG_PATH)
)
backend_server.start()
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
try:
app.run()
except KeyboardInterrupt:
print("\n退出")
finally:
backend_server.shutdown()
parser = argparse.ArgumentParser(description='TrulyMEM - True Human Memory')
parser.add_argument('--web', action='store_true', help='以 Web 服务模式启动headless不启动 TUI')
parser.add_argument('--port', type=int, default=4096, help='Web 服务端口(默认 4096')
args = parser.parse_args()
if args.web:
# Web headless 模式 — web_api.run_web_server 会自动初始化 Backend
from core.web_api import run_web_server, stop_web_server
print(f"TrulyMEM Web 服务启动在 http://0.0.0.0:{args.port}")
run_web_server(port=args.port, host='0.0.0.0')
# 阻塞主线程直到收到信号
shutdown_event = threading.Event()
def _handle_signal(signum, frame):
print("\n收到停止信号,正在关闭...")
shutdown_event.set()
signal.signal(signal.SIGINT, _handle_signal)
signal.signal(signal.SIGTERM, _handle_signal)
shutdown_event.wait()
stop_web_server()
print("TrulyMEM Web 服务已停止")
else:
# TUI 模式(默认)
backend_server = BackendServer(
db_path=str(DB_PATH),
use_embedded_db=True,
config_file=str(CONFIG_PATH)
)
backend_server.start()
app = GraphMemoryApp(backend_server=backend_server, config_file=str(CONFIG_PATH))
try:
app.run()
except KeyboardInterrupt:
print("\n退出")
finally:
backend_server.shutdown()
if __name__ == "__main__":

View File

@ -1,6 +1,6 @@
import asyncio
import sys
import subprocess
import threading
import signal
from pathlib import Path
from textual.app import App, ComposeResult
@ -31,7 +31,6 @@ 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 # 当前登录用户信息
@ -51,7 +50,7 @@ class GraphMemoryApp(App):
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-chat")
initial_config.model = api_config.get("model", "deepseek-v4-flash")
tool_limits = settings_data.get("tool_limits", {})
initial_config.persona_update_max = tool_limits.get("persona_update_max", 1)
@ -138,46 +137,14 @@ class GraphMemoryApp(App):
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:
"""在当前进程通过线程启动 Web 服务(无需子进程"""
if self._web_running:
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
)
from web_api import run_web_server
run_web_server(port=port)
self._web_running = True
if self._backend_client:
self._backend_client.report_web_status(True, port)
@ -186,17 +153,13 @@ class GraphMemoryApp(App):
self.notify(f"启动 Web 服务失败: {e}", severity="error")
def _stop_web_server(self) -> None:
"""停止 Web 服务器子进"""
if self._web_process:
"""停止 Web 服务线"""
if self._web_running:
try:
self._web_process.terminate()
self._web_process.wait(timeout=5)
except:
try:
self._web_process.kill()
except:
pass
self._web_process = None
from web_api import stop_web_server
stop_web_server()
except Exception:
pass
self._web_running = False
if self._backend_client:
self._backend_client.report_web_status(False, 0)
@ -332,7 +295,7 @@ class GraphMemoryApp(App):
api_config = {
"api_key": config.api_key,
"base_url": config.base_url,
"model": getattr(config, 'model', 'deepseek-chat')
"model": getattr(config, 'model', 'deepseek-v4-flash')
}
tool_limits = {
@ -378,7 +341,7 @@ class GraphMemoryApp(App):
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-chat"),
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),

View File

@ -11,7 +11,7 @@ from typing import Optional
class AppConfig:
"""应用配置"""
api_key: str = ""
model: str = "deepseek-chat"
model: str = "deepseek-v4-flash"
base_url: str = "https://api.deepseek.com"
persona_update_max: int = 1
task_update_max: int = 5
@ -38,7 +38,7 @@ class AppConfig:
return cls(
api_key=os.getenv("DEEPSEEK_API_KEY", ""),
model=os.getenv("MODEL_NAME", "deepseek-chat"),
model=os.getenv("MODEL_NAME", "deepseek-v4-flash"),
base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com"),
persona_update_max=int(os.getenv("PERSONA_UPDATE_MAX", 1)),
task_update_max=int(os.getenv("TASK_UPDATE_MAX", 5)),
@ -61,7 +61,7 @@ class AppConfig:
return cls(
api_key=data.get("api_key", ""),
model=data.get("model", "deepseek-chat"),
model=data.get("model", "deepseek-v4-flash"),
base_url=data.get("base_url", "https://api.deepseek.com"),
persona_update_max=data.get("persona_update_max", 1),
task_update_max=data.get("task_update_max", 5),

View File

@ -35,7 +35,7 @@ class ConfigManager:
return AppConfig(
api_key=data.get("api_key", ""),
model=data.get("model", "deepseek-chat"),
model=data.get("model", "deepseek-v4-flash"),
base_url=data.get("base_url", "https://api.deepseek.com")
)
except Exception:

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆星图 - TrulyMEM</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
* {
margin: 0;
@ -307,7 +308,7 @@
<div id="loading">正在加载星图数据...</div>
<div id="debug-info" style="position:absolute;bottom:10px;left:10px;background:rgba(0,0,0,0.7);color:#0f0;padding:4px 8px;font-size:11px;z-index:300;border-radius:4px;"></div>
<div id="stats">
<h3>🌌 记忆星图</h3>
<h3><i class="fas fa-star"></i> 记忆星图</h3>
<p>节点: <span id="node-count">0</span></p>
<p>边: <span id="edge-count">0</span></p>
<p>状态: <span id="status">初始化中...</span></p>
@ -319,15 +320,15 @@
<p><span class="label">连接数:</span> <span id="info-links"></span></p>
</div>
<div id="nav">
<a href="/graph.html" class="nav-btn active">🌌 星图</a>
<a href="/#activity" class="nav-btn">📡 活动</a>
<a href="/settings" class="nav-btn"> 设置</a>
<a href="/graph.html" class="nav-btn active"><i class="fas fa-star"></i> 星图</a>
<a href="/#activity" class="nav-btn"><i class="fas fa-chart-line"></i> 活动</a>
<a href="/settings" class="nav-btn"><i class="fas fa-cog"></i> 设置</a>
</div>
</div>
<!-- 聊天面板 -->
<div id="chat-panel">
<div class="chat-header">
<span>💬 与 TrulyMEM 对话</span>
<span><i class="fas fa-comment-dots"></i> 与 TrulyMEM 对话</span>
<button class="chat-close" id="chatCloseBtn" title="关闭聊天"></button>
</div>
<div class="chat-messages" id="chatMessages">
@ -677,7 +678,7 @@ function init() {
});
// 简单力导向模拟(迭代几次)
for (let iter = 0; iter < 50; iter++) {
for (let iter = 0; iter < 30; iter++) {
// 斥力:节点之间互相排斥
Object.keys(positions).forEach(id1 => {
Object.keys(positions).forEach(id2 => {
@ -690,7 +691,7 @@ function init() {
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 10) {
const force = 0.1 / dist;
const force = 0.04 / Math.max(dist, 0.5);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
const fz = (dz / dist) * force;
@ -840,8 +841,11 @@ function init() {
const dist = Math.sqrt(pos.x*pos.x + pos.y*pos.y + pos.z*pos.z);
maxDist = Math.max(maxDist, dist);
});
camera.position.set(maxDist * 2.2, maxDist * 1.5, maxDist * 2.2);
// 相机距离:节点范围加 20u 余量,但不超过 120u不低于 25u
const targetDist = Math.min(Math.max(maxDist + 15, 25), 120);
camera.position.set(targetDist * 0.9, targetDist * 0.6, targetDist * 0.9);
controls.target.set(0, 0, 0);
controls.update();
}
}
@ -951,8 +955,8 @@ function init() {
hoveredNode.scale.set(pulse * 1.2, pulse * 1.2, pulse * 1.2);
}
// 检查高亮(每2秒)
if (time - lastHighlightCheck > 2) {
// 检查高亮(每3秒)
if (time - lastHighlightCheck > 3) {
checkHighlight();
lastHighlightCheck = time;
}
@ -963,7 +967,134 @@ function init() {
renderer.render(scene, camera);
}
// 检查高亮
// 已消费的高亮变更ID防止重复触发
let _consumedHighlightIds = new Set();
// 平滑重新定位剩余的节点
function smoothReposition() {
if (nodeMeshes.length < 2) return;
// 从当前节点位置计算新布局
const currentPositions = {};
nodeMeshes.forEach(m => {
const nid = m.userData.nodeId;
currentPositions[nid] = {
x: m.position.x,
y: m.position.y,
z: m.position.z
};
});
// 用力导向模拟迭代 30 次平滑
for (let iter = 0; iter < 30; iter++) {
// 斥力
const ids = Object.keys(currentPositions);
for (let i = 0; i < ids.length; i++) {
for (let j = i + 1; j < ids.length; j++) {
const a = currentPositions[ids[i]];
const b = currentPositions[ids[j]];
const dx = a.x - b.x;
const dy = a.y - b.y;
const dz = a.z - b.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist < 12) {
const force = 0.08 / Math.max(dist, 0.5);
a.x += (dx/dist) * force;
a.y += (dy/dist) * force;
a.z += (dz/dist) * force;
b.x -= (dx/dist) * force;
b.y -= (dy/dist) * force;
b.z -= (dz/dist) * force;
}
}
}
// 引力
edges.forEach(e => {
const p1 = currentPositions[e.source];
const p2 = currentPositions[e.target];
if (!p1 || !p2) return;
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
const dz = p2.z - p1.z;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz) + 0.1;
if (dist > 8) {
const force = 0.03;
p1.x += (dx/dist) * force;
p1.y += (dy/dist) * force;
p1.z += (dz/dist) * force;
p2.x -= (dx/dist) * force;
p2.y -= (dy/dist) * force;
p2.z -= (dz/dist) * force;
}
});
}
// 平滑移动到新位置
nodeMeshes.forEach(m => {
const nid = m.userData.nodeId;
const target = currentPositions[nid];
if (!target) return;
// 使用简单插值动画从当前位置到目标
const startPos = m.position.clone();
const endPos = new THREE.Vector3(target.x, target.y, target.z);
const duration = 600;
const startTime = Date.now();
function lerpPosition() {
const elapsed = Date.now() - startTime;
const t = Math.min(elapsed / duration, 1);
// ease-out cubic
const ease = 1 - Math.pow(1 - t, 3);
m.position.lerpVectors(startPos, endPos, ease);
if (t < 1) requestAnimationFrame(lerpPosition);
}
lerpPosition();
});
// 更新边的几何
edgeLines.forEach(line => {
const e = line.userData.edgeData;
const p1 = currentPositions[e.source];
const p2 = currentPositions[e.target];
if (!p1 || !p2) return;
line.geometry.setFromPoints([
new THREE.Vector3(p1.x, p1.y, p1.z),
new THREE.Vector3(p2.x, p2.y, p2.z)
]);
});
// 更新相机
fitCameraToGraph();
}
// 自适应相机
function fitCameraToGraph() {
if (nodeMeshes.length === 0) return;
let maxDist = 0;
nodeMeshes.forEach(m => {
const d = m.position.length();
if (d > maxDist) maxDist = d;
});
if (maxDist < 1) maxDist = 30;
const targetDist = Math.min(Math.max(maxDist + 15, 25), 120);
// 平滑移动相机
const startPos = camera.position.clone();
const endPos = new THREE.Vector3(targetDist * 0.9, targetDist * 0.6, targetDist * 0.9);
const duration = 400;
const startTime = Date.now();
function lerpCamera() {
const elapsed = Date.now() - startTime;
const t = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - t, 3);
camera.position.lerpVectors(startPos, endPos, ease);
controls.target.set(0, 0, 0);
if (t < 1) requestAnimationFrame(lerpCamera);
}
lerpCamera();
}
// 检查高亮 — 增量更新,不再全量 reload
async function checkHighlight() {
try {
const response = await fetch('/api/graph/highlight');
@ -982,28 +1113,41 @@ function init() {
}
});
});
}
// 处理新节点
if (data.new_node_id) {
flashNewNode(data.new_node_id);
}
if (!data.success) return;
// 处理删除的节点
if (data.deleted_node_ids && data.deleted_node_ids.length > 0) {
data.deleted_node_ids.forEach(nodeId => {
deleteNodeAnimation(nodeId);
});
// 延迟刷新图数据,等待动画完成
setTimeout(() => {
// 处理节点
if (data.new_node_id && !_consumedHighlightIds.has('new:' + data.new_node_id)) {
_consumedHighlightIds.add('new:' + data.new_node_id);
// 新节点不在当前场景中,重新加载完整图
loadGraphData();
setTimeout(showDebugInfo, 500);
}, 1000);
setTimeout(showDebugInfo, 500);
return;
}
// 处理删除的节点 — 增量更新,不要全量 reload
if (data.deleted_node_ids && data.deleted_node_ids.length > 0) {
const pending = data.deleted_node_ids.filter(id => !_consumedHighlightIds.has('del:' + id));
if (pending.length > 0) {
pending.forEach(id => _consumedHighlightIds.add('del:' + id));
pending.forEach(nodeId => deleteNodeAnimation(nodeId));
// 动画完成后平滑重新布局
setTimeout(() => {
// 从 edges 和 nodes 中移除已删除的
nodes = nodes.filter(n => !pending.includes(n.id));
edges = edges.filter(e => !pending.includes(e.source) && !pending.includes(e.target));
smoothReposition();
// 更新统计
document.getElementById('node-count').textContent = nodes.length;
document.getElementById('edge-count').textContent = edges.length;
}, 1000);
}
}
} catch (error) {
// 忽略错误
}
}
} catch (error) {
// 忽略错误
}
}
// 更新高亮效果
function updateHighlight() {

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - TrulyMEM</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
* {
margin: 0;

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设置 - TrulyMEM</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
@ -186,10 +187,10 @@
</head>
<body>
<div class="settings-container">
<h1 class="settings-title"> 设置</h1>
<h1 class="settings-title"><i class="fas fa-cog"></i> 设置</h1>
<!-- 修改密码 -->
<div class="section-title">🔑 修改密码</div>
<div class="section-title"><i class="fas fa-key"></i> 修改密码</div>
<form id="passwordForm">
<div class="form-group">
<label for="current_password">当前密码</label>

View File

@ -4,6 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>首次设置 - TrulyMEM</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {

View File

@ -42,7 +42,7 @@ class ConfigSection(Vertical):
yield Input(
value=self._config.model,
placeholder="deepseek-chat",
placeholder="deepseek-v4-flash",
id="model-input"
)