fix: auto-clean orphan entities on purge; fix graph camera zoom; optimize system prompt
This commit is contained in:
92
.gitignore
vendored
92
.gitignore
vendored
@ -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
166
README.md
@ -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.*
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/downloads/)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 故事的开头
|
||||
## 一句话
|
||||
|
||||
行业普遍认为,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
|
||||
|
||||
# AppImage(Linux 通用打包)
|
||||
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)
|
||||
|
||||
176
README_EN.md
176
README_EN.md
@ -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.*
|
||||
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://www.python.org/downloads/)
|
||||
[]()
|
||||
[]()
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
@ -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!"
|
||||
|
||||
@ -22,25 +22,8 @@ pip install -r requirements.txt
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
||||
|
||||
# 共用 hidden imports(TUI + 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
记住:灵活应对,保持自然对话体验。
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)')
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -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 配置模板
|
||||
```
|
||||
|
||||
## 架构图
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
43
harmonyos/entry/src/main/ets/model/ActivityRecorder.ets
Normal file
43
harmonyos/entry/src/main/ets/model/ActivityRecorder.ets
Normal 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;
|
||||
}
|
||||
}
|
||||
77
harmonyos/entry/src/main/ets/model/ToolLimiter.ets
Normal file
77
harmonyos/entry/src/main/ets/model/ToolLimiter.ets
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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__":
|
||||
|
||||
65
ui/app.py
65
ui/app.py
@ -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),
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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() {
|
||||
@ -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;
|
||||
@ -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>
|
||||
@ -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 {
|
||||
@ -42,7 +42,7 @@ class ConfigSection(Vertical):
|
||||
|
||||
yield Input(
|
||||
value=self._config.model,
|
||||
placeholder="deepseek-chat",
|
||||
placeholder="deepseek-v4-flash",
|
||||
id="model-input"
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user