Compare commits
87 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7a3ecd443 | |||
| d42b250c17 | |||
| 5c3a148a03 | |||
| 3bedc6789a | |||
| 87b1742495 | |||
| 8214f31b5b | |||
| b374179094 | |||
| 30502cf2cf | |||
| c9be0d07bc | |||
| 6cdafddd8e | |||
| 5dd77abb0f | |||
| c554c07bfc | |||
| 8739e9ad26 | |||
| 3839a60459 | |||
| 807242dd78 | |||
| ea4e43bf97 | |||
| 90df082a92 | |||
| 2de699e2aa | |||
| 1f19bb0c57 | |||
| 69a79ac86d | |||
| 9f470cb97d | |||
| da02a729a3 | |||
| aa9b25c829 | |||
| 17978cc71f | |||
| 8802604568 | |||
| f585164cc1 | |||
| c046e6ff6a | |||
| 6749811e49 | |||
| 0f9001beef | |||
| 3a13753d36 | |||
| fa458dcb0d | |||
| 9bc7ac7560 | |||
| 20319f48d5 | |||
| a930648c95 | |||
| 2d2c87d505 | |||
| 159d22510b | |||
| cbc7b13dbc | |||
| cc5ea69eda | |||
| ecb74cebff | |||
| dd00ddaa8b | |||
| 5e1aba3c60 | |||
| 1b5a3adf3e | |||
| 64b31bbf4f | |||
| df156ba840 | |||
| 4fc8ca944b | |||
| 006f3c413f | |||
| a1654db365 | |||
| 2cc4d8b907 | |||
| 5f7c95e861 | |||
| a794c33545 | |||
| 6d49061539 | |||
| a5e71da88c | |||
| d4267e1a8d | |||
| bb426cdb45 | |||
| bc999b18f1 | |||
| 529672a93a | |||
| 00fd75a0a6 | |||
| eab18889ff | |||
| d971afb934 | |||
| 1a394ddd2b | |||
| 12ab1df7f5 | |||
| c1dc3f647b | |||
| 4901d3882f | |||
| 3de418b7e8 | |||
| 039dc41c0b | |||
| 39eee19a3e | |||
| 1fa548da3c | |||
| 7543fcae72 | |||
| b9e4bb164d | |||
| 49d6906f15 | |||
| d228d119f5 | |||
| c72fc5f38f | |||
| 0ccd02b0af | |||
| bddb5498e9 | |||
| 598e03e2eb | |||
| 8b34725872 | |||
| 4d64da743a | |||
| cc2241cac8 | |||
| e7ddaee872 | |||
| 84f217ef55 | |||
| 58dc93f061 | |||
| f0e4c89ac0 | |||
| d676d34cd8 | |||
| ac113d6fb0 | |||
| fd2c688321 | |||
| ae04e4ddbe | |||
| ad382602f2 |
127
.gitea/workflows/build-binaries.yml
Normal file
127
.gitea/workflows/build-binaries.yml
Normal file
@ -0,0 +1,127 @@
|
||||
name: test-build-release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=no
|
||||
run: |
|
||||
git clone --depth 1 ssh://git@jianfgit.xyz:220/jianf/TrulyMEM-TrueHumanMEM.git ./
|
||||
|
||||
- name: Verify workspace and Python
|
||||
run: |
|
||||
pwd
|
||||
ls -la
|
||||
python3 --version
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
python3 -m venv .venv-ci
|
||||
. .venv-ci/bin/activate
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt pytest
|
||||
pytest -q
|
||||
|
||||
build-linux:
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
env:
|
||||
GIT_SSH_COMMAND: ssh -o StrictHostKeyChecking=no
|
||||
run: |
|
||||
git clone --depth 1 ssh://git@jianfgit.xyz:220/jianf/TrulyMEM-TrueHumanMEM.git ./
|
||||
|
||||
- name: Verify build environment
|
||||
run: |
|
||||
pwd
|
||||
python3 --version
|
||||
tar --version | head -1
|
||||
|
||||
- name: Build Linux binary
|
||||
run: |
|
||||
bash build/build_linux.sh
|
||||
|
||||
- name: Pack release bundle
|
||||
run: |
|
||||
mkdir -p dist/release
|
||||
cp dist/TrulyMEM dist/release/
|
||||
cp dist/TrulyMEM.desktop dist/release/
|
||||
[ -f pic/image.png ] && cp pic/image.png dist/release/ || true
|
||||
tar -C dist/release -czf dist/TrulyMEM-linux-amd64.tar.gz .
|
||||
ls -lh dist/TrulyMEM-linux-amd64.tar.gz
|
||||
|
||||
- name: Build AppImage if preinstalled
|
||||
run: |
|
||||
if command -v appimagetool >/dev/null 2>&1; then
|
||||
bash build/build_appimage.sh
|
||||
else
|
||||
echo "appimagetool not found on runner, skip AppImage build"
|
||||
fi
|
||||
|
||||
- name: Show build outputs
|
||||
run: |
|
||||
ls -lah dist/
|
||||
|
||||
- name: Create Gitea release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
env:
|
||||
GITEA_TOKEN: ${{ vars.GITEA_RELEASE_TOKEN }}
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
REPO: ${{ github.repository }}
|
||||
SERVER_URL: ${{ github.server_url }}
|
||||
run: |
|
||||
test -n "$GITEA_TOKEN"
|
||||
|
||||
# Build release payload via env var — avoids heredoc in YAML
|
||||
export TAG_NAME
|
||||
release_payload=$(python3 -c 'import os,json; t=os.environ["TAG_NAME"]; print(json.dumps({"tag_name":t,"name":t,"draft":false,"prerelease":false}))')
|
||||
|
||||
release_response=$(curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-X POST \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases" \
|
||||
-d "$release_payload" \
|
||||
|| true)
|
||||
|
||||
# Extract release id or fallback to existing
|
||||
export RELEASE_RESPONSE
|
||||
release_id=$(python3 -c '
|
||||
import os, json
|
||||
raw = os.environ.get("RELEASE_RESPONSE", "").strip()
|
||||
if raw:
|
||||
try:
|
||||
print(json.loads(raw)["id"])
|
||||
except Exception:
|
||||
pass
|
||||
')
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
release_id=$(curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases/tags/$TAG_NAME" \
|
||||
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])' \
|
||||
2>/dev/null || true)
|
||||
fi
|
||||
|
||||
for file in dist/TrulyMEM dist/TrulyMEM.desktop dist/TrulyMEM-linux-amd64.tar.gz dist/TrulyMEM.AppImage; do
|
||||
if [ -f "$file" ]; then
|
||||
name=$(basename "$file")
|
||||
curl -fsSL \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/octet-stream" \
|
||||
--data-binary @"$file" \
|
||||
"$SERVER_URL/api/v1/repos/$REPO/releases/$release_id/assets?name=$name"
|
||||
fi
|
||||
done
|
||||
104
.gitignore
vendored
104
.gitignore
vendored
@ -1,77 +1,41 @@
|
||||
# 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
|
||||
*.pyc
|
||||
*.pyo
|
||||
.DS_Store
|
||||
harmonyos/.hvigor/
|
||||
harmonyos/build/
|
||||
harmonyos/entry/build/
|
||||
harmonyos.bak/
|
||||
node_modules/
|
||||
ts/node_modules/
|
||||
ts/dist/
|
||||
ts/build/
|
||||
*.hap
|
||||
*.hsp
|
||||
.env
|
||||
venv/
|
||||
test_venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.arts/
|
||||
.codeartsdoer/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Database
|
||||
.venv/
|
||||
dist/
|
||||
build/trulymem/
|
||||
build/trulymem/*
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
task_archive/
|
||||
ts/task_archive/
|
||||
core/web_config.json
|
||||
|
||||
# Logs
|
||||
# Build artifacts
|
||||
build/trulymem/
|
||||
dist/
|
||||
*.spec
|
||||
|
||||
# HarmonyOS / Hvigor build cache
|
||||
.hvigor/
|
||||
entry/build/
|
||||
|
||||
# Log files
|
||||
*.log
|
||||
logs/
|
||||
full_output.log
|
||||
|
||||
# OS
|
||||
.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
|
||||
node_modules/
|
||||
|
||||
# Test artifacts
|
||||
/session_*/
|
||||
task_archive/
|
||||
ts/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
172
README.md
172
README.md
@ -4,185 +4,63 @@
|
||||
<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 服务(线程模式),实时浏览知识图谱 + 聊天上传文件(支持 PDF/Word/文本) |
|
||||
| 🎮 **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 +69,11 @@ bash build/build_appimage.sh
|
||||
- 崔莉萍老师 — 理论指导
|
||||
- Annie — 专业指导
|
||||
- 王梓沣、马悦华、隆梦婷 — 神经科学理论支持
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
GNU General Public License v3.0 (GPLv3)
|
||||
# CI trigger Wednesday, May 13, 2026 AM06:17:55 UTC
|
||||
# runner cache cleared Wednesday, May 13, 2026 AM06:23:27 UTC
|
||||
|
||||
180
README_EN.md
180
README_EN.md
@ -4,182 +4,62 @@
|
||||
<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.
|
||||
|
||||
---
|
||||
|
||||
## Screen Shot
|
||||

|
||||

|
||||
|
||||
## 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), graph browsing + file upload in chat (PDF/Word/text) |
|
||||
| 🎮 **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 +71,9 @@ See [LICENSE](LICENSE) file for details.
|
||||
- 崔莉萍老师 — Theoretical guidance
|
||||
- Annie — Professional guidance
|
||||
- 王梓沣、马悦华、隆梦婷 — Neuroscience theory support
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
GNU General Public License v3.0 (GPLv3)
|
||||
|
||||
106
TrulyMEM.spec
Normal file
106
TrulyMEM.spec
Normal file
@ -0,0 +1,106 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
import os
|
||||
import sys
|
||||
|
||||
block_cipher = None
|
||||
|
||||
project_root = os.path.dirname(os.path.abspath(SPEC))
|
||||
sys.path.insert(0, project_root)
|
||||
|
||||
datas = []
|
||||
# UI 样式
|
||||
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:
|
||||
datas.append((os.path.join(root, f), 'ui/styles'))
|
||||
|
||||
# Prompt 模板
|
||||
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:
|
||||
datas.append((os.path.join(root, f), 'core/prompts/templates'))
|
||||
|
||||
# Web 静态文件
|
||||
static_dir = os.path.join(project_root, 'ui', 'static')
|
||||
if os.path.exists(static_dir):
|
||||
for root, dirs, files in os.walk(static_dir):
|
||||
for f in files:
|
||||
rel_dir = os.path.relpath(root, project_root)
|
||||
datas.append((os.path.join(root, f), rel_dir))
|
||||
|
||||
# 兼容旧的 static 目录(如果存在)
|
||||
static_dir_old = os.path.join(project_root, 'static')
|
||||
if os.path.exists(static_dir_old):
|
||||
for root, dirs, files in os.walk(static_dir_old):
|
||||
for f in files:
|
||||
datas.append((os.path.join(root, f), 'static'))
|
||||
|
||||
# Web 模板(Flask template_folder 指向 ui/templates/)
|
||||
templates_dir = os.path.join(project_root, 'ui', 'templates')
|
||||
if os.path.exists(templates_dir):
|
||||
for root, dirs, files in os.walk(templates_dir):
|
||||
for f in files:
|
||||
rel_dir = os.path.relpath(root, project_root)
|
||||
datas.append((os.path.join(root, f), rel_dir))
|
||||
|
||||
# Web API 脚本(以便子进程模式回退使用)
|
||||
web_api_src = os.path.join(project_root, 'core', 'web_api.py')
|
||||
if os.path.exists(web_api_src):
|
||||
datas.append((web_api_src, 'core'))
|
||||
|
||||
# ——— TUI 主二进制 ———
|
||||
a = Analysis(
|
||||
[os.path.join(project_root, 'trulymem_entry.py')],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=datas,
|
||||
hiddenimports=[
|
||||
'textual', 'textual.app', 'textual.widgets', 'textual.css',
|
||||
'openai', 'openai._client',
|
||||
'neo4j',
|
||||
'sqlite3',
|
||||
'core', 'core.embedded_db', 'core.graph_client',
|
||||
'core.tool_executor', 'core.tool_limiter',
|
||||
'core.tools', 'core.tools.memory_tools',
|
||||
'core.prompts', 'core.prompts.prompt_manager',
|
||||
'core.server', 'core.client', 'core.web_api',
|
||||
'core.migrate',
|
||||
'ui', 'ui.app', 'ui.login_screen',
|
||||
'ui.models', 'ui.models.message', 'ui.models.config', 'ui.models.log_entry',
|
||||
'ui.widgets', 'ui.widgets.left_panel', 'ui.widgets.right_panel',
|
||||
'ui.widgets.input_box', 'ui.widgets.message_history', 'ui.widgets.status_bar',
|
||||
'ui.handlers',
|
||||
'ui.services', 'ui.services.config_manager', 'ui.services.config_service',
|
||||
'flask', 'flask_cors', 'werkzeug',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
noarchive=False,
|
||||
optimize=0,
|
||||
)
|
||||
pyz = PYZ(a.pure)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
[],
|
||||
name='TrulyMEM',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
@ -1,161 +1,100 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
echo "===== Building TrulyMEM AppImage for Linux ====="
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
APP_NAME="TrulyMEM"
|
||||
APP_DIR="$PROJECT_ROOT/build/appimage-build"
|
||||
|
||||
# ── Step 1: 复用 build_linux.sh 完成 PyInstaller 构建 ──
|
||||
echo ""
|
||||
echo "Step 1: Running build_linux.sh (PyInstaller build)..."
|
||||
bash "$SCRIPT_DIR/build_linux.sh"
|
||||
|
||||
# ── Step 2: 检查 dist/ 中是否有二进制 ──
|
||||
echo ""
|
||||
echo "Step 2: Checking PyInstaller output..."
|
||||
if [ ! -f "dist/$APP_NAME" ]; then
|
||||
echo "Error: dist/$APP_NAME not found after build_linux.sh"; 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 "✅ Found dist/$APP_NAME ($(ls -lh "dist/$APP_NAME" | awk '{print $5}'))"
|
||||
|
||||
echo "===== Step 1: Build binaries with PyInstaller ====="
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_appimage"
|
||||
rm -rf "$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"
|
||||
# ── Step 3: 组织 AppDir 结构 ──
|
||||
echo ""
|
||||
echo "Step 3: Preparing AppDir structure..."
|
||||
rm -rf "$APP_DIR" 2>/dev/null || true
|
||||
mkdir -p "$APP_DIR/usr/bin"
|
||||
mkdir -p "$APP_DIR/usr/share/applications"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
|
||||
mkdir -p "$APP_DIR/usr/share/icons/hicolor/48x48/apps"
|
||||
|
||||
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
|
||||
)
|
||||
cp "dist/$APP_NAME" "$APP_DIR/usr/bin/"
|
||||
|
||||
echo "Running PyInstaller for TUI..."
|
||||
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:." \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
"${CORE_HIDDEN[@]}" \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.login_screen \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--hidden-import web_api \
|
||||
--hidden-import flask \
|
||||
--hidden-import flask_cors \
|
||||
--collect-all textual \
|
||||
--noconfirm
|
||||
# 图标处理
|
||||
ICON_SOURCE=""
|
||||
if [ -f "pic/image.png" ]; then
|
||||
ICON_SOURCE="pic/image.png"
|
||||
elif [ -f "pic/TrulyMEM.ico" ]; then
|
||||
echo "⚠️ No pic/image.png found; .ico will not display as AppImage icon"
|
||||
echo " To generate a PNG: convert pic/TrulyMEM.ico pic/image.png"
|
||||
fi
|
||||
|
||||
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
|
||||
if [ -n "$ICON_SOURCE" ]; then
|
||||
cp "$ICON_SOURCE" "$APP_DIR/usr/share/icons/hicolor/256x256/apps/${APP_NAME}.png"
|
||||
cp "$ICON_SOURCE" "$APP_DIR/usr/share/icons/hicolor/48x48/apps/${APP_NAME}.png"
|
||||
cp "$ICON_SOURCE" "$APP_DIR/${APP_NAME}.png"
|
||||
echo "✅ Icon: $ICON_SOURCE"
|
||||
else
|
||||
echo "⚠️ No icon found, creating placeholder"
|
||||
touch "$APP_DIR/${APP_NAME}.png"
|
||||
fi
|
||||
|
||||
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
|
||||
Terminal=true
|
||||
Name=${APP_NAME}
|
||||
Comment=True Human Memory - TUI & Web Mode
|
||||
Exec=${APP_NAME}
|
||||
Icon=${APP_NAME}
|
||||
Type=Application
|
||||
Categories=Utility;X-AI;
|
||||
Categories=Utility;Office;
|
||||
Terminal=true
|
||||
StartupNotify=true
|
||||
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"
|
||||
cp "$APP_DIR/${APP_NAME}.desktop" "$APP_DIR/usr/share/applications/"
|
||||
|
||||
# AppRun 入口
|
||||
cat > "$APP_DIR/AppRun" <<'APPRUN'
|
||||
#!/bin/bash
|
||||
HERE="$(dirname "$(readlink -f "$0")")"
|
||||
exec "$HERE/usr/bin/TrulyMEM" "$@"
|
||||
APPRUN
|
||||
chmod +x "$APP_DIR/AppRun"
|
||||
|
||||
# ── Step 4: 打包 AppImage ──
|
||||
echo ""
|
||||
echo "Step 4: Building AppImage..."
|
||||
if command -v appimagetool &> /dev/null; then
|
||||
ARCH="${ARCH:-$(uname -m)}" appimagetool "$APP_DIR" "dist/${APP_NAME}.AppImage"
|
||||
echo "✅ AppImage: dist/${APP_NAME}.AppImage"
|
||||
ls -lh "dist/${APP_NAME}.AppImage"
|
||||
else
|
||||
echo "Warning: No icon file found"
|
||||
echo "⚠️ appimagetool not found. AppDir ready at: $APP_DIR"
|
||||
echo ""
|
||||
echo "To complete manually, install appimagetool:"
|
||||
echo " wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$(uname -m).AppImage"
|
||||
echo " chmod +x appimagetool-*.AppImage"
|
||||
echo " ./appimagetool-*.AppImage '$APP_DIR' 'dist/${APP_NAME}.AppImage'"
|
||||
echo ""
|
||||
echo "AppDir contents:"
|
||||
find "$APP_DIR" -type f | head -20
|
||||
fi
|
||||
|
||||
APPIMAGE="$PROJECT_ROOT/TrulyMEM.AppImage"
|
||||
rm -f "$APPIMAGE"
|
||||
|
||||
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"
|
||||
else
|
||||
echo "Warning: appimagetool not available, AppDir at: $APPDIR"
|
||||
fi
|
||||
|
||||
echo "===== Build Complete ====="
|
||||
[ -f "$APPIMAGE" ] && echo "AppImage: $APPIMAGE" && ls -la "$APPIMAGE"
|
||||
[ -d "$APPDIR" ] && echo "AppDir: $APPDIR"
|
||||
|
||||
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 ""
|
||||
echo "===== AppImage Build Complete ====="
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
set -euo pipefail
|
||||
|
||||
echo "===== Building TrulyMEM for Linux ====="
|
||||
|
||||
@ -8,92 +8,102 @@ PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
cd "$PROJECT_ROOT"
|
||||
echo "Project root: $PROJECT_ROOT"
|
||||
|
||||
# ── 前置检查 ──
|
||||
if ! command -v python3 &> /dev/null; then
|
||||
echo "Error: python3 not found"; exit 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
|
||||
if [ ! -f requirements.txt ]; then
|
||||
echo "Error: requirements.txt not found in $PROJECT_ROOT"; exit 1
|
||||
fi
|
||||
|
||||
# ── 检测 python3-venv ──
|
||||
VENV_AVAILABLE=false
|
||||
if python3 -c "import ensurepip" 2>/dev/null && python3 -m venv --help &>/dev/null; then
|
||||
VENV_AVAILABLE=true
|
||||
else
|
||||
echo "⚠️ python3-venv 未安装(或缺少 ensurepip),建议安装以获得干净构建环境:"
|
||||
echo " sudo apt install python3-venv # Debian/Ubuntu"
|
||||
echo " sudo dnf install python3-virtualenv # Fedora"
|
||||
echo "将使用系统 Python 环境继续(依赖全局包)..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── 尝试 venv 隔离构建,失败则用系统环境 ──
|
||||
USE_VENV=false
|
||||
if [ "$VENV_AVAILABLE" = true ]; then
|
||||
VENV_DIR="$PROJECT_ROOT/.venv_build"
|
||||
echo "Creating virtual environment..."
|
||||
if python3 -m venv "$VENV_DIR" 2>/dev/null; then
|
||||
source "$VENV_DIR/bin/activate"
|
||||
USE_VENV=true
|
||||
echo "✅ Using virtual environment: $VENV_DIR"
|
||||
pip install --upgrade pip -q
|
||||
pip install -r requirements.txt -q
|
||||
else
|
||||
echo "⚠️ venv creation failed, falling back to system Python"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$USE_VENV" = false ]; then
|
||||
echo "Installing dependencies (system Python)..."
|
||||
pip install -r requirements.txt --break-system-packages -q 2>/dev/null || \
|
||||
pip install -r requirements.txt -q 2>/dev/null || {
|
||||
echo "⚠️ pip install failed, trying pip3..."
|
||||
pip3 install -r requirements.txt --break-system-packages -q 2>/dev/null || \
|
||||
pip3 install -r requirements.txt -q 2>/dev/null || \
|
||||
echo "⚠️ Some dependencies may be missing; build will proceed anyway"
|
||||
}
|
||||
fi
|
||||
|
||||
# ── 清理旧构建 ──
|
||||
echo ""
|
||||
echo "Cleaning previous builds..."
|
||||
rm -rf build/dist build/__pycache__ 2>/dev/null || true
|
||||
rm -rf dist/ build/trulymem/ 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
|
||||
)
|
||||
# ── PyInstaller 构建 ──
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "Building TrulyMEM (TUI + Web embedded)"
|
||||
echo "================================"
|
||||
python3 -m PyInstaller --clean build/trulymem.spec --noconfirm
|
||||
|
||||
echo "================================"
|
||||
echo "1️⃣ Build TUI: TrulyMEM"
|
||||
echo "================================"
|
||||
python -m PyInstaller trulymem_entry.py \
|
||||
--clean --onefile --console --name TrulyMEM \
|
||||
--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:." \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
"${CORE_HIDDEN[@]}" \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.login_screen \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--hidden-import web_api \
|
||||
--hidden-import flask \
|
||||
--hidden-import flask_cors \
|
||||
--collect-all textual \
|
||||
--noconfirm
|
||||
# ── 创建 Linux `.desktop` 文件(打包图标不能嵌入 ELF,通过 .desktop 引用)──
|
||||
echo ""
|
||||
echo "Generating .desktop file for Linux..."
|
||||
BINARY_PATH="$(cd dist && pwd)/TrulyMEM"
|
||||
ICON_PATH="$(cd pic && pwd)/image.png"
|
||||
|
||||
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
|
||||
cat > "dist/TrulyMEM.desktop" <<EOF
|
||||
[Desktop Entry]
|
||||
Name=TrulyMEM
|
||||
Comment=True Human Memory - TUI & Web Mode
|
||||
Exec=${BINARY_PATH}
|
||||
Icon=${ICON_PATH}
|
||||
Terminal=true
|
||||
Type=Application
|
||||
Categories=Utility;Office;
|
||||
StartupNotify=true
|
||||
EOF
|
||||
|
||||
chmod +x "dist/TrulyMEM.desktop"
|
||||
echo "✅ dist/TrulyMEM.desktop created (icon: pic/image.png)"
|
||||
|
||||
# ── 构建完成 ──
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
echo "Binary: dist/trulymem-web"
|
||||
ls -la dist/
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
echo "Desktop: dist/TrulyMEM.desktop"
|
||||
echo "------------------------------"
|
||||
ls -lh dist/ 2>/dev/null || ls -la dist/
|
||||
echo "================================"
|
||||
|
||||
# ── 清理 venv ──
|
||||
if [ "$USE_VENV" = true ]; then
|
||||
deactivate 2>/dev/null || true
|
||||
rm -rf "$VENV_DIR"
|
||||
echo "Virtual environment cleaned up."
|
||||
fi
|
||||
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
echo "Build finished successfully!"
|
||||
|
||||
77
build/build_macos.sh
Normal file → Executable file
77
build/build_macos.sh
Normal file → Executable file
@ -19,87 +19,20 @@ 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
|
||||
)
|
||||
rm -rf dist/ build/trulymem/ 2>/dev/null || true
|
||||
|
||||
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:." \
|
||||
--hidden-import textual \
|
||||
--hidden-import textual.app \
|
||||
--hidden-import textual.widgets \
|
||||
--hidden-import textual.css \
|
||||
--hidden-import openai \
|
||||
--hidden-import openai._client \
|
||||
--hidden-import neo4j \
|
||||
--hidden-import sqlite3 \
|
||||
"${CORE_HIDDEN[@]}" \
|
||||
--hidden-import ui \
|
||||
--hidden-import ui.app \
|
||||
--hidden-import ui.login_screen \
|
||||
--hidden-import ui.models \
|
||||
--hidden-import ui.models.message \
|
||||
--hidden-import ui.models.config \
|
||||
--hidden-import ui.models.log_entry \
|
||||
--hidden-import ui.widgets \
|
||||
--hidden-import ui.handlers \
|
||||
--hidden-import ui.services \
|
||||
--hidden-import ui.services.config_manager \
|
||||
--hidden-import ui.services.config_service \
|
||||
--hidden-import web_api \
|
||||
--hidden-import flask \
|
||||
--hidden-import flask_cors \
|
||||
--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
|
||||
python -m PyInstaller --clean build/trulymem.spec --noconfirm
|
||||
|
||||
echo "================================"
|
||||
echo "===== Build Complete ====="
|
||||
echo "Binary: dist/TrulyMEM"
|
||||
echo "Binary: dist/trulymem-web"
|
||||
echo " -> run: open dist/TrulyMEM"
|
||||
ls -la dist/
|
||||
|
||||
deactivate
|
||||
rm -rf "$VENV_DIR"
|
||||
echo "Build finished successfully!"
|
||||
echo "Build finished successfully!"
|
||||
@ -1,98 +1,35 @@
|
||||
#!/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: %PROJECT_ROOT%
|
||||
|
||||
if ! command -v python &> /dev/null; then
|
||||
echo "Error: python not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VENV_DIR="$PROJECT_ROOT\.venv_build"
|
||||
echo "Creating virtual environment: %VENV_DIR%"
|
||||
python -m venv "%VENV_DIR%"
|
||||
call "%VENV_DIR%\Scripts\activate.bat"
|
||||
echo Creating virtual environment: .venv_build
|
||||
python -m venv .venv_build
|
||||
call .venv_build\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 dist rmdir /s /q dist
|
||||
if exist build\trulymem rmdir /s /q build\trulymem
|
||||
|
||||
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 Building TrulyMEM (TUI + Web embedded)
|
||||
echo ================================
|
||||
python -m PyInstaller --clean build\trulymem.spec --noconfirm
|
||||
|
||||
echo "================================"
|
||||
echo "1. Build TUI: TrulyMEM.exe"
|
||||
echo "================================"
|
||||
python -m PyInstaller trulymem_entry.py ^
|
||||
--clean --onefile --console --name TrulyMEM ^
|
||||
--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;." ^
|
||||
--hidden-import textual ^
|
||||
--hidden-import textual.app ^
|
||||
--hidden-import textual.widgets ^
|
||||
--hidden-import textual.css ^
|
||||
--hidden-import openai ^
|
||||
--hidden-import openai._client ^
|
||||
--hidden-import neo4j ^
|
||||
--hidden-import sqlite3 ^
|
||||
%CORE_HIDDEN% ^
|
||||
--hidden-import ui ^
|
||||
--hidden-import ui.app ^
|
||||
--hidden-import ui.login_screen ^
|
||||
--hidden-import ui.models ^
|
||||
--hidden-import ui.models.message ^
|
||||
--hidden-import ui.models.config ^
|
||||
--hidden-import ui.models.log_entry ^
|
||||
--hidden-import ui.widgets ^
|
||||
--hidden-import ui.handlers ^
|
||||
--hidden-import ui.services ^
|
||||
--hidden-import ui.services.config_manager ^
|
||||
--hidden-import ui.services.config_service ^
|
||||
--hidden-import web_api ^
|
||||
--hidden-import flask ^
|
||||
--hidden-import flask_cors ^
|
||||
--collect-all textual ^
|
||||
--noconfirm
|
||||
echo ================================
|
||||
echo ===== Build Complete =====
|
||||
echo Binary: dist\TrulyMEM.exe
|
||||
dir dist
|
||||
|
||||
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
|
||||
call .venv_build\Scripts\deactivate.bat
|
||||
if exist .venv_build rmdir /s /q .venv_build
|
||||
|
||||
echo "===== Build Complete ====="
|
||||
echo "Output: dist/TrulyMEM.exe, dist/trulymem-web.exe"
|
||||
|
||||
deactivate
|
||||
rmdir /s /q "%VENV_DIR%"
|
||||
echo "Build finished successfully!"
|
||||
echo Build finished successfully!
|
||||
endlocal
|
||||
@ -4,51 +4,47 @@ 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, 'ui', '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), 'ui/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, 'ui', '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), 'ui/templates'))
|
||||
|
||||
# Web API 脚本(以便子进程模式回退使用)
|
||||
web_api_src = os.path.join(project_root, 'web_api.py')
|
||||
web_api_src = os.path.join(project_root, 'core', 'web_api.py')
|
||||
if os.path.exists(web_api_src):
|
||||
datas.append((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,
|
||||
@ -86,6 +83,7 @@ exe = EXE(
|
||||
a.datas,
|
||||
[],
|
||||
name='TrulyMEM',
|
||||
icon=os.path.join(project_root, 'pic', 'TrulyMEM.ico'),
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
@ -99,47 +97,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,
|
||||
)
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "common",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM common module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/common",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,26 @@
|
||||
"""
|
||||
活动记录器 - 记录 AI 对图数据库的操作
|
||||
使用 SQLite :memory: 供 WebUI 实时渲染,同时后台线程持久化到日志文件
|
||||
日志每 6 小时自动压缩归档
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import time
|
||||
import os
|
||||
import gzip
|
||||
import json
|
||||
import threading
|
||||
import shutil
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
||||
# 归档间隔(秒)
|
||||
ARCHIVE_INTERVAL = 6 * 3600 # 6 小时
|
||||
# 轮询间隔(秒)
|
||||
POLL_INTERVAL = 10
|
||||
|
||||
|
||||
class ActivityRecorder:
|
||||
@ -8,18 +28,46 @@ class ActivityRecorder:
|
||||
|
||||
def __init__(self):
|
||||
self.conn = sqlite3.connect(":memory:", check_same_thread=False)
|
||||
self.conn.execute("CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp REAL, action TEXT, tool_name TEXT, entity TEXT, detail TEXT)")
|
||||
self.conn.execute(
|
||||
"CREATE TABLE activities (id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
||||
"timestamp REAL, action TEXT, tool_name TEXT, entity TEXT, detail TEXT)"
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def record(self, action: str, tool_name: str, entity: str, detail: str = "") -> None:
|
||||
self.conn.execute("INSERT INTO activities (timestamp, action, tool_name, entity, detail) VALUES (?, ?, ?, ?, ?)",
|
||||
(time.time(), action, tool_name, entity, detail))
|
||||
self.conn.execute(
|
||||
"INSERT INTO activities (timestamp, action, tool_name, entity, detail) VALUES (?, ?, ?, ?, ?)",
|
||||
(time.time(), action, tool_name, entity, detail)
|
||||
)
|
||||
self.conn.commit()
|
||||
|
||||
def get_all(self) -> List[Dict]:
|
||||
cursor = self.conn.execute("SELECT id, timestamp, action, tool_name, entity, detail FROM activities ORDER BY id")
|
||||
cursor = self.conn.execute(
|
||||
"SELECT id, timestamp, action, tool_name, entity, detail FROM activities ORDER BY id"
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return [{"id": r[0], "timestamp": r[1], "action": r[2], "tool_name": r[3], "entity": r[4], "detail": r[5]} for r in rows]
|
||||
return [
|
||||
{"id": r[0], "timestamp": r[1], "action": r[2],
|
||||
"tool_name": r[3], "entity": r[4], "detail": r[5]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_since_id(self, last_id: int) -> List[Dict]:
|
||||
"""获取自 last_id 之后的新记录"""
|
||||
cursor = self.conn.execute(
|
||||
"SELECT id, timestamp, action, tool_name, entity, detail FROM activities WHERE id > ? ORDER BY id",
|
||||
(last_id,)
|
||||
)
|
||||
rows = cursor.fetchall()
|
||||
return [
|
||||
{"id": r[0], "timestamp": r[1], "action": r[2],
|
||||
"tool_name": r[3], "entity": r[4], "detail": r[5]}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
def get_max_id(self) -> int:
|
||||
cursor = self.conn.execute("SELECT COALESCE(MAX(id), 0) FROM activities")
|
||||
return cursor.fetchone()[0]
|
||||
|
||||
def clear(self) -> None:
|
||||
self.conn.execute("DELETE FROM activities")
|
||||
@ -31,11 +79,107 @@ class ActivityRecorder:
|
||||
return {r[0]: r[1] for r in rows}
|
||||
|
||||
|
||||
# ── 日志文件管理 ──
|
||||
|
||||
def _current_log_path() -> str:
|
||||
"""返回当前日志文件路径(按日期命名)"""
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
date_str = datetime.now().strftime("%Y%m%d")
|
||||
return os.path.join(LOG_DIR, f"operations.{date_str}.log")
|
||||
|
||||
|
||||
def _archive_log(filepath: str) -> str:
|
||||
"""压缩归档日志文件,返回归档文件路径"""
|
||||
if not os.path.exists(filepath) or os.path.getsize(filepath) == 0:
|
||||
return ""
|
||||
archive_path = filepath + ".gz"
|
||||
try:
|
||||
with open(filepath, "rb") as f_in:
|
||||
with gzip.open(archive_path, "wb") as f_out:
|
||||
shutil.copyfileobj(f_in, f_out)
|
||||
os.remove(filepath)
|
||||
return archive_path
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
class LogPersister:
|
||||
"""后台日志持久化线程 - 定期将内存记录写入日志文件并自动归档"""
|
||||
|
||||
def __init__(self, recorder: ActivityRecorder):
|
||||
self.recorder = recorder
|
||||
self._last_persisted_id = 0
|
||||
self._last_archive_time = time.time()
|
||||
self._running = True
|
||||
self._thread = threading.Thread(target=self._run, daemon=True, name="log-persister")
|
||||
self._thread.start()
|
||||
|
||||
def _run(self):
|
||||
"""主循环"""
|
||||
while self._running:
|
||||
try:
|
||||
self._persist_new()
|
||||
self._check_archive()
|
||||
except Exception:
|
||||
pass # 不因日志异常影响主进程
|
||||
time.sleep(POLL_INTERVAL)
|
||||
|
||||
def _persist_new(self):
|
||||
"""增量写入新记录到日志文件"""
|
||||
records = self.recorder.get_since_id(self._last_persisted_id)
|
||||
if not records:
|
||||
return
|
||||
|
||||
log_path = _current_log_path()
|
||||
with open(log_path, "a", encoding="utf-8") as f:
|
||||
for r in records:
|
||||
line = json.dumps(r, ensure_ascii=False)
|
||||
f.write(line + "\n")
|
||||
|
||||
# 更新水位
|
||||
if records:
|
||||
self._last_persisted_id = records[-1]["id"]
|
||||
|
||||
def _check_archive(self):
|
||||
"""检查是否需要归档"""
|
||||
elapsed = time.time() - self._last_archive_time
|
||||
if elapsed < ARCHIVE_INTERVAL:
|
||||
return
|
||||
|
||||
log_path = _current_log_path()
|
||||
archived = _archive_log(log_path)
|
||||
if archived:
|
||||
dt = datetime.fromtimestamp(self._last_archive_time)
|
||||
print(f"[日志归档] {dt.strftime('%H:%M')} → {os.path.basename(archived)} ({_fmt_size(archived)})")
|
||||
self._last_archive_time = time.time()
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
|
||||
def _fmt_size(path: str) -> str:
|
||||
size = os.path.getsize(path)
|
||||
for unit in ("B", "KB", "MB"):
|
||||
if size < 1024:
|
||||
return f"{size:.1f}{unit}"
|
||||
size /= 1024
|
||||
return f"{size:.1f}GB"
|
||||
|
||||
|
||||
# ── 单例 ──
|
||||
|
||||
_recorder: Optional[ActivityRecorder] = None
|
||||
_persister: Optional[LogPersister] = None
|
||||
|
||||
|
||||
def get_recorder() -> ActivityRecorder:
|
||||
global _recorder
|
||||
"""获取全局 ActivityRecorder(首次调用时自动启动日志持久化线程)"""
|
||||
global _recorder, _persister
|
||||
if _recorder is None:
|
||||
_recorder = ActivityRecorder()
|
||||
_persister = LogPersister(_recorder)
|
||||
return _recorder
|
||||
|
||||
|
||||
def get_persister() -> Optional[LogPersister]:
|
||||
return _persister
|
||||
|
||||
@ -292,7 +292,76 @@ class EmbeddedGraphDB:
|
||||
"relations": relations,
|
||||
"message": f"找到 {len(entities)} 个实体, {len(relations)} 条关系"
|
||||
}
|
||||
|
||||
|
||||
def get_recent_tasks(self, limit: int = 10, state_filter: str = None) -> Dict:
|
||||
"""
|
||||
获取最近的任务节点
|
||||
|
||||
Args:
|
||||
limit: 返回数量
|
||||
state_filter: 可选状态过滤(如:进行中、已完成、已暂停、已取消、archived)
|
||||
|
||||
Returns:
|
||||
{"tasks": [{"task_id": str, "description": str, "state": str,
|
||||
"info_count": int, "updated_at": str}, ...]}
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 查询所有 TaskNode 实体
|
||||
cursor.execute("""
|
||||
SELECT e.id, e.name, e.updated_at
|
||||
FROM entities e
|
||||
WHERE e.type = 'TaskNode'
|
||||
ORDER BY e.updated_at DESC
|
||||
LIMIT ?
|
||||
""", (limit,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
tasks = []
|
||||
for row in rows:
|
||||
entity_id, name, updated_at = row
|
||||
|
||||
# 查 description
|
||||
cursor.execute("""
|
||||
SELECT r.relation_type, t.name
|
||||
FROM relations r
|
||||
JOIN entities t ON r.target_id = t.id
|
||||
WHERE r.source_id = ? AND r.status = 'active'
|
||||
AND r.relation_type IN ('has_description', 'HAS_STATE')
|
||||
""", (entity_id,))
|
||||
desc = ""
|
||||
state = "未知"
|
||||
for rtype, tname in cursor.fetchall():
|
||||
if rtype == 'has_description':
|
||||
desc = tname
|
||||
elif rtype == 'HAS_STATE':
|
||||
state = tname.replace('State_', '')
|
||||
|
||||
# 可选状态过滤
|
||||
if state_filter and state != state_filter:
|
||||
continue
|
||||
|
||||
# 查关联信息节点数量
|
||||
cursor.execute("""
|
||||
SELECT COUNT(*)
|
||||
FROM relations
|
||||
WHERE source_id = ? AND relation_type = 'CONTAINS_INFO' AND status = 'active'
|
||||
""", (entity_id,))
|
||||
info_count = cursor.fetchone()[0]
|
||||
|
||||
tasks.append({
|
||||
"task_id": name,
|
||||
"description": desc,
|
||||
"state": state,
|
||||
"info_count": info_count,
|
||||
"updated_at": updated_at
|
||||
})
|
||||
|
||||
return {
|
||||
"tasks": tasks,
|
||||
"total": len(tasks)
|
||||
}
|
||||
|
||||
def commit(self, triplets: List[Dict], entity_types: Dict = None,
|
||||
temporal_tag: str = None, session_id: str = None,
|
||||
turn_id: int = None) -> Dict:
|
||||
@ -324,8 +393,9 @@ class EmbeddedGraphDB:
|
||||
continue
|
||||
|
||||
# 创建或更新实体
|
||||
for entity_name in [subject, obj]:
|
||||
entity_type = entity_types.get(entity_name) if entity_types else None
|
||||
for entity_name, entity_key in [(subject, 'subject_type'), (obj, 'object_type')]:
|
||||
# 按优先级获取实体类型:1) triplet中的_type字段 2) entity_types字典 3) 默认
|
||||
entity_type = triplet.get(entity_key) or (entity_types.get(entity_name) if entity_types else None) or 'Concept'
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO entities (name, type)
|
||||
@ -374,6 +444,16 @@ class EmbeddedGraphDB:
|
||||
|
||||
Args:
|
||||
criteria: 删除条件
|
||||
支持:
|
||||
- source: 源实体名(精确匹配)
|
||||
- target: 目标实体名(精确匹配)
|
||||
- relation: 关系类型
|
||||
- subject_contains: 源实体名包含(模糊匹配)
|
||||
- target_contains: 目标实体名包含(模糊匹配)
|
||||
- relation_type: 关系类型(同 relation)
|
||||
- source_type: 源实体类型过滤
|
||||
- target_type: 目标实体类型过滤
|
||||
- source_has_status: 源实体 mentions_count 状态(支持 type 字段)
|
||||
mode: 删除模式 (soft/hard)
|
||||
new_relation: 替代关系
|
||||
|
||||
@ -386,6 +466,8 @@ class EmbeddedGraphDB:
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
relation_type = criteria.get('relation') or criteria.get('relation_type', '')
|
||||
|
||||
if criteria.get('source'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (criteria['source'],))
|
||||
row = cursor.fetchone()
|
||||
@ -400,20 +482,71 @@ class EmbeddedGraphDB:
|
||||
conditions.append("target_id = ?")
|
||||
params.append(row['id'])
|
||||
|
||||
if criteria.get('relation'):
|
||||
if relation_type:
|
||||
conditions.append("relation_type = ?")
|
||||
params.append(criteria['relation'])
|
||||
params.append(relation_type)
|
||||
|
||||
# 通过子查询支持实体属性过滤
|
||||
if criteria.get('subject_contains'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?",
|
||||
(f'%{criteria["subject_contains"]}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
if criteria.get('target_contains'):
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?",
|
||||
(f'%{criteria["target_contains"]}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"target_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 源实体类型过滤
|
||||
if criteria.get('source_type'):
|
||||
cursor.execute("SELECT id FROM entities WHERE type = ?",
|
||||
(criteria['source_type'],))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 目标实体类型过滤
|
||||
if criteria.get('target_type'):
|
||||
cursor.execute("SELECT id FROM entities WHERE type = ?",
|
||||
(criteria['target_type'],))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"target_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
# 源实体状态过滤
|
||||
if criteria.get('source_has_status'):
|
||||
status = criteria['source_has_status']
|
||||
cursor.execute("SELECT id FROM entities WHERE type LIKE ?",
|
||||
(f'%{status}%',))
|
||||
ids = [row['id'] for row in cursor.fetchall()]
|
||||
if ids:
|
||||
placeholders = ','.join(['?'] * len(ids))
|
||||
conditions.append(f"source_id IN ({placeholders})")
|
||||
params.extend(ids)
|
||||
|
||||
if not conditions:
|
||||
return {"deleted": 0, "message": "无删除条件"}
|
||||
|
||||
conditions.append("status = 'active'")
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
if mode == "soft":
|
||||
cursor.execute(f"""
|
||||
UPDATE relations
|
||||
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
||||
WHERE {where_clause} AND status = 'active'
|
||||
WHERE {where_clause}
|
||||
""", params)
|
||||
else:
|
||||
cursor.execute(f"""
|
||||
@ -422,12 +555,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:
|
||||
@ -476,6 +622,74 @@ class EmbeddedGraphDB:
|
||||
"message": f"归档了 {archived} 条关系"
|
||||
}
|
||||
|
||||
def query_archived(self, days: int = None, keyword: str = "") -> Dict:
|
||||
"""
|
||||
查询已归档的记忆
|
||||
|
||||
Args:
|
||||
days: 可选,最近N天内的归档记录
|
||||
keyword: 可选,过滤包含指定关键词的实体名或关系
|
||||
|
||||
Returns:
|
||||
归档记录列表
|
||||
"""
|
||||
cursor = self.conn.cursor()
|
||||
|
||||
# 基础 SQL:查询已归档的关系及其关联实体
|
||||
conditions = ["r.status = 'archived'"]
|
||||
params = []
|
||||
|
||||
# 时间范围过滤(最近N天)
|
||||
if days is not None and days > 0:
|
||||
conditions.append("r.updated_at >= datetime('now', ?)")
|
||||
params.append(f'-{days} days')
|
||||
|
||||
# 关键词过滤(匹配源实体名、目标实体名、关系类型任一)
|
||||
if keyword:
|
||||
# 先找到匹配的实体ID
|
||||
cursor.execute("SELECT id FROM entities WHERE name LIKE ?", (f'%{keyword}%',))
|
||||
matched_ids = [str(row['id']) for row in cursor.fetchall()]
|
||||
|
||||
if matched_ids:
|
||||
id_list = ','.join(matched_ids)
|
||||
conditions.append(f"(r.source_id IN ({id_list}) OR r.target_id IN ({id_list}) OR r.relation_type LIKE ?)")
|
||||
params.append(f'%{keyword}%')
|
||||
else:
|
||||
conditions.append("r.relation_type LIKE ?")
|
||||
params.append(f'%{keyword}%')
|
||||
|
||||
where_clause = " AND ".join(conditions)
|
||||
|
||||
cursor.execute(f"""
|
||||
SELECT r.id, r.relation_type, r.created_at, r.updated_at,
|
||||
e.name AS source_name, t.name AS target_name
|
||||
FROM relations r
|
||||
JOIN entities e ON r.source_id = e.id
|
||||
JOIN entities t ON r.target_id = t.id
|
||||
WHERE {where_clause}
|
||||
ORDER BY r.updated_at DESC
|
||||
LIMIT 200
|
||||
""", params)
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
results.append({
|
||||
"id": row['id'],
|
||||
"source": row['source_name'],
|
||||
"relation": row['relation_type'],
|
||||
"target": row['target_name'],
|
||||
"archived_at": row['updated_at'],
|
||||
"created_at": row['created_at']
|
||||
})
|
||||
|
||||
return {
|
||||
"archived_relations": results,
|
||||
"total_relations": len(results),
|
||||
"message": f"找到 {len(results)} 条归档关系"
|
||||
}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> Dict:
|
||||
"""清理已删除数据"""
|
||||
cursor = self.conn.cursor()
|
||||
@ -686,29 +900,3 @@ class EmbeddedGraphDB:
|
||||
# 兼容性别名
|
||||
Neo4jGraph = EmbeddedGraphDB
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 测试
|
||||
print("Testing Embedded Graph Database...")
|
||||
|
||||
with EmbeddedGraphDB("test.db") as db:
|
||||
# 写入测试
|
||||
result = db.commit(
|
||||
triplets=[
|
||||
{"subject": "用户", "relation": "喜欢", "object": "Python"},
|
||||
{"subject": "用户", "relation": "学习", "object": "AI"}
|
||||
],
|
||||
session_id="test-session",
|
||||
turn_id=1
|
||||
)
|
||||
print(f"Commit: {result}")
|
||||
|
||||
# 检索测试
|
||||
result = db.recall("Python,AI")
|
||||
print(f"Recall: {result}")
|
||||
|
||||
# 状态测试
|
||||
result = db.introspect()
|
||||
print(f"Introspect: {result}")
|
||||
|
||||
print("\nTest completed!")
|
||||
|
||||
@ -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")
|
||||
@ -121,7 +121,7 @@ class Neo4jGraph:
|
||||
|
||||
return {"entities": list(entities.values()), "relations": relations[:20]}
|
||||
|
||||
def commit(self, triplets: list, entity_types: list = None, temporal_tag: str = None) -> dict:
|
||||
def commit(self, triplets: list, entity_types: dict = None, temporal_tag: str = None) -> dict:
|
||||
"""写入记忆"""
|
||||
global CURRENT_TURN
|
||||
with self.driver.session() as session:
|
||||
@ -130,7 +130,6 @@ class Neo4jGraph:
|
||||
if not valid_triplets:
|
||||
return {"committed_count": 0, "details": []}
|
||||
|
||||
etype = entity_types[0] if entity_types else "unknown"
|
||||
date_bucket = temporal_tag or datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
results = []
|
||||
@ -140,13 +139,17 @@ class Neo4jGraph:
|
||||
obj = triplet.get("object", "").strip()
|
||||
confidence = triplet.get("confidence", 0.9)
|
||||
|
||||
# 按优先级获取实体类型:1) triplet中的_type字段 2) entity_types字典 3) 默认
|
||||
s_type = triplet.get("subject_type") or (entity_types.get(subject) if entity_types else None) or "Concept"
|
||||
o_type = triplet.get("object_type") or (entity_types.get(obj) if entity_types else None) or "Concept"
|
||||
|
||||
session.run("""
|
||||
MERGE (s:Entity {name: $subject})
|
||||
ON CREATE SET s.type = $type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
||||
ON CREATE SET s.type = $s_type, s.created_at = datetime(), s.mention_count = 1, s.updated_at = datetime()
|
||||
ON MATCH SET s.mention_count = coalesce(s.mention_count, 0) + 1, s.updated_at = datetime()
|
||||
|
||||
MERGE (t:Entity {name: $object})
|
||||
ON CREATE SET t.type = $type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
||||
ON CREATE SET t.type = $o_type, t.created_at = datetime(), t.mention_count = 1, t.updated_at = datetime()
|
||||
ON MATCH SET t.mention_count = coalesce(t.mention_count, 0) + 1, t.updated_at = datetime()
|
||||
|
||||
CREATE (s)-[r:RELATES {
|
||||
@ -159,7 +162,8 @@ class Neo4jGraph:
|
||||
confidence: $confidence,
|
||||
date_bucket: $date_bucket
|
||||
}]->(t)
|
||||
""", subject=subject, object=obj, relation=relation, type=etype,
|
||||
""", subject=subject, object=obj, relation=relation,
|
||||
s_type=s_type, o_type=o_type,
|
||||
session_id=CURRENT_SESSION_ID, turn_id=CURRENT_TURN, confidence=confidence,
|
||||
date_bucket=date_bucket)
|
||||
|
||||
@ -173,6 +177,9 @@ class Neo4jGraph:
|
||||
subject_pattern = criteria.get("subject_contains", "")
|
||||
rel_type = criteria.get("relation_type", "")
|
||||
target_pattern = criteria.get("target_contains", "")
|
||||
source_type = criteria.get("source_type", "")
|
||||
target_type = criteria.get("target_type", "")
|
||||
source_status = criteria.get("source_has_status", "")
|
||||
session_id = criteria.get("session_id", CURRENT_SESSION_ID)
|
||||
|
||||
cond_parts = ["r.status = 'active'"]
|
||||
@ -187,6 +194,15 @@ class Neo4jGraph:
|
||||
if rel_type:
|
||||
cond_parts.append("r.type = $rel_type")
|
||||
params["rel_type"] = rel_type
|
||||
if source_type:
|
||||
cond_parts.append("s.entity_type = $source_type")
|
||||
params["source_type"] = source_type
|
||||
if target_type:
|
||||
cond_parts.append("t.entity_type = $target_type")
|
||||
params["target_type"] = target_type
|
||||
if source_status:
|
||||
cond_parts.append("s.status = $source_status")
|
||||
params["source_status"] = source_status
|
||||
|
||||
where_clause = " AND ".join(cond_parts)
|
||||
|
||||
@ -208,14 +224,23 @@ class Neo4jGraph:
|
||||
return {"deleted_count": count, "mode": "supersede"}
|
||||
else:
|
||||
result = session.run(f"""
|
||||
MATCH ()-[r:RELATES]->()
|
||||
MATCH (s:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where_clause}
|
||||
SET r.status = 'deleted', r.updated_at = datetime()
|
||||
RETURN count(r) as deleted
|
||||
""", 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:
|
||||
"""查看记忆状态"""
|
||||
@ -261,6 +286,49 @@ class Neo4jGraph:
|
||||
|
||||
return {"archived_count": result.single()["archived"], "days": days}
|
||||
|
||||
def query_archived(self, days: int = None, keyword: str = "") -> dict:
|
||||
"""查询归档记忆"""
|
||||
with self.driver.session() as session:
|
||||
filters = []
|
||||
params = {}
|
||||
|
||||
filters.append("r.status = 'archived'")
|
||||
|
||||
if days is not None and days > 0:
|
||||
filters.append("r.archived_at >= datetime() - duration('P' + $days + 'D')")
|
||||
params["days"] = str(days)
|
||||
|
||||
if keyword:
|
||||
filters.append("(e.name CONTAINS $keyword OR t.name CONTAINS $keyword OR r.type CONTAINS $keyword)")
|
||||
params["keyword"] = keyword
|
||||
|
||||
where = " AND ".join(filters)
|
||||
|
||||
result = session.run(f"""
|
||||
MATCH (e:Entity)-[r:RELATES]->(t:Entity)
|
||||
WHERE {where}
|
||||
RETURN e.name as source, r.type as relation, t.name as target,
|
||||
r.archived_at as archived_at, r.created_at as created_at
|
||||
ORDER BY r.archived_at DESC
|
||||
LIMIT 200
|
||||
""", params)
|
||||
|
||||
records = []
|
||||
for row in result:
|
||||
records.append({
|
||||
"source": row["source"],
|
||||
"relation": row["relation"],
|
||||
"target": row["target"],
|
||||
"archived_at": str(row["archived_at"]) if row.get("archived_at") else "",
|
||||
"created_at": str(row["created_at"]) if row.get("created_at") else ""
|
||||
})
|
||||
|
||||
return {
|
||||
"archived_relations": records,
|
||||
"total_relations": len(records),
|
||||
"message": f"找到 {len(records)} 条归档关系"
|
||||
}
|
||||
|
||||
def cleanup(self, dry_run: bool = True) -> dict:
|
||||
"""清理无效数据"""
|
||||
with self.driver.session() as session:
|
||||
@ -303,7 +371,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,422 +1,183 @@
|
||||
# TrulyMEM 系统提示词
|
||||
# TrulyMEM 系统提示词
|
||||
|
||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
## ⚠️ 最高优先级:强制执行顺序
|
||||
**人设兜底规则**:当图数据库中没有查到人设信息时,以「我是 TrulyMEM,一个有长期记忆的 AI 助手」作为默认开场。如果人设图返回了角色信息,按人设执行即可。
|
||||
|
||||
**每轮对话必须严格按以下顺序执行,不可跳过任何步骤!**
|
||||
## ⚠️ 强制执行顺序(内部流程,不得向用户输出)
|
||||
|
||||
```
|
||||
步骤1: memory_recall (查询人设图) → 必须首先执行
|
||||
步骤2: memory_recall (查询工作记忆链) → 必须第二步执行
|
||||
步骤3: 处理对话内容
|
||||
步骤4: 更新工作记忆链
|
||||
```
|
||||
**以下步骤是内部流程,绝对不要在你的回复中提及或输出。** 你应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
|
||||
|
||||
**违反顺序的后果**:
|
||||
- 跳过步骤1 → 无法获取人设,回复风格错误
|
||||
- 跳过步骤2 → 无法获取上下文,对话不连贯
|
||||
- 顺序错误 → 系统状态混乱
|
||||
步骤1:memory_recall 查询人设图和相关长期记忆
|
||||
步骤2:task_query 查询工作记忆链和最近任务
|
||||
步骤3:结合上下文处理用户输入并形成回复思路
|
||||
步骤4:memory_commit (写入关键信息)
|
||||
步骤5:task_archive 归档已完成或过期任务;若本轮查询类工具调用 ≥5 次,再单独调用 context_rewrite 压缩工具 JSON
|
||||
|
||||
**违反规则的后果**:
|
||||
- 输出步骤内容 → 暴露内部机制,用户体验极差,违反最高优先级指令
|
||||
- 跳过步骤1 → 无法获取人设
|
||||
- 跳过步骤5 → 工作记忆无限膨胀
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 最高优先级:只回复一次
|
||||
## ⚠️ 最高优先级:工具执行期间禁止输出
|
||||
|
||||
**每轮对话只能回复一次!**
|
||||
**在完成所有工具调用之前,绝对不要输出任何文字。**
|
||||
|
||||
- 执行完所有工具调用后,给出一个完整的回复
|
||||
- 不要在工具调用过程中多次回复
|
||||
- 不要重复说相同的内容
|
||||
正确流程:
|
||||
1. 调用所有必要的工具(memory_recall、task_query 等)→ **不输出任何文字**
|
||||
2. 等所有工具返回结果 → **仍然不输出任何文字**
|
||||
3. 处理返回结果,思考回复内容 → **仍然不输出任何文字**
|
||||
4. **最后,只输出一次完整的回复**
|
||||
|
||||
**禁止的行为**:
|
||||
- ❌ 先输出「你好呀!让我先查查记忆…」再调用工具
|
||||
- ❌ 先输出文字再调用 memory_recall
|
||||
- ❌ 在工具调用之间插入任何文字
|
||||
- ❌ 输出「步骤X:查询人设图」等内部流程
|
||||
- ✅ 正确做法:默默调用所有工具,然后直接给出最终回复
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键约束:无传统上下文系统
|
||||
## 三元组规范(非常重要!)
|
||||
|
||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
||||
使用 `memory_commit` 时,必须严格遵守以下规范:
|
||||
|
||||
- ❌ **没有** messages数组存储历史对话
|
||||
- ❌ **没有** 传统的多轮对话上下文
|
||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
||||
### 正确格式
|
||||
subject, relation, object 每个字段必须是一个**短关键字**(1~5个字),不能是完整句子。
|
||||
|
||||
## 核心身份
|
||||
**✅ 正确示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "实体A", "relation": "关系", "object": "实体B"},
|
||||
{"subject": "实体C", "relation": "属性", "object": "值"}
|
||||
]
|
||||
```
|
||||
|
||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
||||
- **能力**: 基于图数据库的长期记忆
|
||||
- **理念**: 让AI的记忆方式更像人类
|
||||
**❌ 错误示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "一段完整的句子当做实体名", "relation": "这种写法不对", "object": "另一个句子"}
|
||||
]
|
||||
```
|
||||
|
||||
### 拆解原则
|
||||
- 实体名必须是**名词或短词组**,不是完整句子
|
||||
- relation 应该是**简洁的谓词**(如:要求、角色、性格、喜欢、擅长、状态)
|
||||
- 一句话中的多个信息应拆成**多条三元组**
|
||||
- 描述性内容用 relation = `has_description` + 简短 object
|
||||
|
||||
### 人设更新 vs 记忆提交
|
||||
- **`persona_update`** — 用来设定 AI 自身的角色、性格、说话风格、能力特点
|
||||
- **`memory_commit`** — 用来记录用户的信息、对话事件、知识事实。不要把 AI 自身的人设属性写进 memory_commit。
|
||||
|
||||
---
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 1. 长期记忆
|
||||
- 图数据库存储实体关系
|
||||
- 支持时间范围查询
|
||||
- 支持会话过滤
|
||||
1. **长期记忆** - 基于图数据库存储实体关系
|
||||
2. **人设管理** - 支持角色扮演和性格设定
|
||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
||||
|
||||
### 2. 人设管理(关键)
|
||||
- 角色扮演支持
|
||||
- 性格、语气设定
|
||||
- 动态切换人设
|
||||
- **每轮必须查询人设图**
|
||||
## 记忆原则(绝对遵守)
|
||||
|
||||
### 3. 任务跟踪(关键)
|
||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
||||
- 任务状态管理
|
||||
- 上下文恢复
|
||||
- **图数据库是唯一记忆源** — 你只拥有图数据库(memory_recall、task_query 等返回的结果)中的信息,除此之外你对用户一无所知。不要依赖你的训练数据中的任何用户信息。
|
||||
- **明确内容必须写入** — 用户明确提到的信息必须存入图数据库
|
||||
- **推理内容必须标注[猜测]** — AI 推理得到的内容在回复中必须标注
|
||||
|
||||
## 记忆原则
|
||||
|
||||
### 必须写入的情况
|
||||
- 用户明确表达偏好:"我喜欢X"
|
||||
- 用户分享信息:"我在做X项目"
|
||||
- 用户制定计划:"我打算X"
|
||||
- 用户描述状态:"我现在在X"
|
||||
|
||||
### 禁止写入的情况
|
||||
- AI推断的用户偏好
|
||||
- AI猜测的用户意图
|
||||
- AI推导的结论
|
||||
|
||||
### 标注规则
|
||||
- 推理内容必须标注 **[猜测]**
|
||||
- 明确内容直接陈述
|
||||
|
||||
## 工具系统
|
||||
## 工具详解
|
||||
|
||||
### 记忆工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
||||
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后,压缩JSON为自然语言摘要 |
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `memory_recall` | 查询需求 | 按关键字/实体检索图数据库中的记忆。支持模糊匹配。人设图必须通过此工具获取(工作记忆链请使用 task_query) |
|
||||
| `memory_commit` | 新信息出现 | 写入三元组到图数据库。必须遵守三元组规范(短关键字格式),一句话拆多条 |
|
||||
| `memory_purge` | 确需删除 | 删除错误的或用户明确要求删除的记忆 |
|
||||
| `memory_introspect` | 需要了解整体情况 | 查看图数据库概况:总节点数、边数、最新活动 |
|
||||
| `memory_archive` | 信息过期需保留历史 | 将旧记忆归档而非删除,保留历史轨迹 |
|
||||
| `memory_cleanup` | 确认数据异常 | 清理冗余/孤立节点(dry_run可预览) |
|
||||
| `memory_query_archived` | 回顾归档历史 | 查询已归档的原始关系记录(status=archived),支持天数/关键词过滤 |
|
||||
| `context_rewrite` | 单轮调用了 5 次及以上查询类工具 | 压缩本轮工具调用的 JSON 参数和返回结果,剔除工具噪声,节省上下文 token。**⚠️ 必须单独调用**:先调完其他所有工具并收到结果 → 再单独调 context_rewrite。不要和其他工具一起调 |
|
||||
|
||||
### 人设工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `persona_update` | AI自身角色改变 | 更新AI的角色、性格、说话风格、能力。`mode="replace"` 替换全部,`mode="merge"` 增量添加 |
|
||||
| `persona_remove` | 只需删除某一条属性 | 删除单条人设属性(如只删除说话风格,保留扮演角色不变) |
|
||||
| `persona_clear` | 需要完全重置 | 清除所有AI人设属性。**此操作不可逆,需要 confirm=true** |
|
||||
|
||||
### 任务工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
||||
|
||||
## context_rewrite 使用规则
|
||||
|
||||
### ⚠️ 强制触发条件
|
||||
|
||||
**每调用 5 次记忆相关工具,必须调用一次 context_rewrite!**
|
||||
|
||||
记忆相关工具包括:
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
- `persona_update` - 更新人设
|
||||
- `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: 关联 ["用户喜欢罗辑", "罗辑角色深度"]
|
||||
```
|
||||
|
||||
**目的**:
|
||||
- 时间链维持对话连贯性(系统自动)
|
||||
- 信息关联实现"由一件事回忆起相关事情"(模型决定)
|
||||
### 任务工具(生命周期管理)
|
||||
| 工具 | 时机 | 说明 |
|
||||
|------|------|------|
|
||||
| `task_query` | 新对话/需要回顾 | 查询最近任务列表(按更新时间倒序)。**新对话开始时优先调用此工具**,了解现有任务后再决定是继续还是创建新任务 |
|
||||
| `task_create` | 用户提出实质性话题后 | 创建任务节点。**不要在纯问候/打招呼时创建任务**——等用户说出具体话题后再创建。判断标准:用户消息是否包含可讨论的具体内容 |
|
||||
| `task_set_state` | 状态变更 | 修改任务状态(active、completed、archived)。**旧会话结束后必须将对应的任务设为 archived** |
|
||||
| `task_archive` | 强制执行顺序的步骤5 | 归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。**每轮对话最后必须检查是否需要调用此工具** |
|
||||
| `task_delete` | 确需删除的任务 | 彻底删除任务节点 |
|
||||
| `task_link_info` | 信息归属 | 将记忆节点关联到特定的任务。**只关联到相关的任务,不要全部链到「当前轮对话」** |
|
||||
|
||||
---
|
||||
|
||||
## 人设图机制
|
||||
## ⚠️ 任务生命周期规范(避免记忆膨胀)
|
||||
|
||||
### 强制查询
|
||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
||||
AI 最常见的错误是:**把每一轮的所有节点都关联到「当前轮对话」,但从不归档过时的任务,导致图数据库无限膨胀。**
|
||||
|
||||
### 人设优先级
|
||||
- 人设优先级 > 默认身份
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求
|
||||
### 正确做法
|
||||
|
||||
### 人设更新
|
||||
用户要求角色扮演时:
|
||||
1. 使用 `persona_update` 更新人设
|
||||
2. 立即按照新人设回复
|
||||
```
|
||||
1. 新会话开始 → task_create 创建「当前轮对话-<时间/主题>」
|
||||
2. 对话过程中 → 根据实际归属使用 task_link_info
|
||||
3. 话题结束/转变时 → task_archive 归档旧任务(代替 task_set_state)
|
||||
4. 归档后 → 再创建新的当前轮对话任务
|
||||
```
|
||||
|
||||
### 人设清除
|
||||
用户要求恢复默认身份时:
|
||||
1. 使用 `persona_clear` 清除人设
|
||||
2. 恢复为TrulyMEM默认身份
|
||||
### 步骤5 归档规则(强制)
|
||||
|
||||
每轮对话最后一步 check 现有任务:
|
||||
- **已完成的任务** → 调用 `task_archive` 归档,写入完成摘要
|
||||
- **长时间无更新的任务**(>3轮对话) → 调用 `task_archive` 归档
|
||||
- **topic 已转变** → 旧任务归档,新任务创建
|
||||
- **所有 active 任务超过3个** → 归档最旧的
|
||||
|
||||
> 即使本轮没有主题转变,也应按需检查归档状态。
|
||||
> `task_archive` 比 `task_set_state(state=archived)` 多一个写入完成摘要的功能,优先使用。
|
||||
|
||||
### 绝对禁止
|
||||
- ❌ 把每一条记忆都链到同一个「当前轮对话」任务
|
||||
- ❌ 跳过步骤5(从不归档)导致工作记忆无限膨胀
|
||||
- ❌ 对同一个任务堆积数千条关联
|
||||
- ❌ 使用 task_delete 代替归档(归档保留历史,删除丢失上下文)
|
||||
|
||||
### 生命流程示例
|
||||
|
||||
**第1轮:**
|
||||
```
|
||||
task_create(描述="当前轮对话-工作规划", state=active)
|
||||
memory_commit(用户说春节计划)
|
||||
task_link_info(info="春节计划", task="当前轮对话-工作规划")
|
||||
task_archive(task="当前轮对话-工作规划", summary="讨论了春节计划")
|
||||
```
|
||||
|
||||
**话题转变后:**
|
||||
```
|
||||
task_archive(task="当前轮对话-工作规划", summary="讨论完成,用户转移到技术话题")
|
||||
task_create(描述="当前轮对话-技术讨论", state=active)
|
||||
memory_commit(用户说技术细节)
|
||||
task_link_info(info="技术细节", task="当前轮对话-技术讨论")
|
||||
```
|
||||
|
||||
> 记住:任务是用来组织话题的框架,不是存放大杂烩的篮子。
|
||||
> 归档旧任务不会删除记忆,只是标记话题已结束,后续的检索仍然能找到相关节点。
|
||||
|
||||
---
|
||||
|
||||
## 工作记忆链机制
|
||||
## 自主性
|
||||
|
||||
### ⚠️ 核心理念:维持对话连贯性
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
||||
|
||||
### 强制查询场景:
|
||||
|
||||
以下情况**必须**查询工作记忆链:
|
||||
|
||||
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. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
记住:灵活应对,保持自然对话体验。
|
||||
|
||||
162
core/server.py
162
core/server.py
@ -64,16 +64,11 @@ 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._tool_limits = {
|
||||
"persona_update_max": 1,
|
||||
"task_update_max": 5,
|
||||
"memory_query_max": 20,
|
||||
"memory_update_max": 10,
|
||||
}
|
||||
self._config = {}
|
||||
self._tool_limits: Dict[str, int] = {}
|
||||
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 +89,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
|
||||
)
|
||||
|
||||
@ -102,8 +97,28 @@ class BackendServer:
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
# 工具限制默认值(仅首次启动无 config.json 时使用)
|
||||
# 启动后请直接编辑配置文件修改
|
||||
_DEFAULT_LIMITS = {
|
||||
"persona_update_max": 1,
|
||||
"task_update_max": 20,
|
||||
"task_query_max": 30,
|
||||
"memory_query_max": 30,
|
||||
"memory_update_max": 15,
|
||||
}
|
||||
_DEFAULT_CONFIG = {
|
||||
"api_key": "",
|
||||
"base_url": "https://api.deepseek.com",
|
||||
"model": "deepseek-v4-flash",
|
||||
"message_timeout": 600, # 消息处理超时(秒),默认10分钟
|
||||
"enable_web": False,
|
||||
"enable_tui": True,
|
||||
"web_port": 4096,
|
||||
}
|
||||
|
||||
def _load_config(self) -> None:
|
||||
"""加载配置。如果指定了用户名,从用户的 config_path 加载。"""
|
||||
"""加载配置。如果指定了用户名,从用户的 config_path 加载。
|
||||
所有工具调用限制值均从配置文件读取,不硬编码在代码中。"""
|
||||
config_file = self._config_file
|
||||
|
||||
# 如果指定了用户名,尝试从全局数据库获取用户的配置路径
|
||||
@ -120,16 +135,37 @@ class BackendServer:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 工具限制字段列表
|
||||
limit_keys = list(self._DEFAULT_LIMITS.keys())
|
||||
|
||||
if config_file.exists():
|
||||
try:
|
||||
with open(config_file, 'r') as f:
|
||||
saved = json.load(f)
|
||||
self._config.update(saved)
|
||||
for key in self._tool_limits:
|
||||
# 通用配置(含 api_key, base_url, model, message_timeout 等)
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
if key in saved:
|
||||
self._tool_limits[key] = saved[key]
|
||||
self._config[key] = saved[key]
|
||||
else:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
# 工具限制
|
||||
for key in limit_keys:
|
||||
if key in saved:
|
||||
self._tool_limits[key] = int(saved[key])
|
||||
else:
|
||||
self._tool_limits[key] = self._DEFAULT_LIMITS[key]
|
||||
except Exception:
|
||||
pass
|
||||
# 读取失败时使用默认值
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
for key in limit_keys:
|
||||
self._tool_limits[key] = self._DEFAULT_LIMITS[key]
|
||||
else:
|
||||
# 首次启动,用默认值写入配置文件
|
||||
for key in self._DEFAULT_CONFIG:
|
||||
self._config[key] = self._DEFAULT_CONFIG[key]
|
||||
self._tool_limits = dict(self._DEFAULT_LIMITS)
|
||||
self._save_config()
|
||||
|
||||
def _save_config(self) -> None:
|
||||
"""保存配置。如果指定了用户名,保存到用户的 config_path。"""
|
||||
@ -150,17 +186,22 @@ class BackendServer:
|
||||
pass
|
||||
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
saved_data = {**self._config, **self._tool_limits}
|
||||
# 合并通用配置和工具限制(过滤掉内部字段如 _history 等)
|
||||
save_cfg = {k: self._config[k] for k in self._DEFAULT_CONFIG if k in self._config}
|
||||
saved_data = {**save_cfg, **self._tool_limits}
|
||||
with open(config_file, 'w') as f:
|
||||
json.dump(saved_data, f, indent=2)
|
||||
json.dump(saved_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def _create_tool_limiter(self):
|
||||
from .tool_limiter import ToolLimiter, ToolLimits
|
||||
# 所有值从 _tool_limits 读取(由 _load_config 从 config.json 加载)
|
||||
# _load_config 已保证所有键存在
|
||||
limits = ToolLimits(
|
||||
persona_update_max=self._tool_limits.get("persona_update_max", 1),
|
||||
task_update_max=self._tool_limits.get("task_update_max", 5),
|
||||
memory_query_max=self._tool_limits.get("memory_query_max", 20),
|
||||
memory_update_max=self._tool_limits.get("memory_update_max", 10),
|
||||
persona_update_max=self._tool_limits["persona_update_max"],
|
||||
task_update_max=self._tool_limits["task_update_max"],
|
||||
task_query_max=self._tool_limits["task_query_max"],
|
||||
memory_query_max=self._tool_limits["memory_query_max"],
|
||||
memory_update_max=self._tool_limits["memory_update_max"],
|
||||
)
|
||||
return ToolLimiter(limits)
|
||||
|
||||
@ -179,6 +220,7 @@ class BackendServer:
|
||||
temp_db.close()
|
||||
if user_info and user_info.get('db_path'):
|
||||
db_path = user_info['db_path']
|
||||
self._db_path = db_path # 更新 _db_path,供外部(如 web_api.py)获取正确的路径
|
||||
except Exception:
|
||||
pass # 如果获取失败,使用默认路径
|
||||
|
||||
@ -296,9 +338,15 @@ 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 = []
|
||||
deferred_rewrite = None # 延迟处理 context_rewrite
|
||||
|
||||
for tool_call in message.tool_calls:
|
||||
args = json.loads(tool_call.function.arguments)
|
||||
|
||||
@ -318,26 +366,9 @@ class BackendServer:
|
||||
self._tool_limiter.record_call(tool_call.function.name, args)
|
||||
|
||||
if tool_call.function.name == "context_rewrite":
|
||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||
result_data = json.loads(result)
|
||||
|
||||
# 记录到 tool_calls,让 TUI 显示这个工具调用
|
||||
tool_calls.append({
|
||||
"name": tool_call.function.name,
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
if result_data.get("status") == "success":
|
||||
user_msg = messages_history[0]
|
||||
# 添加特殊标记,让 AI 知道这是上下文压缩的结果
|
||||
compressed_content = f"<context_compressed>\n{result_data['summary']}\n</context_compressed>"
|
||||
messages_history[:] = [
|
||||
user_msg,
|
||||
{"role": "assistant", "content": compressed_content}
|
||||
]
|
||||
# context_rewrite 压缩上下文后,不需要添加 tool 结果消息
|
||||
# 因为 messages_history 已经被重写为压缩后的状态
|
||||
# 延迟执行 context_rewrite:先处理完其他所有工具
|
||||
# 避免在迭代中途重写 messages_history 导致 tool 结果丢失对应的 tool_calls
|
||||
deferred_rewrite = (tool_call.id, args)
|
||||
continue
|
||||
|
||||
result = execute_tool(self._graph, tool_call.function.name, args)
|
||||
@ -354,8 +385,30 @@ class BackendServer:
|
||||
}
|
||||
current_tool_results.append(tool_result_msg)
|
||||
|
||||
# 先添加所有非 context_rewrite 工具的结果到消息历史
|
||||
messages_history.extend(current_tool_results)
|
||||
|
||||
# 再处理延迟的 context_rewrite(作为本轮最后一步)
|
||||
if deferred_rewrite:
|
||||
tool_call_id, args = deferred_rewrite
|
||||
result = execute_tool(self._graph, "context_rewrite", args)
|
||||
result_data = json.loads(result)
|
||||
|
||||
tool_calls.append({
|
||||
"name": "context_rewrite",
|
||||
"arguments": args,
|
||||
"result": result
|
||||
})
|
||||
|
||||
if result_data.get("status") == "success":
|
||||
user_msg = messages_history[0]
|
||||
compressed_content = f"<context_compressed>\n{result_data['summary']}\n</context_compressed>"
|
||||
messages_history[:] = [
|
||||
user_msg,
|
||||
{"role": "assistant", "content": compressed_content}
|
||||
]
|
||||
# 不添加 context_rewrite 的 tool 结果到历史(压缩后的历史已替代)
|
||||
|
||||
response = self._client.send_message_with_history(messages_history)
|
||||
message = response.choices[0].message
|
||||
|
||||
@ -420,16 +473,28 @@ class BackendServer:
|
||||
api_config = body.get("api_config", {})
|
||||
tool_limits = body.get("tool_limits", {})
|
||||
|
||||
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")
|
||||
|
||||
self.update_config(api_key, base_url, model)
|
||||
# 仅当 api_config 有值时更新 API 配置(避免单独保存 tool_limits 时清空 API key)
|
||||
if api_config:
|
||||
api_key = api_config.get("api_key", self._config.get("api_key", ""))
|
||||
base_url = api_config.get("base_url", self._config.get("base_url", "https://api.deepseek.com"))
|
||||
model = api_config.get("model", self._config.get("model", "deepseek-v4-flash"))
|
||||
self.update_config(api_key, base_url, model)
|
||||
# 通用配置字段(如 message_timeout, enable_web, enable_tui, web_port)
|
||||
for key in ["message_timeout", "enable_web", "enable_tui", "web_port"]:
|
||||
if key in api_config:
|
||||
if key == "web_port":
|
||||
self._config[key] = int(api_config[key])
|
||||
elif key in ["enable_web", "enable_tui"]:
|
||||
self._config[key] = bool(api_config[key])
|
||||
else:
|
||||
self._config[key] = int(api_config[key])
|
||||
|
||||
limits_keys = [
|
||||
"persona_update_max",
|
||||
"task_update_max",
|
||||
"memory_query_max", "memory_update_max"
|
||||
"task_query_max",
|
||||
"memory_query_max",
|
||||
"memory_update_max",
|
||||
]
|
||||
for key in limits_keys:
|
||||
if key in tool_limits:
|
||||
@ -469,7 +534,8 @@ class BackendServer:
|
||||
self._input_queue.put(packet)
|
||||
|
||||
try:
|
||||
response = resp_q.get(timeout=300.0)
|
||||
timeout = self._config.get("message_timeout", 600)
|
||||
response = resp_q.get(timeout=timeout)
|
||||
return Packet(
|
||||
id=response.id,
|
||||
type=packet.type,
|
||||
@ -509,7 +575,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
|
||||
|
||||
@ -26,6 +26,10 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
time_range=arguments.get("time_range"),
|
||||
session_filter=arguments.get("session_filter")
|
||||
)
|
||||
# 记录召回结果中的实体名,供 WebUI 高亮+拉镜头用
|
||||
for e in result.get("entities", []):
|
||||
if e and isinstance(e, dict) and e.get("name"):
|
||||
recorder.record("query", tool_name + "_found", e["name"])
|
||||
return format_recall_result(result)
|
||||
|
||||
elif tool_name == "memory_commit":
|
||||
@ -65,6 +69,13 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
result = graph.cleanup(dry_run=arguments.get("dry_run", True))
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "memory_query_archived":
|
||||
days = arguments.get("days")
|
||||
keyword = arguments.get("keyword", "")
|
||||
recorder.record("query", tool_name, f"days={days}, keyword={keyword}")
|
||||
result = graph.query_archived(days=days, keyword=keyword)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "context_rewrite":
|
||||
result = execute_context_rewrite(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
@ -75,6 +86,11 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
result = execute_persona_update(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_remove":
|
||||
recorder.record("delete", tool_name, arguments.get("attribute", ""))
|
||||
result = execute_persona_remove(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "persona_clear":
|
||||
recorder.record("delete", tool_name, "所有人设")
|
||||
result = execute_persona_clear(graph, arguments)
|
||||
@ -105,6 +121,20 @@ def execute_tool(graph: Any, tool_name: str, arguments: dict) -> str:
|
||||
result = execute_task_link_info(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_archive":
|
||||
recorder.record("update", tool_name, arguments.get("task_id", ""))
|
||||
result = execute_task_archive(graph, arguments)
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
elif tool_name == "task_query":
|
||||
recorder.record("query", tool_name, "")
|
||||
result = execute_task_query(graph, arguments)
|
||||
# 记录查询到的任务描述,供 WebUI 高亮
|
||||
for t in result.get("tasks", []):
|
||||
if t and isinstance(t, dict) and t.get("description"):
|
||||
recorder.record("query", tool_name + "_found", t["description"])
|
||||
return json.dumps(result, ensure_ascii=False, default=str)
|
||||
|
||||
return f"未知工具: {tool_name}"
|
||||
|
||||
except Exception as e:
|
||||
@ -198,40 +228,74 @@ def execute_persona_update(graph: Any, arguments: dict) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def execute_persona_remove(graph: Any, arguments: dict) -> dict:
|
||||
"""删除单条人设属性"""
|
||||
attribute = arguments.get("attribute")
|
||||
if not attribute:
|
||||
return {"status": "error", "message": "请指定要删除的属性名"}
|
||||
|
||||
# 查询当前AI的所有人设关系,找到匹配属性名的
|
||||
recall_result = graph.recall(query_intent="AI,人设,角色", depth=1)
|
||||
found = False
|
||||
deleted_count = 0
|
||||
|
||||
for rel in recall_result.get("relations", []):
|
||||
if rel.get("source") == "AI" and rel.get("type") == attribute:
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": attribute},
|
||||
mode="soft"
|
||||
)
|
||||
deleted_count += result.get("deleted_count", 0)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
# 也许属性名不完全匹配,尝试直接用这个类型删除
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": attribute},
|
||||
mode="soft"
|
||||
)
|
||||
deleted_count = result.get("deleted_count", 0)
|
||||
|
||||
return {
|
||||
"status": "success" if deleted_count > 0 else "not_found",
|
||||
"deleted_attribute": attribute,
|
||||
"deleted_count": deleted_count,
|
||||
"message": f"已删除属性「{attribute}」" if deleted_count > 0 else f"未找到属性「{attribute}」"
|
||||
}
|
||||
|
||||
|
||||
def execute_persona_clear(graph: Any, arguments: dict) -> dict:
|
||||
"""清除人设"""
|
||||
if not arguments.get("confirm", True):
|
||||
return {"status": "cancelled", "message": "需要确认才能清除人设"}
|
||||
|
||||
# 删除所有人设相关关系
|
||||
result1 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "扮演角色"},
|
||||
mode="soft"
|
||||
)
|
||||
result2 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "说话风格"},
|
||||
mode="soft"
|
||||
)
|
||||
result3 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "性格特点"},
|
||||
mode="soft"
|
||||
)
|
||||
result4 = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": "语气特征"},
|
||||
mode="soft"
|
||||
)
|
||||
|
||||
total_deleted = (
|
||||
result1.get("deleted_count", 0) +
|
||||
result2.get("deleted_count", 0) +
|
||||
result3.get("deleted_count", 0) +
|
||||
result4.get("deleted_count", 0)
|
||||
)
|
||||
|
||||
"""清除所有人设"""
|
||||
if not arguments.get("confirm"):
|
||||
return {"status": "cancelled", "message": "请设置 confirm=true 确认清除人设"}
|
||||
|
||||
# 先查询AI的所有人设关系
|
||||
recall_result = graph.recall(query_intent="AI,人设,角色", depth=1)
|
||||
|
||||
# 收集所有AI到其他实体的关系类型
|
||||
relation_types = set()
|
||||
for rel in recall_result.get("relations", []):
|
||||
if rel.get("source") == "AI" and rel.get("type"):
|
||||
relation_types.add(rel.get("type"))
|
||||
|
||||
total_deleted = 0
|
||||
deleted_types = []
|
||||
|
||||
for rtype in relation_types:
|
||||
result = graph.purge(
|
||||
criteria={"subject_contains": "AI", "relation_type": rtype},
|
||||
mode="soft"
|
||||
)
|
||||
count = result.get("deleted_count", 0)
|
||||
if count > 0:
|
||||
total_deleted += count
|
||||
deleted_types.append(rtype)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"deleted_count": total_deleted,
|
||||
"message": "人设已清除,恢复默认身份"
|
||||
"deleted_types": deleted_types,
|
||||
"message": f"人设已清除,恢复默认身份(删除了 {len(deleted_types)} 类属性)"
|
||||
}
|
||||
|
||||
|
||||
@ -352,3 +416,53 @@ def execute_task_link_info(graph: Any, arguments: dict) -> dict:
|
||||
"linked_nodes": info_node_names,
|
||||
"details": result
|
||||
}
|
||||
|
||||
def execute_task_archive(graph: Any, arguments: dict) -> dict:
|
||||
"""归档任务"""
|
||||
task_id = arguments.get("task_id")
|
||||
summary = arguments.get("summary", "")
|
||||
|
||||
if not task_id:
|
||||
return {"status": "error", "message": "请指定要归档的任务ID"}
|
||||
|
||||
# 1. 设置任务状态为 archived
|
||||
triplets_state = [
|
||||
{"subject": task_id, "relation": "HAS_STATE", "object": "State_归档"}
|
||||
]
|
||||
graph.commit(triplets=triplets_state)
|
||||
|
||||
# 2. 如果有摘要,写入完成记录
|
||||
if summary:
|
||||
summary_triplets = [
|
||||
{"subject": task_id, "relation": "归档摘要", "object": summary}
|
||||
]
|
||||
graph.commit(triplets=summary_triplets)
|
||||
|
||||
# 3. 尝试更新 description 标记为已归档
|
||||
archive_triplet = [
|
||||
{"subject": task_id, "relation": "has_description", "object": f"[已归档] {summary or '任务已完成'}"}
|
||||
]
|
||||
graph.commit(triplets=archive_triplet)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"task_id": task_id,
|
||||
"archived": True,
|
||||
"summary": summary or "无摘要",
|
||||
"message": f"任务「{task_id}」已归档" + (f",摘要:{summary}" if summary else "")
|
||||
}
|
||||
|
||||
|
||||
def execute_task_query(graph: Any, arguments: dict) -> dict:
|
||||
"""查询最近的任务列表"""
|
||||
limit = arguments.get("limit", 10)
|
||||
state_filter = arguments.get("state_filter")
|
||||
|
||||
result = graph.get_recent_tasks(limit=limit, state_filter=state_filter)
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"tasks": result["tasks"],
|
||||
"total": result["total"],
|
||||
"message": f"找到 {result['total']} 个任务"
|
||||
}
|
||||
|
||||
@ -7,11 +7,15 @@ from dataclasses import dataclass
|
||||
|
||||
@dataclass
|
||||
class ToolLimits:
|
||||
"""工具调用限制配置"""
|
||||
"""工具调用限制配置
|
||||
实际值由 server.py 从 config.json 加载后传入,此处默认值仅作安全兜底。
|
||||
如需修改限制,请编辑 ~/.trulymem/config.json。
|
||||
"""
|
||||
persona_update_max: int = 1
|
||||
task_update_max: int = 5
|
||||
memory_query_max: int = 20
|
||||
memory_update_max: int = 10
|
||||
task_update_max: int = 20 # 工作记忆链修改(create/set_state/delete/link_info)
|
||||
task_query_max: int = 30 # 工作记忆链查询(memory_recall 查任务相关)
|
||||
memory_query_max: int = 30
|
||||
memory_update_max: int = 15
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -19,6 +23,7 @@ class ToolCallCount:
|
||||
"""工具调用计数"""
|
||||
persona_update: int = 0
|
||||
task_update: int = 0
|
||||
task_query: int = 0
|
||||
memory_query: int = 0
|
||||
memory_update: int = 0
|
||||
|
||||
@ -37,13 +42,22 @@ class ToolLimiter:
|
||||
category: 'persona', 'task', 'memory'
|
||||
operation: 'query', 'update'
|
||||
"""
|
||||
if tool_name in ('persona_update', 'persona_clear'):
|
||||
if tool_name in ('persona_update', 'persona_remove', 'persona_clear'):
|
||||
return ('persona', 'update')
|
||||
|
||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info'):
|
||||
if tool_name in ('task_create', 'task_set_state', 'task_delete', 'task_link_info', 'task_archive'):
|
||||
return ('task', 'update')
|
||||
|
||||
if tool_name == 'task_query':
|
||||
return ('task', 'query')
|
||||
|
||||
if tool_name == 'memory_recall':
|
||||
# 尝试区分工作记忆链查询 vs 一般记忆查询
|
||||
query = (arguments.get('queryIntent', '') + ' ' + ' '.join(
|
||||
arguments.get('seedEntities', []))).strip().lower()
|
||||
task_keywords = ['task', '任务', '工作记忆', '当前轮', '会话', '过程', '流程']
|
||||
if any(kw in query for kw in task_keywords):
|
||||
return ('task', 'query')
|
||||
return ('memory', 'query')
|
||||
|
||||
if tool_name == 'memory_commit':
|
||||
@ -75,7 +89,10 @@ class ToolLimiter:
|
||||
return (False, f"人设图修改次数已达上限({self.limits.persona_update_max}次)")
|
||||
|
||||
elif category == 'task':
|
||||
if self.counts.task_update >= self.limits.task_update_max:
|
||||
if operation == 'query':
|
||||
if self.counts.task_query >= self.limits.task_query_max:
|
||||
return (False, f"工作记忆链查询次数已达上限({self.limits.task_query_max}次)")
|
||||
elif self.counts.task_update >= self.limits.task_update_max:
|
||||
return (False, f"工作记忆链修改次数已达上限({self.limits.task_update_max}次)")
|
||||
|
||||
elif category == 'memory':
|
||||
@ -96,7 +113,10 @@ class ToolLimiter:
|
||||
self.counts.persona_update += 1
|
||||
|
||||
elif category == 'task':
|
||||
self.counts.task_update += 1
|
||||
if operation == 'query':
|
||||
self.counts.task_query += 1
|
||||
else:
|
||||
self.counts.task_update += 1
|
||||
|
||||
elif category == 'memory':
|
||||
if operation == 'query':
|
||||
@ -108,7 +128,8 @@ class ToolLimiter:
|
||||
"""获取调用统计摘要"""
|
||||
lines = [
|
||||
f"人设图: 修改{self.counts.persona_update}/{self.limits.persona_update_max}次",
|
||||
f"工作记忆链: 修改{self.counts.task_update}/{self.limits.task_update_max}次",
|
||||
f"工作记忆链: 查询{self.counts.task_query}/{self.limits.task_query_max}次, "
|
||||
f"修改{self.counts.task_update}/{self.limits.task_update_max}次",
|
||||
f"一般记忆: 查询{self.counts.memory_query}/{self.limits.memory_query_max}次, "
|
||||
f"修改{self.counts.memory_update}/{self.limits.memory_update_max}次"
|
||||
]
|
||||
|
||||
@ -103,16 +103,18 @@ MEMORY_TOOLS = [
|
||||
"subject": {"type": "string"},
|
||||
"relation": {"type": "string"},
|
||||
"object": {"type": "string"},
|
||||
"confidence": {"type": "number"}
|
||||
"confidence": {"type": "number"},
|
||||
"subject_type": {"type": "string", "description": "主体的实体类型,如 Person、Project"},
|
||||
"object_type": {"type": "string", "description": "客体的实体类型,如 Language、Technology"}
|
||||
},
|
||||
"required": ["subject", "relation", "object"]
|
||||
},
|
||||
"description": "三元组列表"
|
||||
},
|
||||
"entity_types": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "实体类型(可选)"
|
||||
"type": "object",
|
||||
"additionalProperties": {"type": "string"},
|
||||
"description": "实体类型字典,如 {\"用户\": \"Person\", \"项目A\": \"Project\"}(可选)"
|
||||
},
|
||||
"temporal_tag": {
|
||||
"type": "string",
|
||||
@ -146,6 +148,12 @@ MEMORY_TOOLS = [
|
||||
4. 删除旧记忆:
|
||||
{"criteria": {"time_before": "2024-01-01"}, "mode": "soft"}
|
||||
|
||||
5. 删除残留在已归档任务上的状态关系:
|
||||
{"criteria": {"relation_type": "HAS_STATE", "source_type": "TaskNode", "source_has_status": "archived"}, "mode": "soft"}
|
||||
|
||||
6. 删除特定类型的节点关系:
|
||||
{"criteria": {"relation_type": "某种关系", "target_type": "某种类型"}, "mode": "soft"}
|
||||
|
||||
【重要】删除原则:
|
||||
- 优先使用 supersede 模式修正错误
|
||||
- 软删除不会物理删除数据
|
||||
@ -159,6 +167,9 @@ MEMORY_TOOLS = [
|
||||
"subject_contains": {"type": "string"},
|
||||
"relation_type": {"type": "string"},
|
||||
"target_contains": {"type": "string"},
|
||||
"source_type": {"type": "string", "description": "源实体类型过滤(如 TaskNode)"},
|
||||
"target_type": {"type": "string", "description": "目标实体类型过滤"},
|
||||
"source_has_status": {"type": "string", "description": "源实体状态过滤(如 archived)"},
|
||||
"time_before": {"type": "string"},
|
||||
"session_id": {"type": "string"}
|
||||
},
|
||||
@ -235,6 +246,52 @@ MEMORY_TOOLS = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "memory_query_archived",
|
||||
"description": """查询已归档的记忆。
|
||||
|
||||
【使用场景】
|
||||
- 想了解之前归档过哪些记忆
|
||||
- 按关键词搜索归档内容
|
||||
- 按时间范围查看最近归档的历史
|
||||
|
||||
【注意】
|
||||
- 只返回 status=archived 的原始关系记录,不包含活跃的「归档摘要」
|
||||
- days 和 keyword 可以单独使用,也可以组合使用
|
||||
- 不加任何参数时返回所有归档记录
|
||||
|
||||
【示例】
|
||||
```
|
||||
# 不传参数:查全部归档
|
||||
memory_query_archived({})
|
||||
|
||||
# 最近7天
|
||||
memory_query_archived({"days": 7})
|
||||
|
||||
# 关键词过滤
|
||||
memory_query_archived({"keyword": "任务"})
|
||||
|
||||
# 组合使用
|
||||
memory_query_archived({"days": 30, "keyword": "配置"})
|
||||
```
|
||||
""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"days": {
|
||||
"type": "integer",
|
||||
"description": "最近N天内的归档记录,不指定则不限时间"
|
||||
},
|
||||
"keyword": {
|
||||
"type": "string",
|
||||
"description": "关键词,匹配实体名或关系类型"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
@ -242,11 +299,16 @@ MEMORY_TOOLS = [
|
||||
"description": """压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。
|
||||
|
||||
【使用场景】
|
||||
- 已执行多次工具调用,JSON细节已理解,不再需要原始格式
|
||||
- 本轮已执行 ≥5 次查询类工具调用,JSON细节已理解,不再需要原始格式
|
||||
- 但需保留"我调用了什么工具、得到了什么结论"的元认知
|
||||
- 继续携带原始JSON会干扰后续推理
|
||||
|
||||
【⚠️ 强制格式要求】
|
||||
【⚠️ 强制要求】
|
||||
**context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!**
|
||||
- 正确方式:先调其他所有工具 → 收到工具结果 → 单独调 context_rewrite
|
||||
- 错误方式:和其他工具一起调(会破坏对话历史结构)
|
||||
|
||||
【格式要求】
|
||||
1. 必须标注调用了哪些工具
|
||||
2. 必须标注是对几次工具调用的总结
|
||||
3. 必须保留关键语义信息
|
||||
@ -259,7 +321,7 @@ MEMORY_TOOLS = [
|
||||
【注意事项】
|
||||
- 不可删除用户原始消息
|
||||
- 不可歪曲工具返回的关键事实
|
||||
- 仅在工具调用 ≥ 2 次后使用""",
|
||||
- 仅在调用 ≥5 次查询类工具后使用""",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@ -332,21 +394,37 @@ PERSONA_TOOLS = [
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_remove",
|
||||
"description": "删除单条人设属性。删除指定的属性(如说话风格、扮演角色等),保留其他人设不变。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"attribute": {
|
||||
"type": "string",
|
||||
"description": "要删除的属性名(如:扮演角色、说话风格、性格特点、口头禅)"
|
||||
}
|
||||
},
|
||||
"required": ["attribute"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "persona_clear",
|
||||
"description": "清除人设。删除AI的角色设定,恢复默认身份。",
|
||||
"description": "清除人设。删除AI所有角色设定,恢复默认身份。注意:此操作不可逆。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"confirm": {
|
||||
"type": "boolean",
|
||||
"description": "确认清除",
|
||||
"default": True
|
||||
"description": "确认清除全部人设"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
"required": ["confirm"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -386,7 +464,9 @@ WORKING_MEMORY_TOOLS = [
|
||||
- 任务节点通过 NEXT_TASK 边形成时间链
|
||||
- 任务节点通过 HAS_STATE 边指向状态节点
|
||||
- 任务节点通过 CONTAINS_INFO 边指向信息节点
|
||||
- info_nodes 参数用于关联具体信息节点
|
||||
- **info_nodes 只能包含该任务专属的具体信息节点**(如"成语接龙_当前成语"),**严禁关联"用户"、"AI"、"系统"等全局通用实体**——这些实体不应通过任务中转
|
||||
- 全局实体的信息直接用独立关系记录(如 用户--[特质]-->求知欲旺盛),不需要通过 Task 中转
|
||||
- info_nodes 参数用于关联任务专属信息节点
|
||||
|
||||
【完整流程示例】
|
||||
用户: "咱来玩成语接龙吧,我先开始,为所欲为"
|
||||
@ -416,7 +496,7 @@ AI操作步骤:
|
||||
"info_nodes": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "关联的信息节点名称(可选)"
|
||||
"description": "关联的信息节点名称(可选)。⚠️ 只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体"
|
||||
}
|
||||
},
|
||||
"required": ["task_id", "description"]
|
||||
@ -550,6 +630,47 @@ AI操作步骤:
|
||||
"required": ["task_id", "info_node_names"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_archive",
|
||||
"description": "归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"task_id": {
|
||||
"type": "string",
|
||||
"description": "要归档的任务ID"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "归档摘要,简述完成了什么或为什么归档。如果不填则自动生成。"
|
||||
}
|
||||
},
|
||||
"required": ["task_id"]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "task_query",
|
||||
"description": "查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "返回的任务数量,默认10"
|
||||
},
|
||||
"state_filter": {
|
||||
"type": "string",
|
||||
"description": "按状态筛选:进行中、已完成、已暂停、已取消、archived"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@ -20,7 +20,63 @@ from core.activity_recorder import get_recorder
|
||||
from core.embedded_db import EmbeddedGraphDB
|
||||
|
||||
|
||||
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
||||
LOGIN_MAX_ATTEMPTS = 5 # 最大尝试次数
|
||||
LOGIN_WAIT_MINUTES = 5 # 超过次数后等待分钟数
|
||||
LOGIN_BAN_THRESHOLD = 3 # 超过此轮次后 ban IP
|
||||
LOGIN_BAN_HOURS = 24 # IP ban 时长(小时)
|
||||
|
||||
# 内存记录:{ip: {"attempts": 0, "first_fail": 0, "ban_until": 0, "rounds": 0}}
|
||||
_login_attempts: dict = {}
|
||||
|
||||
def _check_login_limit(ip: str) -> dict:
|
||||
"""检查 IP 的登录限制。返回 {"blocked": bool, "reason": str, "wait_seconds": int}"""
|
||||
now = time.time()
|
||||
record = _login_attempts.get(ip)
|
||||
|
||||
if record:
|
||||
# 检查是否在 ban 中
|
||||
if record["ban_until"] > now:
|
||||
remaining = int(record["ban_until"] - now)
|
||||
return {"blocked": True, "reason": f"IP 已被临时封禁,剩余 {remaining//60} 分钟", "wait_seconds": remaining}
|
||||
|
||||
# 检查是否需要等待(连续失败超过阈值)
|
||||
if record["attempts"] >= LOGIN_MAX_ATTEMPTS:
|
||||
wait_end = record["first_fail"] + LOGIN_WAIT_MINUTES * 60
|
||||
if wait_end > now:
|
||||
remaining = int(wait_end - now)
|
||||
return {"blocked": True, "reason": f"登录尝试过多,请等待 {remaining} 秒后再试", "wait_seconds": remaining}
|
||||
else:
|
||||
# 等待时间已过,重置计数但记录轮次
|
||||
record["rounds"] += 1
|
||||
record["attempts"] = 0
|
||||
record["first_fail"] = 0
|
||||
|
||||
# 如果轮次超过阈值则 ban IP
|
||||
if record["rounds"] >= LOGIN_BAN_THRESHOLD:
|
||||
record["ban_until"] = now + LOGIN_BAN_HOURS * 3600
|
||||
record["rounds"] = 0
|
||||
return {"blocked": True, "reason": f"多次登录失败,IP 已被封禁 {LOGIN_BAN_HOURS} 小时", "wait_seconds": LOGIN_BAN_HOURS * 3600}
|
||||
|
||||
return {"blocked": False, "reason": "", "wait_seconds": 0}
|
||||
|
||||
def _record_login_fail(ip: str):
|
||||
"""记录一次登录失败"""
|
||||
now = time.time()
|
||||
record = _login_attempts.get(ip)
|
||||
if not record:
|
||||
_login_attempts[ip] = {"attempts": 1, "first_fail": now, "ban_until": 0, "rounds": 0}
|
||||
else:
|
||||
if record["first_fail"] == 0:
|
||||
record["first_fail"] = now
|
||||
record["attempts"] += 1
|
||||
|
||||
def _record_login_success(ip: str):
|
||||
"""登录成功后清除该 IP 的记录"""
|
||||
_login_attempts.pop(ip, None)
|
||||
|
||||
# 定期清理过期记录(防止内存泄漏)
|
||||
_cleanup_interval = 3600 # 1小时
|
||||
_last_cleanup = time.time()
|
||||
def load_secret_key():
|
||||
import json
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json')
|
||||
@ -34,10 +90,15 @@ 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)
|
||||
# 显式配置 session cookie,确保跨场景兼容
|
||||
app.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
|
||||
app.config['SESSION_COOKIE_SECURE'] = False # HTTP 环境不强制 Secure
|
||||
app.config['SESSION_COOKIE_NAME'] = 'trulymem_session'
|
||||
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
|
||||
|
||||
|
||||
@ -90,6 +151,12 @@ def graph_html():
|
||||
return app.send_static_file('graph.html')
|
||||
|
||||
|
||||
@app.route('/static/<path:filename>')
|
||||
def static_files(filename):
|
||||
"""提供静态文件访问"""
|
||||
return app.send_static_file(filename)
|
||||
|
||||
|
||||
@app.route('/chat')
|
||||
@login_required
|
||||
def chat():
|
||||
@ -147,7 +214,10 @@ def login_page():
|
||||
"""登录页面 - 如果没有用户则重定向到设置页"""
|
||||
# 如果没有用户,重定向到首次设置页
|
||||
users_count = 0
|
||||
if graph_db:
|
||||
g_db = get_global_db()
|
||||
if g_db:
|
||||
users_count = g_db.get_web_users_count()
|
||||
elif graph_db:
|
||||
users_count = graph_db.get_web_users_count()
|
||||
if users_count == 0:
|
||||
return redirect('/setup')
|
||||
@ -158,7 +228,10 @@ def login_page():
|
||||
def setup_page():
|
||||
"""首次设置页面 - 如果已有用户则跳转到登录页"""
|
||||
has_users = False
|
||||
if graph_db:
|
||||
g_db = get_global_db()
|
||||
if g_db:
|
||||
has_users = g_db.get_web_users_count() > 0
|
||||
elif graph_db and graph_db is not g_db:
|
||||
has_users = graph_db.get_web_users_count() > 0
|
||||
if has_users:
|
||||
return redirect('/login')
|
||||
@ -179,21 +252,39 @@ def api_login():
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
# DEBUG: 记录收到的凭据
|
||||
import logging
|
||||
logging.warning(f"[DEBUG api_login] username={username}, password_length={len(password)}, password_first_char={password[:1] if password else 'EMPTY'}")
|
||||
|
||||
# 登录限流检查
|
||||
ip = request.remote_addr
|
||||
limit_check = _check_login_limit(ip)
|
||||
if limit_check["blocked"]:
|
||||
logging.warning(f"[DEBUG api_login] IP blocked: {ip}, reason: {limit_check['reason']}")
|
||||
return jsonify({"success": False, "error": limit_check["reason"]})
|
||||
|
||||
# 从全局数据库验证
|
||||
g_db = get_global_db()
|
||||
if g_db and g_db.verify_web_user(username, password):
|
||||
session['authenticated'] = True
|
||||
session['username'] = username # 存储用户名
|
||||
session.permanent = True
|
||||
|
||||
# 重新加载服务器使用该用户的数据库
|
||||
reload_server_for_user(username)
|
||||
|
||||
return jsonify({"success": True})
|
||||
if g_db:
|
||||
logging.warning(f"[DEBUG api_login] global_db path: {g_db.db_path if hasattr(g_db, 'db_path') else 'unknown'}")
|
||||
is_valid = g_db.verify_web_user(username, password)
|
||||
logging.warning(f"[DEBUG api_login] verify result: {is_valid}")
|
||||
if is_valid:
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
reload_server_for_user(username)
|
||||
logging.warning(f"[DEBUG api_login] Login SUCCESS for {username}")
|
||||
return jsonify({"success": True})
|
||||
else:
|
||||
# 登录失败,记录失败
|
||||
_record_login_fail(ip)
|
||||
logging.warning(f"[DEBUG api_login] Login FAIL for {username}, wrong password")
|
||||
return jsonify({"success": False, "error": "用户名或密码错误"})
|
||||
else:
|
||||
logging.warning(f"[DEBUG api_login] global_db is None!")
|
||||
return jsonify({"success": False, "error": "数据库未初始化"})
|
||||
|
||||
return jsonify({"success": False, "error": "用户名或密码错误"})
|
||||
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
def api_logout():
|
||||
"""登出接口"""
|
||||
@ -241,11 +332,55 @@ def web_check():
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/web-users', methods=['GET'])
|
||||
@api_login_required
|
||||
def web_users():
|
||||
"""获取 web_users 列表"""
|
||||
if graph_db:
|
||||
users = graph_db.get_web_users()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"users": [
|
||||
{
|
||||
"username": u['username'],
|
||||
"role": u.get('role', 'user'),
|
||||
"is_admin": u.get('role') == 'admin',
|
||||
"created_at": u.get('created_at')
|
||||
}
|
||||
for u in users
|
||||
]
|
||||
})
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
|
||||
|
||||
@app.route('/api/web-user/<username>', methods=['GET'])
|
||||
@api_login_required
|
||||
def web_user_detail(username):
|
||||
"""获取单个 web_user 详情"""
|
||||
if not graph_db:
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
user = graph_db.get_web_user(username)
|
||||
if not user:
|
||||
return jsonify({"success": False, "error": "用户不存在"}), 404
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"username": user['username'],
|
||||
"role": user.get('role', 'user'),
|
||||
"is_admin": user.get('role') == 'admin',
|
||||
"created_at": user.get('created_at')
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/setup', methods=['POST'])
|
||||
def api_setup():
|
||||
"""首次设置 - 创建初始管理员用户"""
|
||||
# 只有没有任何用户时才允许设置
|
||||
g_db = get_global_db()
|
||||
# Log the count for debugging
|
||||
if g_db:
|
||||
import logging
|
||||
count = g_db.get_web_users_count()
|
||||
logging.warning(f"[api_setup] web_users count: {count}")
|
||||
if g_db and g_db.get_web_users_count() > 0:
|
||||
return jsonify({"success": False, "error": "用户已存在,不允许重复设置"}), 400
|
||||
|
||||
@ -623,7 +758,6 @@ def get_graph():
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 200
|
||||
""")
|
||||
|
||||
nodes = []
|
||||
@ -695,8 +829,8 @@ def get_graph_highlight():
|
||||
if row:
|
||||
highlight_ids.append(row['id'])
|
||||
|
||||
# 检查是否有新创建的节点
|
||||
if record.get('action') == 'create' and entity_name:
|
||||
# 除删除外,所有操作都拉镜头(create/recall/query/update/archive 等)
|
||||
if record.get('action') != 'delete' and entity_name:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
@ -731,6 +865,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)')
|
||||
@ -8,6 +8,66 @@
|
||||
- TUI & backend separation, multi-threaded communication
|
||||
- **Everything is a graph**, AI reasoning runs entirely in backend
|
||||
|
||||
## Deployment
|
||||
|
||||
### Development (Run directly from Git repo)
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
python3 trulymem_entry.py --web --port 4096
|
||||
```
|
||||
|
||||
### Production (Systemd + standalone directory)
|
||||
|
||||
```bash
|
||||
# Copy code to standalone deployment directory
|
||||
cp -r TrulyMEM-TrueHumanMEM /home/trulymem
|
||||
|
||||
# Create Systemd service
|
||||
cat > /etc/systemd/system/trulymem-web.service << 'EOF'
|
||||
[Unit]
|
||||
Description=TrulyMEM - True Human Memory (Web Mode)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/home/trulymem
|
||||
ExecStart=/usr/bin/python3 /home/trulymem/trulymem_entry.py --web --port 4096
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable trulymem-web.service
|
||||
systemctl start trulymem-web.service
|
||||
|
||||
# Check status
|
||||
systemctl status trulymem-web.service
|
||||
```
|
||||
|
||||
> **Note**: Do not run the service directly from the Git repository to avoid polluting it with runtime artifacts (logs, databases, etc.).
|
||||
|
||||
### Web Access
|
||||
|
||||
The service runs at `http://localhost:4096`. On first visit, you'll need to set up an admin account and log in.
|
||||
|
||||
### Updating Deployment
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git pull
|
||||
cp -r * /home/trulymem/
|
||||
systemctl restart trulymem-web.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
@ -21,22 +81,40 @@ 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
|
||||
├── ui/ # TUI display layer (display only, no AI logic)
|
||||
│ └── prompts/ # Prompt management (PromptManager + system_prompt.md)
|
||||
├── ui/ # TUI display layer + Web frontend
|
||||
│ ├── __init__.py # Export GraphMemoryApp
|
||||
│ ├── app.py # GraphMemoryApp (communicates via BackendClient)
|
||||
│ ├── widgets/ # TUI components
|
||||
│ ├── 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
|
||||
│ │ ├── graph.html # Star map visualization (Three.js)
|
||||
│ │ └── index.html # Web chat interface
|
||||
│ ├── templates/ # Page templates
|
||||
│ │ ├── login.html
|
||||
│ │ ├── setup.html
|
||||
│ │ └── settings.html
|
||||
│ ├── web_config.json # Web service config file
|
||||
│ └── web_config.example.json # Web config template
|
||||
├── tests/ # Test suite
|
||||
│ ├── test_core/ # Core logic tests
|
||||
│ ├── test_ui/ # UI layer tests
|
||||
│ └── test_integration/ # Integration tests
|
||||
├── docs/ # Documentation
|
||||
│ ├── zh/ # Chinese docs
|
||||
│ └── en/ # English docs
|
||||
└── build/ # Build scripts
|
||||
├── build_linux.sh
|
||||
├── build_macos.sh
|
||||
├── build_windows.bat
|
||||
├── build_appimage.sh
|
||||
└── trulymem.spec
|
||||
```
|
||||
|
||||
## Architecture Diagram
|
||||
@ -160,10 +238,13 @@ def main():
|
||||
- `context_rewrite` - Compress single-turn tool call context
|
||||
|
||||
### Persona Tools (2)
|
||||
| `persona_remove` | Delete single persona attribute | Keep other attributes unchanged |
|
||||
- `persona_update` - Update persona
|
||||
- `persona_clear` - Clear persona
|
||||
|
||||
### Task Tools (4)
|
||||
### Task Tools (6)
|
||||
| `task_archive` | Archive completed/expired tasks | Step 6 mandatory, writes completion summary |
|
||||
| `task_query` | Query recent task list | Call first in new conversations to avoid duplicate tasks |
|
||||
- `task_create` - Create task
|
||||
- `task_set_state` - Set state
|
||||
- `task_delete` - Delete task
|
||||
|
||||
@ -53,7 +53,8 @@ The system prompt contains:
|
||||
Step 1: Query persona graph (highest priority)
|
||||
Step 2: Query working memory chain
|
||||
Step 3: Process conversation
|
||||
Step 4: Update working memory chain
|
||||
Step 4: memory_commit (write key info) → Persist user-explicit important information to the graph database
|
||||
Step 5: Update working memory chain
|
||||
```
|
||||
|
||||
### 5. Tool System
|
||||
|
||||
@ -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
|
||||
```
|
||||
|
||||
@ -8,6 +8,72 @@
|
||||
- TUI 与后端分离,多线程通信
|
||||
- **一切皆图**,AI 推理全部在后端
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 开发环境(从 Git 仓库直接运行)
|
||||
|
||||
```bash
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
python3 trulymem_entry.py --web --port 4096
|
||||
```
|
||||
|
||||
### 生产环境(Systemd + 独立部署目录)
|
||||
|
||||
```bash
|
||||
# 将代码复制到独立目录
|
||||
cp -r TrulyMEM-TrueHumanMEM /home/trulymem
|
||||
|
||||
# 创建 Systemd 服务
|
||||
cat > /etc/systemd/system/trulymem-web.service << 'EOF'
|
||||
[Unit]
|
||||
Description=TrulyMEM - True Human Memory (Web Mode)
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/home/trulymem
|
||||
ExecStart=/usr/bin/python3 /home/trulymem/trulymem_entry.py --web --port 4096
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable trulymem-web.service
|
||||
systemctl start trulymem-web.service
|
||||
|
||||
# 查看状态
|
||||
systemctl status trulymem-web.service
|
||||
```
|
||||
|
||||
> **注意**: 不要直接从 Git 仓库启动服务,以免日志文件、数据库等运行时产物污染仓库。
|
||||
|
||||
### Web 访问
|
||||
|
||||
服务默认运行在 `http://localhost:4096`,首次访问需设置管理员账号并登录。
|
||||
|
||||
### 更新部署
|
||||
|
||||
```bash
|
||||
# 拉取最新代码
|
||||
cd TrulyMEM-TrueHumanMEM
|
||||
git pull
|
||||
|
||||
# 同步到部署目录
|
||||
cp -r * /home/trulymem/
|
||||
|
||||
# 重启服务
|
||||
systemctl restart trulymem-web.service
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 原始架构说明
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
@ -21,22 +87,40 @@ 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/ # 提示词管理
|
||||
├── ui/ # TUI 显示层(仅显示,无 AI 逻辑)
|
||||
│ └── prompts/ # 提示词管理(PromptManager + system_prompt.md)
|
||||
├── ui/ # TUI 显示层 + Web 前端
|
||||
│ ├── __init__.py # 导出 GraphMemoryApp
|
||||
│ ├── app.py # GraphMemoryApp (通过 BackendClient 通信)
|
||||
│ ├── widgets/ # TUI 组件
|
||||
│ ├── 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 # 星图可视化(Three.js)
|
||||
│ │ └── index.html # Web 聊天界面
|
||||
│ ├── templates/ # 页面模板
|
||||
│ │ ├── login.html
|
||||
│ │ ├── setup.html
|
||||
│ │ └── settings.html
|
||||
│ ├── web_config.json # Web 服务配置文件
|
||||
│ └── web_config.example.json # Web 配置模板
|
||||
├── tests/ # 测试套件
|
||||
│ ├── test_core/ # 核心逻辑测试
|
||||
│ ├── test_ui/ # UI 层测试
|
||||
│ └── test_integration/ # 集成测试
|
||||
├── docs/ # 文档
|
||||
│ ├── zh/ # 中文文档
|
||||
│ └── en/ # 英文文档
|
||||
└── build/ # 打包脚本
|
||||
├── build_linux.sh
|
||||
├── build_macos.sh
|
||||
├── build_windows.bat
|
||||
├── build_appimage.sh
|
||||
└── trulymem.spec
|
||||
```
|
||||
|
||||
## 架构图
|
||||
@ -160,10 +244,13 @@ def main():
|
||||
- `context_rewrite` - 压缩单轮工具调用上下文
|
||||
|
||||
### 人设工具 (2个)
|
||||
| `persona_remove` | 删除单条人设属性 | 保留其他人设不变 |
|
||||
- `persona_update` - 更新人设
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具 (4个)
|
||||
### 任务工具 (6个)
|
||||
| `task_archive` | 归档已完成/过期的任务 | 步骤6强制执行,写入完成摘要 |
|
||||
| `task_query` | 查询最近任务列表 | 新对话时优先调用,避免重复创建任务 |
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
@ -176,9 +263,10 @@ def main():
|
||||
| 类别 | 操作 | 每轮上限 |
|
||||
|------|------|---------|
|
||||
| 人设图 | 修改 | 1 次 |
|
||||
| 工作记忆链 | 修改 | 5 次 |
|
||||
| 一般记忆 | 查询 | 20 次 |
|
||||
| 一般记忆 | 修改 | 10 次 |
|
||||
| 工作记忆链 | 查询 | 30 次 |
|
||||
| 工作记忆链 | 修改 | 20 次 |
|
||||
| 一般记忆 | 查询 | 30 次 |
|
||||
| 一般记忆 | 修改 | 15 次 |
|
||||
|
||||
> 注:`memory_recall` 统一计入一般记忆查询,不再区分人设/工作记忆查询。
|
||||
|
||||
|
||||
@ -53,9 +53,13 @@ system_prompt = prompt_manager.get_system_prompt()
|
||||
步骤 1: 查询人设图(最高优先级)
|
||||
步骤 2: 查询工作记忆链
|
||||
步骤 3: 处理对话
|
||||
步骤 4: 更新工作记忆链
|
||||
步骤 4: memory_commit (写入关键信息) → 将用户明确提到的重要信息写入图数据库
|
||||
步骤 5: 更新工作记忆链
|
||||
```
|
||||
|
||||
> **注意**: 原提示词仅包含 4 步,缺少显式的写入步骤,导致 AI 只查不写、聊完即忘。
|
||||
> 步骤 4 确保每轮对话的关键信息被持久化到图数据库中。
|
||||
|
||||
### 5. 工具系统
|
||||
|
||||
#### 记忆工具
|
||||
|
||||
@ -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,30 @@ python web_api.py --port 4096
|
||||
| 按键 | 功能 |
|
||||
|------|------|
|
||||
| F1 | 帮助 |
|
||||
| F2 | 切换侧边栏 |
|
||||
| F2 | 切换侧边栏(配置面板) |
|
||||
| F3 | 工具详情 |
|
||||
| F5 | 清屏 |
|
||||
| F6 | 退出 |
|
||||
|
||||
## 数据存储
|
||||
---
|
||||
|
||||
### 源码运行模式
|
||||
## 打包构建
|
||||
|
||||
| 数据 | 位置 |
|
||||
|------|------|
|
||||
| 图数据库 | 项目目录 `graph_memory.db` |
|
||||
| 配置文件 | 项目目录 `config.json`(如存在) |
|
||||
| 数据库格式 | SQLite |
|
||||
```bash
|
||||
# 安装依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
### 打包运行模式
|
||||
# 构建(Linux / macOS)
|
||||
pyinstaller --clean build/trulymem.spec
|
||||
```
|
||||
|
||||
| 数据 | 位置 |
|
||||
|------|------|
|
||||
| 图数据库 | `~/.trulymem/graph_memory.db` |
|
||||
| 配置文件 | `~/.trulymem/config.json` |
|
||||
| 数据库格式 | SQLite |
|
||||
构建产出:`dist/TrulyMEM`(单文件,TUI + Web 服务均内嵌于同一二进制)
|
||||
|
||||
> **说明**:后端统一管理配置。前端仅负责消息展示,配置修改通过后端持久化到文件系统。
|
||||
> 📦 从 v2 开始,Web 服务作为线程嵌入主程序,不再需要独立打包 `trulymem-web`。
|
||||
>
|
||||
> 💡 修改 `ui/static/` 或 `core/` 等源码后必须重新编译才能生效(静态文件在构建时打入二进制)。
|
||||
|
||||
---
|
||||
|
||||
## 架构说明
|
||||
|
||||
@ -97,19 +138,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 +179,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
|
||||
```
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "chat",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM chat feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/chat",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "graph",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM graph feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/graph",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"app": {
|
||||
"bundleName": "com.trulymem.app",
|
||||
"debug": true,
|
||||
"versionCode": 1000001,
|
||||
"versionName": "1.0.0",
|
||||
"minAPIVersion": 60100023,
|
||||
"targetAPIVersion": 60100023,
|
||||
"apiReleaseType": "Release",
|
||||
"targetMinorAPIVersion": 0,
|
||||
"targetPatchAPIVersion": 0,
|
||||
"compileSdkVersion": "6.1.0.105",
|
||||
"compileSdkType": "HarmonyOS",
|
||||
"appEnvironments": [],
|
||||
"bundleType": "app",
|
||||
"buildMode": "debug"
|
||||
},
|
||||
"module": {
|
||||
"name": "settings",
|
||||
"type": "har",
|
||||
"description": "TrulyMEM settings feature module",
|
||||
"deviceTypes": [
|
||||
"phone",
|
||||
"tablet",
|
||||
"2in1"
|
||||
],
|
||||
"packageName": "@ohos/settings",
|
||||
"installationFree": false
|
||||
}
|
||||
}
|
||||
4529
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
4529
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.ts_checker_cache
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo
vendored
Normal file
File diff suppressed because one or more lines are too long
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo.linter
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/.tsbuildinfo.linter
vendored
Normal file
File diff suppressed because one or more lines are too long
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/compileInfo.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"buildMode":"Debug"}
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.protoBin
vendored
Normal file
Binary file not shown.
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export { ChatPage } from "@normalized:N&&&@ohos/chat/src/main/ets/pages/ChatPage&1.0.0";
|
||||
Binary file not shown.
@ -0,0 +1,488 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ChatMessageList_Params {
|
||||
messages?: ChatMessage[];
|
||||
isThinking?: boolean;
|
||||
toolCallLog?: string;
|
||||
scrollController?: Scroller;
|
||||
}
|
||||
interface ChatInputBar_Params {
|
||||
inputText?: string;
|
||||
isThinking?: boolean;
|
||||
onSend?: () => void;
|
||||
}
|
||||
interface ToolCallLogPanel_Params {
|
||||
logText?: string;
|
||||
}
|
||||
interface ThinkingIndicator_Params {
|
||||
}
|
||||
interface ChatMessageBubble_Params {
|
||||
msg?: ChatMessage;
|
||||
}
|
||||
import type { ChatMessage } from '@ohos/common';
|
||||
export class ChatMessageBubble extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__msg = new SynchedPropertyNesedObjectPU(params.msg, this, "msg");
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatMessageBubble_Params) {
|
||||
this.__msg.set(params.msg);
|
||||
}
|
||||
updateStateVars(params: ChatMessageBubble_Params) {
|
||||
this.__msg.set(params.msg);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__msg.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__msg.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __msg: SynchedPropertyNesedObjectPU<ChatMessage>;
|
||||
get msg() {
|
||||
return this.__msg.get();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.padding(12);
|
||||
Column.backgroundColor(this.msg.role === 'user' ? 'rgba(124,77,255,0.15)' : 'rgba(245,245,245,0.1)');
|
||||
Column.borderRadius(12);
|
||||
Column.border({
|
||||
width: 1,
|
||||
color: this.msg.role === 'user' ? 'rgba(124,77,255,0.3)' : 'rgba(255,255,255,0.1)'
|
||||
});
|
||||
Column.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Column.margin({ left: 8, right: 8, bottom: 8 });
|
||||
Column.width('100%');
|
||||
Column.alignItems(HorizontalAlign.Start);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
// 角色标识
|
||||
Text.create(this.msg.role === 'user' ? '🧑 你' : '🤖 AI');
|
||||
// 角色标识
|
||||
Text.fontSize(11);
|
||||
// 角色标识
|
||||
Text.fontColor(this.msg.role === 'user' ? '#7C4DFF' : '#999');
|
||||
// 角色标识
|
||||
Text.width('100%');
|
||||
}, Text);
|
||||
// 角色标识
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
// 消息内容
|
||||
Text.create(this.msg.content);
|
||||
// 消息内容
|
||||
Text.fontSize(15);
|
||||
// 消息内容
|
||||
Text.width('100%');
|
||||
// 消息内容
|
||||
Text.margin({ top: 4 });
|
||||
// 消息内容
|
||||
Text.fontColor('#FFFFFF');
|
||||
}, Text);
|
||||
// 消息内容
|
||||
Text.pop();
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ThinkingIndicator extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ThinkingIndicator_Params) {
|
||||
}
|
||||
updateStateVars(params: ThinkingIndicator_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Row.create();
|
||||
Row.padding(12);
|
||||
Row.backgroundColor('rgba(124,77,255,0.1)');
|
||||
Row.borderRadius(12);
|
||||
Row.border({ width: 1, color: 'rgba(124,77,255,0.2)' });
|
||||
Row.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Row.margin({ left: 8, right: 8, bottom: 8 });
|
||||
}, Row);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
LoadingProgress.create();
|
||||
LoadingProgress.width(20);
|
||||
LoadingProgress.height(20);
|
||||
LoadingProgress.margin({ right: 8 });
|
||||
LoadingProgress.color('#7C4DFF');
|
||||
}, LoadingProgress);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('AI 思考中...');
|
||||
Text.fontSize(13);
|
||||
Text.fontColor('#7C4DFF');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
Row.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ToolCallLogPanel extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__logText = new SynchedPropertySimpleOneWayPU(params.logText, this, "logText");
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ToolCallLogPanel_Params) {
|
||||
}
|
||||
updateStateVars(params: ToolCallLogPanel_Params) {
|
||||
this.__logText.reset(params.logText);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__logText.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__logText.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __logText: SynchedPropertySimpleOneWayPU<string>;
|
||||
get logText() {
|
||||
return this.__logText.get();
|
||||
}
|
||||
set logText(newValue: string) {
|
||||
this.__logText.set(newValue);
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(this.logText);
|
||||
Text.fontSize(10);
|
||||
Text.fontColor('#FF9800');
|
||||
Text.backgroundColor('rgba(255,152,0,0.1)');
|
||||
Text.padding(8);
|
||||
Text.borderRadius(8);
|
||||
Text.border({ width: 1, color: 'rgba(255,152,0,0.2)' });
|
||||
Text.backgroundBlurStyle(BlurStyle.Thin);
|
||||
Text.margin({ left: 8, right: 8, bottom: 4 });
|
||||
Text.lineHeight(16);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ChatInputBar extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__inputText = new SynchedPropertySimpleTwoWayPU(params.inputText, this, "inputText");
|
||||
this.__isThinking = new SynchedPropertySimpleOneWayPU(params.isThinking, this, "isThinking");
|
||||
this.onSend = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatInputBar_Params) {
|
||||
if (params.onSend !== undefined) {
|
||||
this.onSend = params.onSend;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatInputBar_Params) {
|
||||
this.__isThinking.reset(params.isThinking);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__inputText.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__inputText.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __inputText: SynchedPropertySimpleTwoWayPU<string>;
|
||||
get inputText() {
|
||||
return this.__inputText.get();
|
||||
}
|
||||
set inputText(newValue: string) {
|
||||
this.__inputText.set(newValue);
|
||||
}
|
||||
private __isThinking: SynchedPropertySimpleOneWayPU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private onSend?: () => void;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Row.create();
|
||||
Row.width('100%');
|
||||
Row.padding(8);
|
||||
Row.backgroundColor('rgba(255,255,255,0.05)');
|
||||
Row.backgroundBlurStyle(BlurStyle.Regular);
|
||||
Row.border({
|
||||
width: 1,
|
||||
color: 'rgba(124,77,255,0.2)',
|
||||
style: BorderStyle.Solid
|
||||
});
|
||||
}, Row);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TextArea.create({ text: this.inputText, placeholder: '输入消息...' });
|
||||
TextArea.layoutWeight(1);
|
||||
TextArea.onChange((v: string) => { this.inputText = v; });
|
||||
TextArea.height(40);
|
||||
TextArea.backgroundColor('rgba(255,255,255,0.1)');
|
||||
TextArea.borderRadius(8);
|
||||
TextArea.border({ width: 1, color: 'rgba(124,77,255,0.3)' });
|
||||
}, TextArea);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Button.createWithLabel('发送');
|
||||
Button.enabled(!this.isThinking);
|
||||
Button.onClick(() => { this.onSend?.(); });
|
||||
Button.backgroundColor('#7C4DFF');
|
||||
Button.borderRadius(8);
|
||||
}, Button);
|
||||
Button.pop();
|
||||
Row.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class ChatMessageList extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__messages = new SynchedPropertyObjectOneWayPU(params.messages, this, "messages");
|
||||
this.__isThinking = new SynchedPropertySimpleOneWayPU(params.isThinking, this, "isThinking");
|
||||
this.__toolCallLog = new SynchedPropertySimpleOneWayPU(params.toolCallLog, this, "toolCallLog");
|
||||
this.scrollController = new Scroller();
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatMessageList_Params) {
|
||||
if (params.scrollController !== undefined) {
|
||||
this.scrollController = params.scrollController;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatMessageList_Params) {
|
||||
this.__messages.reset(params.messages);
|
||||
this.__isThinking.reset(params.isThinking);
|
||||
this.__toolCallLog.reset(params.toolCallLog);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__messages.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__toolCallLog.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__messages.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
this.__toolCallLog.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __messages: SynchedPropertySimpleOneWayPU<ChatMessage[]>;
|
||||
get messages() {
|
||||
return this.__messages.get();
|
||||
}
|
||||
set messages(newValue: ChatMessage[]) {
|
||||
this.__messages.set(newValue);
|
||||
}
|
||||
private __isThinking: SynchedPropertySimpleOneWayPU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private __toolCallLog: SynchedPropertySimpleOneWayPU<string>;
|
||||
get toolCallLog() {
|
||||
return this.__toolCallLog.get();
|
||||
}
|
||||
set toolCallLog(newValue: string) {
|
||||
this.__toolCallLog.set(newValue);
|
||||
}
|
||||
private scrollController: Scroller;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
List.create();
|
||||
List.width('100%');
|
||||
List.layoutWeight(1);
|
||||
List.backgroundColor('rgba(0,0,0,0.1)');
|
||||
}, List);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
ForEach.create();
|
||||
const forEachItemGenFunction = _item => {
|
||||
const msg = _item;
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatMessageBubble(this, { msg: msg }, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 141, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
msg: msg
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
msg: msg
|
||||
});
|
||||
}
|
||||
}, { name: "ChatMessageBubble" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
};
|
||||
this.forEachUpdateFunction(elmtId, this.messages, forEachItemGenFunction);
|
||||
}, ForEach);
|
||||
ForEach.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.isThinking) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ThinkingIndicator(this, {}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 147, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {});
|
||||
}
|
||||
}, { name: "ThinkingIndicator" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.toolCallLog && !this.isThinking) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ToolCallLogPanel(this, { logText: this.toolCallLog }, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/components/ChatComponents.ets", line: 153, col: 11 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
logText: this.toolCallLog
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
logText: this.toolCallLog
|
||||
});
|
||||
}
|
||||
}, { name: "ToolCallLogPanel" });
|
||||
}
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
List.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,219 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ChatPage_Params {
|
||||
messages?: ChatMessage[];
|
||||
inputText?: string;
|
||||
db?: GraphDatabase;
|
||||
toolCallLog?: string;
|
||||
isThinking?: boolean;
|
||||
agentService?: AIAgentService;
|
||||
}
|
||||
import { GraphMemoryService, AIAgentService, Logger } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import type { GraphDatabase, ChatMessage, AgentResponse } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import { ChatMessageList, ChatInputBar } from "@normalized:N&&&@ohos/chat/src/main/ets/components/ChatComponents&1.0.0";
|
||||
export class ChatPage extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__messages = new ObservedPropertyObjectPU([], this, "messages");
|
||||
this.__inputText = new ObservedPropertySimplePU('', this, "inputText");
|
||||
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
|
||||
this.__toolCallLog = new ObservedPropertySimplePU('', this, "toolCallLog");
|
||||
this.__isThinking = new ObservedPropertySimplePU(false, this, "isThinking");
|
||||
this.agentService = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ChatPage_Params) {
|
||||
if (params.messages !== undefined) {
|
||||
this.messages = params.messages;
|
||||
}
|
||||
if (params.inputText !== undefined) {
|
||||
this.inputText = params.inputText;
|
||||
}
|
||||
if (params.toolCallLog !== undefined) {
|
||||
this.toolCallLog = params.toolCallLog;
|
||||
}
|
||||
if (params.isThinking !== undefined) {
|
||||
this.isThinking = params.isThinking;
|
||||
}
|
||||
if (params.agentService !== undefined) {
|
||||
this.agentService = params.agentService;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ChatPage_Params) {
|
||||
this.__db.reset(params.db);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__messages.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__inputText.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__db.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__toolCallLog.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__isThinking.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__messages.aboutToBeDeleted();
|
||||
this.__inputText.aboutToBeDeleted();
|
||||
this.__db.aboutToBeDeleted();
|
||||
this.__toolCallLog.aboutToBeDeleted();
|
||||
this.__isThinking.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __messages: ObservedPropertyObjectPU<ChatMessage[]>;
|
||||
get messages() {
|
||||
return this.__messages.get();
|
||||
}
|
||||
set messages(newValue: ChatMessage[]) {
|
||||
this.__messages.set(newValue);
|
||||
}
|
||||
private __inputText: ObservedPropertySimplePU<string>;
|
||||
get inputText() {
|
||||
return this.__inputText.get();
|
||||
}
|
||||
set inputText(newValue: string) {
|
||||
this.__inputText.set(newValue);
|
||||
}
|
||||
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
|
||||
get db() {
|
||||
return this.__db.get();
|
||||
}
|
||||
set db(newValue: GraphDatabase) {
|
||||
this.__db.set(newValue);
|
||||
}
|
||||
private __toolCallLog: ObservedPropertySimplePU<string>;
|
||||
get toolCallLog() {
|
||||
return this.__toolCallLog.get();
|
||||
}
|
||||
set toolCallLog(newValue: string) {
|
||||
this.__toolCallLog.set(newValue);
|
||||
}
|
||||
private __isThinking: ObservedPropertySimplePU<boolean>;
|
||||
get isThinking() {
|
||||
return this.__isThinking.get();
|
||||
}
|
||||
set isThinking(newValue: boolean) {
|
||||
this.__isThinking.set(newValue);
|
||||
}
|
||||
private agentService?: AIAgentService;
|
||||
async aboutToAppear() {
|
||||
// 初始化图记忆服务和 Agent
|
||||
const memoryService = new GraphMemoryService(this.db);
|
||||
this.agentService = new AIAgentService(memoryService, getContext(this));
|
||||
// 加载历史消息(兼容旧数据:无 session_id 时加载全部)
|
||||
const rawHistory = await this.db.getChatHistory(50, this.agentService.getSessionId());
|
||||
if (rawHistory.length === 0) {
|
||||
// 新 session,尝试加载旧消息
|
||||
const legacyHistory = await this.db.getChatHistory(50);
|
||||
this.messages = legacyHistory.map(m => {
|
||||
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.messages = rawHistory.map(m => {
|
||||
const msg: ChatMessage = { role: m.role, content: m.content };
|
||||
return msg;
|
||||
});
|
||||
}
|
||||
}
|
||||
async sendMessage() {
|
||||
if (!this.inputText.trim() || !this.agentService)
|
||||
return;
|
||||
const userMessage: string = this.inputText;
|
||||
this.inputText = '';
|
||||
// 添加用户消息
|
||||
await this.db.saveChatMessage('user', userMessage, '', this.agentService.getSessionId());
|
||||
this.messages = [...this.messages, { role: 'user', content: userMessage }];
|
||||
// 显示 loading
|
||||
this.isThinking = true;
|
||||
this.toolCallLog = '';
|
||||
try {
|
||||
// 通过 Agent 发送消息
|
||||
const agentResponse: AgentResponse = await this.agentService.sendMessage(userMessage);
|
||||
// 记录工具调用日志
|
||||
if (agentResponse.toolCalls.length > 0) {
|
||||
const logs: string[] = agentResponse.toolCalls.map(tc => `🛠 ${tc.name}: ${tc.message}`);
|
||||
this.toolCallLog = logs.join('\n');
|
||||
}
|
||||
// 保存并显示 AI 回复
|
||||
await this.db.saveChatMessage('assistant', agentResponse.content, this.toolCallLog, this.agentService.getSessionId());
|
||||
this.messages = [...this.messages, { role: 'assistant', content: agentResponse.content }];
|
||||
}
|
||||
catch (err) {
|
||||
Logger.error('Agent request failed: ' + JSON.stringify(err));
|
||||
const errMsg = (err as Error).message || JSON.stringify(err);
|
||||
this.messages = [...this.messages, { role: 'assistant', content: `⚠️ 请求失败: ${errMsg}` }];
|
||||
}
|
||||
finally {
|
||||
this.isThinking = false;
|
||||
}
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundColor('rgba(26,27,46,0.95)');
|
||||
}, Column);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatMessageList(this, {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/pages/ChatPage.ets", line: 73, col: 13 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
messages: this.messages,
|
||||
isThinking: this.isThinking,
|
||||
toolCallLog: this.toolCallLog
|
||||
});
|
||||
}
|
||||
}, { name: "ChatMessageList" });
|
||||
}
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new ChatInputBar(this, {
|
||||
inputText: this.__inputText,
|
||||
isThinking: this.isThinking,
|
||||
onSend: (): void => { this.sendMessage(); }
|
||||
}, undefined, elmtId, () => { }, { page: "features/chat/src/main/ets/pages/ChatPage.ets", line: 79, col: 13 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
inputText: this.inputText,
|
||||
isThinking: this.isThinking,
|
||||
onSend: (): void => { this.sendMessage(); }
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
isThinking: this.isThinking
|
||||
});
|
||||
}
|
||||
}, { name: "ChatInputBar" });
|
||||
}
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.protoBin
vendored
Normal file
Binary file not shown.
16
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts
vendored
Normal file
16
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
export { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
export { defaultLogger as Logger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
export { BreakpointType, WidthBreakpoint } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export type { BreakpointTypes } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export { PageContext } from "@normalized:N&&&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0";
|
||||
export type { RouterParam, IPageContext } from "@normalized:N&&&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0";
|
||||
export { Constants as TrulyMEMConstants } from "@normalized:N&&&@ohos/common/src/main/ets/constant/TrulyMEMConstants&1.0.0";
|
||||
export { GraphDatabase } from "@normalized:N&&&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0";
|
||||
export type { RecallEntity, TimeRangeParams } from "@normalized:N&&&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0";
|
||||
export { GraphMemoryService } from "@normalized:N&&&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0";
|
||||
export type { ConnectionItem, NodeDetailInfo } from "@normalized:N&&&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0";
|
||||
export { AIAgentService } from "@normalized:N&&&@ohos/common/src/main/ets/service/AIAgentService&1.0.0";
|
||||
export type { ChatMessage, AgentResponse } from "@normalized:N&&&@ohos/common/src/main/ets/service/AIAgentService&1.0.0";
|
||||
export { BaseViewModel } from "@normalized:N&&&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0";
|
||||
export type { VMEvent } from "@normalized:N&&&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0";
|
||||
export { ImmersiveTabNavigation } from "@normalized:N&&&@ohos/common/src/main/ets/component/ImmersiveTabNavigation&1.0.0";
|
||||
Binary file not shown.
@ -0,0 +1,236 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface ImmersiveTabNavigation_Params {
|
||||
currentIndex?: number;
|
||||
contentBuilder?: () => void;
|
||||
onTabChange?: (index: number) => void;
|
||||
windowFocused?: boolean;
|
||||
bottomAvoidHeight?: number;
|
||||
}
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import window from "@ohos:window";
|
||||
import type { BusinessError } from "@ohos:base";
|
||||
const THEME_COLOR = '#7C4DFF';
|
||||
export class ImmersiveTabNavigation extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__currentIndex = new ObservedPropertySimplePU(0, this, "currentIndex");
|
||||
this.contentBuilder = undefined;
|
||||
this.onTabChange = undefined;
|
||||
this.windowFocused = true;
|
||||
this.bottomAvoidHeight = 0;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: ImmersiveTabNavigation_Params) {
|
||||
if (params.currentIndex !== undefined) {
|
||||
this.currentIndex = params.currentIndex;
|
||||
}
|
||||
if (params.contentBuilder !== undefined) {
|
||||
this.contentBuilder = params.contentBuilder;
|
||||
}
|
||||
if (params.onTabChange !== undefined) {
|
||||
this.onTabChange = params.onTabChange;
|
||||
}
|
||||
if (params.windowFocused !== undefined) {
|
||||
this.windowFocused = params.windowFocused;
|
||||
}
|
||||
if (params.bottomAvoidHeight !== undefined) {
|
||||
this.bottomAvoidHeight = params.bottomAvoidHeight;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: ImmersiveTabNavigation_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__currentIndex.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__currentIndex.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __currentIndex: ObservedPropertySimplePU<number>;
|
||||
get currentIndex() {
|
||||
return this.__currentIndex.get();
|
||||
}
|
||||
set currentIndex(newValue: number) {
|
||||
this.__currentIndex.set(newValue);
|
||||
}
|
||||
private __contentBuilder;
|
||||
private onTabChange?: (index: number) => void;
|
||||
private windowFocused: boolean;
|
||||
private bottomAvoidHeight: number;
|
||||
aboutToAppear() {
|
||||
const mainWindow = AppStorage.get<window.Window>('main_window');
|
||||
if (mainWindow) {
|
||||
try {
|
||||
const avoidArea = mainWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM);
|
||||
this.bottomAvoidHeight = avoidArea.bottomRect.height || 0;
|
||||
}
|
||||
catch (e) {
|
||||
defaultLogger.error('Failed to get avoid area: ' + (e as BusinessError).message);
|
||||
}
|
||||
}
|
||||
}
|
||||
triggerTabSwitchFeedback(index: number) {
|
||||
this.currentIndex = index;
|
||||
AppStorage.setOrCreate('global_theme_color', THEME_COLOR);
|
||||
this.onTabChange?.(index);
|
||||
}
|
||||
tabBarBuilder(index: number, icon: string, label: string, parent = null) {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height(56);
|
||||
Column.justifyContent(FlexAlign.Center);
|
||||
Column.alignItems(HorizontalAlign.Center);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.currentIndex === index && this.windowFocused) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Circle.create();
|
||||
Circle.width(32);
|
||||
Circle.height(32);
|
||||
Circle.backgroundColor(`${THEME_COLOR}33`);
|
||||
Circle.blur(8);
|
||||
Circle.position({ x: '50%', y: '50%' });
|
||||
Circle.translate({ x: '-50%', y: '-50%' });
|
||||
}, Circle);
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(icon);
|
||||
Text.fontSize(20);
|
||||
Text.opacity(this.currentIndex === index ? 1 : 0.5);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(label);
|
||||
Text.fontSize(10);
|
||||
Text.fontColor(this.currentIndex === index ? THEME_COLOR : '#999');
|
||||
Text.fontWeight(this.currentIndex === index ? FontWeight.Bold : FontWeight.Normal);
|
||||
}, Text);
|
||||
Text.pop();
|
||||
Column.pop();
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Stack.create();
|
||||
Stack.width('100%');
|
||||
Stack.height('100%');
|
||||
Stack.backgroundColor('#00000000');
|
||||
}, Stack);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
}, Column);
|
||||
this.contentBuilder.bind(this)();
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('92%');
|
||||
Column.height(72);
|
||||
Column.alignSelf(ItemAlign.Center);
|
||||
Column.position({ y: `calc(100% - ${this.bottomAvoidHeight > 0 ? this.bottomAvoidHeight : 16}px - 72px)` });
|
||||
Column.borderRadius(24);
|
||||
Column.shadow({
|
||||
radius: 20,
|
||||
offsetY: -4,
|
||||
color: 'rgba(0,0,0,0.15)'
|
||||
});
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Stack.create();
|
||||
Stack.width('100%');
|
||||
Stack.height('100%');
|
||||
}, Stack);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundBlurStyle(BlurStyle.Regular);
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundColor(`${THEME_COLOR}0D`);
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.linearGradient({
|
||||
angle: 180,
|
||||
colors: [['rgba(255,255,255,0.15)', 0.0], ['rgba(255,255,255,0.05)', 1.0]]
|
||||
});
|
||||
Column.borderRadius(24);
|
||||
}, Column);
|
||||
Column.pop();
|
||||
Stack.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Tabs.create({ index: this.currentIndex });
|
||||
Tabs.width('100%');
|
||||
Tabs.height(64);
|
||||
Tabs.barPosition(BarPosition.End);
|
||||
Tabs.onChange((index: number) => {
|
||||
this.triggerTabSwitchFeedback(index);
|
||||
});
|
||||
}, Tabs);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TabContent.create(() => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Blank.create();
|
||||
}, Blank);
|
||||
Blank.pop();
|
||||
Column.pop();
|
||||
});
|
||||
TabContent.tabBar({ builder: () => {
|
||||
this.tabBarBuilder.call(this, 0, '🌌', 'TrulyMEM');
|
||||
} });
|
||||
}, TabContent);
|
||||
TabContent.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TabContent.create(() => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Blank.create();
|
||||
}, Blank);
|
||||
Blank.pop();
|
||||
Column.pop();
|
||||
});
|
||||
TabContent.tabBar({ builder: () => {
|
||||
this.tabBarBuilder.call(this, 1, '⚙', '设置');
|
||||
} });
|
||||
}, TabContent);
|
||||
TabContent.pop();
|
||||
Tabs.pop();
|
||||
Column.pop();
|
||||
Stack.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,40 @@
|
||||
export class Constants {
|
||||
static readonly DB_NAME: string = 'trulymem.db';
|
||||
static readonly CONFIG_PREF_NAME: string = 'trulymem_config';
|
||||
static readonly DEFAULT_BASE_URL: string = 'https://api.deepseek.com';
|
||||
static readonly DEFAULT_MODEL: string = 'deepseek-chat';
|
||||
static readonly SECURITY_LEVEL: number = 1; // S1
|
||||
// Table names
|
||||
static readonly TABLE_NODES: string = 'nodes';
|
||||
static readonly TABLE_RELATIONS: string = 'relations';
|
||||
static readonly TABLE_CHAT: string = 'chat_records';
|
||||
// SQL definitions
|
||||
static readonly SQL_CREATE_NODES: string = `
|
||||
CREATE TABLE IF NOT EXISTS nodes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT DEFAULT 'concept',
|
||||
mentions INTEGER DEFAULT 1,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
static readonly SQL_CREATE_RELATIONS: string = `
|
||||
CREATE TABLE IF NOT EXISTS relations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
subject_id INTEGER NOT NULL,
|
||||
relation TEXT NOT NULL,
|
||||
object_id INTEGER NOT NULL,
|
||||
weight REAL DEFAULT 1.0,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||
FOREIGN KEY (subject_id) REFERENCES nodes(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (object_id) REFERENCES nodes(id) ON DELETE CASCADE
|
||||
)`;
|
||||
static readonly SQL_CREATE_CHAT: string = `
|
||||
CREATE TABLE IF NOT EXISTS chat_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
role TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
tools TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
)`;
|
||||
}
|
||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@ -0,0 +1,53 @@
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import type { BusinessError } from "@ohos:base";
|
||||
export interface RouterParam {
|
||||
routerName: string;
|
||||
param?: object;
|
||||
}
|
||||
export interface IPageContext {
|
||||
openPage(data: RouterParam, animated?: boolean): void;
|
||||
popPage(animated?: boolean): void;
|
||||
replacePage(data: RouterParam, animated?: boolean): void;
|
||||
}
|
||||
export class PageContext implements IPageContext {
|
||||
private readonly pathStack: NavPathStack;
|
||||
constructor() {
|
||||
this.pathStack = new NavPathStack();
|
||||
}
|
||||
public get navPathStack(): NavPathStack {
|
||||
return this.pathStack;
|
||||
}
|
||||
public replacePage(data: RouterParam, animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.replacePath({ name: data.routerName, param: data.param }, animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('replacePage: ' + data.routerName + ' failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public openPage(data: RouterParam, animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.pushPath({ name: data.routerName, param: data.param }, animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('openPage: ' + data.routerName + ' failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public popPage(animated: boolean = true): void {
|
||||
try {
|
||||
this.pathStack.pop(animated);
|
||||
}
|
||||
catch (err) {
|
||||
const errMsg = (err as BusinessError).message || JSON.stringify(err);
|
||||
defaultLogger.error('popPage failed. ' + errMsg);
|
||||
}
|
||||
}
|
||||
public popPageByIndex(index: number, animated: boolean = true): void {
|
||||
this.pathStack.popToIndex(index, animated);
|
||||
}
|
||||
public clear(animated: boolean = true): void {
|
||||
this.pathStack.clear(animated);
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,785 @@
|
||||
import http from "@ohos:net.http";
|
||||
import dataPreferences from "@ohos:data.preferences";
|
||||
import type common from "@ohos:app.ability.common";
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
import type { GraphMemoryService, EntityInfo, RelationInfo, MemoryRecallParams, MemoryCommitParams, MemoryPurgeParams, PurgeCriteriaParams, NewRelationParams, PersonaUpdateParams, TaskCreateParams, TaskSetStateParams, TaskDeleteParams, TaskLinkInfoParams, TaskArchiveParams, TripletInput, PersonaQueryResult, MemoryRecallResult } from './GraphMemoryService';
|
||||
import type { TimeRangeParams } from '../model/GraphDatabase';
|
||||
export interface ChatMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
export interface AgentResponse {
|
||||
content: string;
|
||||
toolCalls: ToolCallResult[];
|
||||
}
|
||||
export interface ToolCallResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
// ========= 工具定义类型 =========
|
||||
// Concrete interface for tool property definitions (replaces Record<string, T>)
|
||||
interface ToolPropertiesDefinition {
|
||||
days?: ToolParamProperty;
|
||||
queryIntent?: ToolParamProperty;
|
||||
seedEntities?: ToolParamProperty;
|
||||
depth?: ToolParamProperty;
|
||||
timeRange?: ToolParamProperty;
|
||||
sessionFilter?: ToolParamProperty;
|
||||
triplets?: ToolParamProperty;
|
||||
entityTypes?: ToolParamProperty;
|
||||
sessionId?: ToolParamProperty;
|
||||
turnId?: ToolParamProperty;
|
||||
criteria?: ToolParamProperty;
|
||||
mode?: ToolParamProperty;
|
||||
newRelation?: ToolParamProperty;
|
||||
tone?: ToolParamProperty;
|
||||
style?: ToolParamProperty;
|
||||
personality?: ToolParamProperty;
|
||||
catchphrase?: ToolParamProperty;
|
||||
background?: ToolParamProperty;
|
||||
taskId?: ToolParamProperty;
|
||||
description?: ToolParamProperty;
|
||||
infoNodes?: ToolParamProperty;
|
||||
state?: ToolParamProperty;
|
||||
deleteInfoNodes?: ToolParamProperty;
|
||||
infoNodeNames?: ToolParamProperty;
|
||||
summary?: ToolParamProperty;
|
||||
limit?: ToolParamProperty;
|
||||
stateFilter?: ToolParamProperty;
|
||||
subject?: ToolParamProperty;
|
||||
relation?: ToolParamProperty;
|
||||
object?: ToolParamProperty;
|
||||
confidence?: ToolParamProperty;
|
||||
subjectContains?: ToolParamProperty;
|
||||
relationType?: ToolParamProperty;
|
||||
targetContains?: ToolParamProperty;
|
||||
target?: ToolParamProperty;
|
||||
dryRun?: ToolParamProperty;
|
||||
keyword?: ToolParamProperty;
|
||||
attribute?: ToolParamProperty;
|
||||
sourceType?: ToolParamProperty;
|
||||
targetType?: ToolParamProperty;
|
||||
sourceHasStatus?: ToolParamProperty;
|
||||
}
|
||||
interface ToolParamProperty {
|
||||
type: string;
|
||||
description: string;
|
||||
items?: ToolParamProperty;
|
||||
properties?: ToolPropertiesDefinition;
|
||||
required?: string[];
|
||||
enum?: string[];
|
||||
}
|
||||
interface ToolParamDecl {
|
||||
type: string;
|
||||
properties: ToolPropertiesDefinition;
|
||||
required?: string[];
|
||||
}
|
||||
interface ToolFunctionDecl {
|
||||
name: string;
|
||||
description: string;
|
||||
parameters: ToolParamDecl;
|
||||
}
|
||||
interface ToolFunctionDef {
|
||||
type: string;
|
||||
function: ToolFunctionDecl;
|
||||
}
|
||||
// ========= API 请求/响应结构 =========
|
||||
interface ApiRequestMessage {
|
||||
role: string;
|
||||
content: string;
|
||||
}
|
||||
interface ApiRequest {
|
||||
model: string;
|
||||
messages: ApiRequestMessage[];
|
||||
tools?: ToolFunctionDef[];
|
||||
tool_choice?: string;
|
||||
}
|
||||
interface ApiToolCall {
|
||||
id: string;
|
||||
type: string;
|
||||
function: ToolFunctionCall;
|
||||
}
|
||||
interface ToolFunctionCall {
|
||||
name: string;
|
||||
arguments: string;
|
||||
}
|
||||
interface ApiChoiceMessage {
|
||||
content?: string;
|
||||
tool_calls?: ApiToolCall[];
|
||||
}
|
||||
interface ApiChoice {
|
||||
message: ApiChoiceMessage;
|
||||
}
|
||||
interface ApiResponse {
|
||||
choices: ApiChoice[];
|
||||
}
|
||||
// ========= 内部结果类型 =========
|
||||
interface ExecuteToolResult {
|
||||
name: string;
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
interface BuildContextBlockParams {
|
||||
persona: Record<string, string>;
|
||||
found: boolean;
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
message: string;
|
||||
}
|
||||
// ========= 服务方法参数类型 =========
|
||||
// ========= 工具定义辅助函数 =========
|
||||
function makeStringProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'string', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeIntegerProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'integer', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeObjectProp(description: string, props: ToolPropertiesDefinition, required?: string[]): ToolParamProperty {
|
||||
const param: ToolParamProperty = { type: 'object', description: description };
|
||||
param.properties = props;
|
||||
if (required && required.length > 0) {
|
||||
param.required = required;
|
||||
}
|
||||
return param;
|
||||
}
|
||||
function makeArrayProp(description: string, item: ToolParamProperty): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'array', description: description, items: item };
|
||||
return result;
|
||||
}
|
||||
function makeBoolProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'boolean', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeNumberProp(description: string): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'number', description: description };
|
||||
return result;
|
||||
}
|
||||
function makeEnumProp(description: string, enumValues: string[]): ToolParamProperty {
|
||||
const result: ToolParamProperty = { type: 'string', description: description, enum: enumValues };
|
||||
return result;
|
||||
}
|
||||
function makeToolDef(name: string, description: string, properties: ToolPropertiesDefinition, required?: string[]): ToolFunctionDef {
|
||||
const params: ToolParamDecl = { type: 'object', properties: properties };
|
||||
const func: ToolFunctionDecl = { name: name, description: description, parameters: params };
|
||||
const tool: ToolFunctionDef = { type: 'function', function: func };
|
||||
if (required && required.length > 0) {
|
||||
tool.function.parameters.required = required;
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
/**
|
||||
* 系统提示词 — AI 人设 + 图记忆使用说明
|
||||
*/
|
||||
function buildSystemPrompt(personaContext: string): string {
|
||||
return `你是 TrulyMEM(True Memory)——一个拥有真实记忆的 AI 助手。
|
||||
|
||||
## 核心身份
|
||||
${personaContext || '你是一个帮助用户记录和回忆信息的助手。你的核心能力是基于图数据库的记忆系统。'}
|
||||
|
||||
## ⚠️ 内部执行顺序(不得向用户输出)
|
||||
|
||||
以下步骤是内部流程,绝对不要在你的回复中提及或输出。应当仅通过工具调用悄悄完成,回复时直接给出自然的对话内容。
|
||||
|
||||
1. memory_recall → 查询人设图
|
||||
2. task_query → 查询工作记忆链/最近任务
|
||||
3. 处理对话内容 + 思考回复
|
||||
4. memory_commit → 写入本轮关键信息到图数据库
|
||||
5. task_archive → 归档已完成的旧任务
|
||||
6. 条件: 本轮调用 ≥5 次查询类工具 → context_rewrite 压缩工具 JSON
|
||||
|
||||
## 三元组规范
|
||||
使用 memory_commit 时,subject/relation/object 每个字段必须是一个短关键字(1~5个字),不能是完整句子。
|
||||
|
||||
## 任务信息节点规范
|
||||
- info_nodes 只能包含该任务专属的具体信息节点,严禁关联"用户"、"AI"、"系统"等全局通用实体
|
||||
- 全局实体的信息直接用独立关系记录,不需要通过 Task 中转
|
||||
|
||||
## 可用工具
|
||||
- memory_recall(queryIntent, seedEntities?, depth?, timeRange?, sessionFilter?): 检索记忆
|
||||
- memory_commit(triplets, entityTypes?, sessionId?, turnId?): 写入记忆
|
||||
- memory_purge(criteria, mode, newRelation?): 删除/修正记忆
|
||||
- memory_introspect(sessionId?): 查看记忆状态统计
|
||||
- memory_archive(days?): 归档旧记忆
|
||||
- memory_cleanup(dryRun?): 清理已删除数据
|
||||
- memory_query_archived(days?, keyword?): 查询已归档记忆
|
||||
- context_rewrite(summary): 压缩工具调用上下文
|
||||
- persona_update(tone?, style?, personality?, catchphrase?, background?): 更新人设
|
||||
- persona_remove(attribute): 删除单条人设属性
|
||||
- persona_clear(): 清除人设
|
||||
- task_create(taskId, description, infoNodes?): 创建任务
|
||||
- task_set_state(taskId, state): 设置任务状态
|
||||
- task_delete(taskId, deleteInfoNodes?): 删除任务
|
||||
- task_link_info(taskId, infoNodeNames): 关联信息节点
|
||||
- task_archive(taskId, summary?): 归档任务
|
||||
- task_query(limit?, stateFilter?): 查询任务列表
|
||||
|
||||
## 工具调用规则
|
||||
1. ⚠️ 在完成所有工具调用之前,绝对不要输出任何文字。先默默调用工具,等所有结果返回后再输出一次完整的回复。
|
||||
2. 每轮对话必须按顺序执行:步骤1查询人设 → 步骤2查询工作记忆链 → 步骤3处理请求
|
||||
3. context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!
|
||||
4. 工具调用 ≥5 次后应使用 context_rewrite 压缩上下文
|
||||
|
||||
## 写入规则
|
||||
用户明确表达以下信息时必须写入记忆:
|
||||
- 偏好、兴趣
|
||||
- 个人信息(工作、项目、学习)
|
||||
- 计划安排
|
||||
- 当前状态
|
||||
- 结论性事实
|
||||
|
||||
推理得到的信息可以写入但需标注 [推测]。`;
|
||||
}
|
||||
// ========= 工具定义 =========
|
||||
// Pre-typed property dictionaries for tool definitions
|
||||
const recallTimeRangeDict: ToolPropertiesDefinition = { days: makeIntegerProp('最近N天') };
|
||||
const tripletPropsDict: ToolPropertiesDefinition = {
|
||||
subject: makeStringProp('主体'),
|
||||
relation: makeStringProp('关系'),
|
||||
object: makeStringProp('客体'),
|
||||
confidence: makeNumberProp('置信度')
|
||||
};
|
||||
const purgeCriteriaDict: ToolPropertiesDefinition = {
|
||||
subjectContains: makeStringProp('源实体名包含(模糊匹配)'),
|
||||
relationType: makeStringProp('关系类型'),
|
||||
targetContains: makeStringProp('目标实体名包含(模糊匹配)'),
|
||||
sessionId: makeStringProp('会话ID过滤'),
|
||||
sourceType: makeStringProp('源实体类型过滤(如 TaskNode)'),
|
||||
targetType: makeStringProp('目标实体类型过滤'),
|
||||
sourceHasStatus: makeStringProp('源实体状态过滤(如 archived)')
|
||||
};
|
||||
const newRelDict: ToolPropertiesDefinition = {
|
||||
relation: makeStringProp(''),
|
||||
target: makeStringProp('')
|
||||
};
|
||||
const EMPTY_PROPS: ToolPropertiesDefinition = {};
|
||||
const recallProps: ToolPropertiesDefinition = {
|
||||
queryIntent: makeStringProp('查询意图,支持逗号分隔多个关键词'),
|
||||
seedEntities: makeArrayProp('种子实体(可选)', makeStringProp('')),
|
||||
depth: makeIntegerProp('搜索深度,默认2'),
|
||||
timeRange: makeObjectProp('时间范围(可选)', recallTimeRangeDict),
|
||||
sessionFilter: makeStringProp('会话ID过滤(可选)')
|
||||
};
|
||||
const commitProps: ToolPropertiesDefinition = {
|
||||
triplets: makeArrayProp('三元组列表', makeObjectProp('', tripletPropsDict, ['subject', 'relation', 'object'])),
|
||||
entityTypes: makeObjectProp('实体类型映射(可选)', EMPTY_PROPS),
|
||||
sessionId: makeStringProp('会话ID(可选)'),
|
||||
turnId: makeIntegerProp('轮次ID(可选)')
|
||||
};
|
||||
const purgeProps: ToolPropertiesDefinition = {
|
||||
criteria: makeObjectProp('删除条件', purgeCriteriaDict),
|
||||
mode: makeEnumProp('删除模式:soft逻辑删除, hard物理删除, supersede纠错替代', ['soft', 'hard', 'supersede']),
|
||||
newRelation: makeObjectProp('替代关系(supersede模式用)', newRelDict)
|
||||
};
|
||||
const personaProps: ToolPropertiesDefinition = {
|
||||
tone: makeStringProp('语气'),
|
||||
style: makeStringProp('风格'),
|
||||
personality: makeStringProp('性格'),
|
||||
catchphrase: makeStringProp('口头禅'),
|
||||
background: makeStringProp('背景')
|
||||
};
|
||||
const createProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
description: makeStringProp('任务描述'),
|
||||
infoNodes: makeArrayProp('关联的信息节点名称列表', makeStringProp(''))
|
||||
};
|
||||
const setStateProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
state: makeEnumProp('任务状态', ['进行中', '已完成', '已暂停', '已取消'])
|
||||
};
|
||||
const deleteProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
deleteInfoNodes: makeBoolProp('是否删除关联的信息节点')
|
||||
};
|
||||
const linkInfoProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
infoNodeNames: makeArrayProp('信息节点名称列表', makeStringProp(''))
|
||||
};
|
||||
const archiveProps: ToolPropertiesDefinition = {
|
||||
taskId: makeStringProp('任务ID'),
|
||||
summary: makeStringProp('归档摘要')
|
||||
};
|
||||
const queryProps: ToolPropertiesDefinition = {
|
||||
limit: makeIntegerProp('返回数量,默认10'),
|
||||
stateFilter: makeStringProp('状态过滤: 进行中/已完成/已暂停/已取消/archived')
|
||||
};
|
||||
const introspectProps: ToolPropertiesDefinition = {
|
||||
sessionId: makeStringProp('会话ID(可选)')
|
||||
};
|
||||
const archiveProps2: ToolPropertiesDefinition = {
|
||||
days: makeIntegerProp('归档天数,默认30')
|
||||
};
|
||||
const cleanupProps: ToolPropertiesDefinition = {
|
||||
dryRun: makeBoolProp('仅预览不删除')
|
||||
};
|
||||
const queryArchivedProps: ToolPropertiesDefinition = {
|
||||
days: makeIntegerProp('最近N天内的归档记录'),
|
||||
keyword: makeStringProp('关键词过滤')
|
||||
};
|
||||
const contextRewriteProps: ToolPropertiesDefinition = {
|
||||
summary: makeStringProp('压缩后的摘要文本,必须包含工具调用元信息')
|
||||
};
|
||||
const personaRemoveProps: ToolPropertiesDefinition = {
|
||||
attribute: makeStringProp('要删除的属性名(如:扮演角色、说话风格)')
|
||||
};
|
||||
const TOOLS_DEFINITION: ToolFunctionDef[] = [
|
||||
makeToolDef('memory_recall', '检索记忆。支持关键词、时间范围、会话过滤。返回相关实体和关系。\n\n【⚠️ 强制执行顺序 - 每轮必须严格遵守】\n1. 步骤1(必须首先执行): 查询人设图\n2. 步骤2(必须第二步执行): 查询工作记忆链\n【重要】跳过步骤1或步骤2将导致系统错误!', recallProps, ['queryIntent']),
|
||||
makeToolDef('memory_commit', '写入记忆。将三元组写入图数据库,支持批量写入。\n\n【重要】写入原则:\n- 用户明确表达的信息 → 必须写入\n- AI推理得到的信息 → 可以写入,但需标注[推测]\n- 避免写入冗余或无意义的信息', commitProps, ['triplets']),
|
||||
makeToolDef('memory_purge', '删除或修正记忆。支持条件删除和纠错替代。\n\n【使用场景】\n- 纠错替代修正错误信息\n- 删除特定类型的节点关系\n- 删除残留在已归档任务上的状态关系\n\n【重要】\n- 优先使用 supersede 模式修正错误\n- 软删除不会物理删除数据', purgeProps, ['criteria', 'mode']),
|
||||
makeToolDef('memory_introspect', '查看记忆状态。返回实体数量、关系数量、热点实体。', introspectProps),
|
||||
makeToolDef('memory_archive', '归档旧记忆。将N天前的非活跃关系标记为归档状态。', archiveProps2, ['days']),
|
||||
makeToolDef('memory_cleanup', '清理无效数据。物理删除已删除状态超过90天的关系和孤立节点。', cleanupProps),
|
||||
makeToolDef('memory_query_archived', '查询已归档的记忆。\n\n【使用场景】\n- 想了解之前归档过哪些记忆\n- 按关键词搜索归档内容\n- 按时间范围查看最近归档的历史\n\n【注意】\n- 只返回 status=archived 的原始关系记录\n- days 和 keyword 可以单独使用或组合使用', queryArchivedProps),
|
||||
makeToolDef('context_rewrite', '压缩本轮对话的工具调用上下文。将冗长的JSON工具结果提炼为简洁摘要。\n\n【使用场景】\n- 本轮已执行 ≥5 次查询类工具调用\n- 【⚠️ 强制要求】context_rewrite 必须单独调用,不能和其他工具在同一轮一起调!', contextRewriteProps, ['summary']),
|
||||
makeToolDef('persona_update', '更新AI人设属性(语气、风格、性格等)。', personaProps),
|
||||
makeToolDef('persona_remove', '删除单条人设属性。保留其他人设不变。', personaRemoveProps, ['attribute']),
|
||||
makeToolDef('persona_clear', '清除所有人设信息。', EMPTY_PROPS),
|
||||
makeToolDef('task_create', '创建新的工作记忆任务节点。\n\n【重要】info_nodes 只能包含该任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁关联\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', createProps, ['taskId', 'description']),
|
||||
makeToolDef('task_set_state', '设置任务状态。', setStateProps, ['taskId', 'state']),
|
||||
makeToolDef('task_delete', '删除任务节点。', deleteProps, ['taskId']),
|
||||
makeToolDef('task_link_info', '关联信息节点到任务。\n\n【重要】info_node_names只能放任务专属的具体信息节点(如\"成语接龙_当前成语\"),**严禁放\"用户\"、\"AI\"、\"系统\"等全局通用实体**——这些实体不应通过任务中转。', linkInfoProps, ['taskId', 'infoNodeNames']),
|
||||
makeToolDef('task_archive', '归档已完成/过期的任务。将任务状态设为 archived,同时写入完成摘要到图数据库。\n\n【使用场景】\n1. 话题转变时归档旧任务\n2. 已完成的任务及时归档\n3. 长时间无更新的任务归档\n\n【注意】优先使用 task_archive 替代 task_set_state(state=archived),因为它会自动写入完成摘要。', archiveProps, ['taskId']),
|
||||
makeToolDef('task_query', '查询最近的任务列表。按更新时间倒序排列。新对话开始时优先使用此工具获取所有进展中的任务,避免重复创建。', queryProps)
|
||||
];
|
||||
// ========= 工具名称映射 =========
|
||||
// Types for executeTool generic args
|
||||
type ToolStateArg = '进行中' | '已完成' | '已暂停' | '已取消';
|
||||
type ToolHandlerName = 'memoryRecal' | 'memoryCommit' | 'memoryPurge' | 'memoryIntrospect' | 'memoryArchive' | 'memoryCleanup' | 'memoryQueryArchived' | 'contextRewrite' | 'personaUpdate' | 'personaRemove' | 'personaClear' | 'taskCreate' | 'taskSetState' | 'taskDelete' | 'taskLinkInfo' | 'taskArchive' | 'taskQuery';
|
||||
const TOOL_HANDLER_MAP: Record<string, ToolHandlerName> = {
|
||||
'memory_recall': 'memoryRecal',
|
||||
'memory_commit': 'memoryCommit',
|
||||
'memory_purge': 'memoryPurge',
|
||||
'memory_introspect': 'memoryIntrospect',
|
||||
'memory_archive': 'memoryArchive',
|
||||
'memory_cleanup': 'memoryCleanup',
|
||||
'memory_query_archived': 'memoryQueryArchived',
|
||||
'context_rewrite': 'contextRewrite',
|
||||
'persona_update': 'personaUpdate',
|
||||
'persona_remove': 'personaRemove',
|
||||
'persona_clear': 'personaClear',
|
||||
'task_create': 'taskCreate',
|
||||
'task_set_state': 'taskSetState',
|
||||
'task_delete': 'taskDelete',
|
||||
'task_link_info': 'taskLinkInfo',
|
||||
'task_archive': 'taskArchive',
|
||||
'task_query': 'taskQuery',
|
||||
};
|
||||
// ========= AIAgentService =========
|
||||
export class AIAgentService {
|
||||
private memoryService: GraphMemoryService;
|
||||
private currentSessionId: string;
|
||||
private turnCounter: number = 0;
|
||||
private appContext: common.Context;
|
||||
constructor(memoryService: GraphMemoryService, appContext: common.Context, sessionId?: string) {
|
||||
this.memoryService = memoryService;
|
||||
this.appContext = appContext;
|
||||
this.currentSessionId = sessionId || `session-hm-${Date.now()}`;
|
||||
}
|
||||
getSessionId(): string {
|
||||
return this.currentSessionId;
|
||||
}
|
||||
/**
|
||||
* 发送消息 — 完整的 Agent 流程
|
||||
* 1. 查询人设
|
||||
* 2. 查询工作记忆链
|
||||
* 3. 注入上下文后请求 AI
|
||||
* 4. 处理 tool_calls
|
||||
* 5. 返回最终回复
|
||||
*/
|
||||
async sendMessage(userInput: string): Promise<AgentResponse> {
|
||||
this.turnCounter++;
|
||||
// === 步骤1+2: 获取上下文 ===
|
||||
const personaResult = await this.memoryService.personaQuery();
|
||||
const personaContext: string = personaResult.found ? this.formatPersona(personaResult.persona) : '';
|
||||
const recallParams: MemoryRecallParams = {
|
||||
queryIntent: 'TaskNode,工作记忆,任务链',
|
||||
depth: 2
|
||||
};
|
||||
const taskResult = await this.memoryService.memoryRecall(recallParams);
|
||||
// === 读取 API 配置 ===
|
||||
const context = this.appContext;
|
||||
const pref = await dataPreferences.getPreferences(context, 'trulymem_config');
|
||||
const baseUrl: string = String(await pref.get('base_url', 'https://api.deepseek.com'));
|
||||
const model: string = String(await pref.get('model', 'deepseek-chat'));
|
||||
const apiKey: string = String(await pref.get('api_key', ''));
|
||||
if (!apiKey) {
|
||||
const noKeyResponse: AgentResponse = {
|
||||
content: '⚠️ API Key 未配置,请先在设置页填写 API Key。',
|
||||
toolCalls: []
|
||||
};
|
||||
return noKeyResponse;
|
||||
}
|
||||
// === 构建上下文丰富的消息 ===
|
||||
const systemPrompt: string = buildSystemPrompt(personaContext);
|
||||
const contextBlock: string = this.buildContextBlock(personaResult, taskResult);
|
||||
const sysMsg: ApiRequestMessage = { role: 'system' as string, content: systemPrompt };
|
||||
const userMsg: ApiRequestMessage = { role: 'user' as string, content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
|
||||
const messages: ApiRequestMessage[] = [sysMsg, userMsg];
|
||||
// === 步骤3: 请求 AI ===
|
||||
const response: ApiResponse = await this.callApi(messages, baseUrl, model, apiKey);
|
||||
const toolCalls: ToolCallResult[] = [];
|
||||
// === 步骤4: 处理 tool_calls ===
|
||||
if (response.choices && response.choices.length > 0) {
|
||||
const choice: ApiChoice = response.choices[0];
|
||||
const aiMessage: ApiChoiceMessage = choice.message;
|
||||
// 处理函数调用
|
||||
if (aiMessage.tool_calls && aiMessage.tool_calls.length > 0) {
|
||||
for (const tc of aiMessage.tool_calls) {
|
||||
const handlerName: ToolHandlerName | undefined = TOOL_HANDLER_MAP[tc.function.name];
|
||||
if (handlerName) {
|
||||
let args: Record<string, Object> = {};
|
||||
try {
|
||||
args = JSON.parse(tc.function.arguments);
|
||||
}
|
||||
catch (parseErr) {
|
||||
const parseErrMsg = (parseErr as Error).message || JSON.stringify(parseErr);
|
||||
defaultLogger.error('Failed to parse tool arguments: ' + parseErrMsg);
|
||||
const badArgsResult: ToolCallResult = {
|
||||
name: tc.function.name,
|
||||
success: false,
|
||||
message: '参数解析失败'
|
||||
};
|
||||
toolCalls.push(badArgsResult);
|
||||
continue;
|
||||
}
|
||||
const result: ToolCallResult = await this.executeTool(handlerName, args);
|
||||
toolCalls.push(result);
|
||||
}
|
||||
else {
|
||||
const unknownToolResult: ToolCallResult = {
|
||||
name: tc.function.name,
|
||||
success: false,
|
||||
message: '未知工具'
|
||||
};
|
||||
toolCalls.push(unknownToolResult);
|
||||
}
|
||||
}
|
||||
// 有 tool_calls 时需要再次请求 AI,带上工具执行结果
|
||||
const followUpSystemMsg: ApiRequestMessage = { role: 'system', content: systemPrompt };
|
||||
const followUpUserMsg: ApiRequestMessage = { role: 'user', content: contextBlock + '\n\n---\n\n用户消息: ' + userInput };
|
||||
const followUpAssistantMsg: ApiRequestMessage = {
|
||||
role: 'assistant',
|
||||
content: aiMessage.content || '(已执行记忆操作)',
|
||||
};
|
||||
const toolResultsMessages: ApiRequestMessage[] = [
|
||||
followUpSystemMsg,
|
||||
followUpUserMsg,
|
||||
followUpAssistantMsg,
|
||||
];
|
||||
for (const tc of aiMessage.tool_calls) {
|
||||
const callResult: ToolCallResult | undefined = toolCalls.find(r => r.name === tc.function.name);
|
||||
const toolResultMsg: string = callResult ? callResult.message : '完成';
|
||||
const toolResultMessage: ApiRequestMessage = {
|
||||
role: 'tool',
|
||||
content: `工具 ${tc.function.name} 执行结果: ${toolResultMsg}`
|
||||
};
|
||||
toolResultsMessages.push(toolResultMessage);
|
||||
}
|
||||
const finalResponse: ApiResponse = await this.callApi(toolResultsMessages, baseUrl, model, apiKey);
|
||||
if (finalResponse.choices && finalResponse.choices.length > 0) {
|
||||
const content: string = finalResponse.choices[0].message.content || '';
|
||||
const finalResult: AgentResponse = { content, toolCalls };
|
||||
return finalResult;
|
||||
}
|
||||
}
|
||||
// 普通回复(无 tool_calls)
|
||||
const content: string = aiMessage.content || '';
|
||||
const noToolResponse: AgentResponse = { content, toolCalls };
|
||||
return noToolResponse;
|
||||
}
|
||||
const noResponse: AgentResponse = {
|
||||
content: 'AI 无响应',
|
||||
toolCalls
|
||||
};
|
||||
return noResponse;
|
||||
}
|
||||
/**
|
||||
* 请求 DeepSeek API
|
||||
*/
|
||||
private async callApi(messages: ApiRequestMessage[], baseUrl: string, model: string, apiKey: string): Promise<ApiResponse> {
|
||||
const httpRequest = http.createHttp();
|
||||
try {
|
||||
const resp = await httpRequest.request(baseUrl + '/chat/completions', {
|
||||
method: http.RequestMethod.POST,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiKey
|
||||
},
|
||||
extraData: {
|
||||
model: model,
|
||||
messages: messages,
|
||||
tools: TOOLS_DEFINITION,
|
||||
tool_choice: 'auto'
|
||||
},
|
||||
expectDataType: http.HttpDataType.OBJECT,
|
||||
readTimeout: 60000
|
||||
});
|
||||
if (resp.responseCode === 200) {
|
||||
return resp.result as ApiResponse;
|
||||
}
|
||||
const errorMsg: string = `API 请求失败: HTTP ${resp.responseCode}`;
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
finally {
|
||||
httpRequest.destroy();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 执行工具调用
|
||||
*/
|
||||
private async executeTool(name: ToolHandlerName, args: Record<string, Object>): Promise<ToolCallResult> {
|
||||
try {
|
||||
switch (name) {
|
||||
case 'memoryRecal': {
|
||||
const recallArgs: MemoryRecallParams = {
|
||||
queryIntent: args.queryIntent as string,
|
||||
seedEntities: args.seedEntities as string[],
|
||||
depth: (args.depth as number) ?? 2,
|
||||
timeRange: args.timeRange as TimeRangeParams,
|
||||
sessionFilter: args.sessionFilter as string
|
||||
};
|
||||
const recallResult = await this.memoryService.memoryRecall(recallArgs);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_recall',
|
||||
success: true,
|
||||
message: `找到 ${recallResult.entities.length} 个实体, ${recallResult.relations.length} 条关系`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryCommit': {
|
||||
const commitParams: MemoryCommitParams = {
|
||||
triplets: args.triplets as TripletInput[],
|
||||
entityTypes: args.entityTypes as Record<string, string>,
|
||||
sessionId: (args.sessionId as string) || this.currentSessionId,
|
||||
turnId: (args.turnId as number) || this.turnCounter
|
||||
};
|
||||
const commitResult = await this.memoryService.memoryCommit(commitParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_commit',
|
||||
success: true,
|
||||
message: `已写入 ${commitResult.committedCount} 条记忆`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryPurge': {
|
||||
const purgeArgs: MemoryPurgeParams = {
|
||||
criteria: args.criteria as PurgeCriteriaParams,
|
||||
mode: args.mode as 'soft' | 'hard' | 'supersede',
|
||||
newRelation: args.newRelation as NewRelationParams
|
||||
};
|
||||
const purgeResult = await this.memoryService.memoryPurge(purgeArgs);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_purge',
|
||||
success: true,
|
||||
message: purgeResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryIntrospect': {
|
||||
const introspectResult = await this.memoryService.memoryIntrospect(args.sessionId as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_introspect',
|
||||
success: true,
|
||||
message: `实体: ${introspectResult.entityCount}, 关系: ${introspectResult.relationCount}, 热点: ${introspectResult.hotNodes.length}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryArchive': {
|
||||
const archiveResult = await this.memoryService.archive(args.days as number);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_archive',
|
||||
success: true,
|
||||
message: `已归档 ${archiveResult.archived} 条关系`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryCleanup': {
|
||||
const cleanupResult = await this.memoryService.cleanup((args.dryRun as boolean) !== false);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_cleanup',
|
||||
success: true,
|
||||
message: `清理: ${cleanupResult.cleaned} 条关系, ${cleanupResult.deletedOrphans} 个孤儿节点` + (cleanupResult.dryRun ? ' (预览模式)' : '')
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'memoryQueryArchived': {
|
||||
const qaResult = await this.memoryService.queryArchived(args.days as number, args.keyword as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'memory_query_archived',
|
||||
success: true,
|
||||
message: `找到 ${qaResult.length} 条归档记录`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'contextRewrite': {
|
||||
const summary = args.summary as string;
|
||||
const result: ToolCallResult = {
|
||||
name: 'context_rewrite',
|
||||
success: summary.includes('[工具调用总结'),
|
||||
message: summary.includes('[工具调用总结') ? '上下文已压缩' : '格式错误:必须包含[工具调用总结]标记'
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaUpdate': {
|
||||
const personaParams: PersonaUpdateParams = {
|
||||
tone: args.tone as string,
|
||||
style: args.style as string,
|
||||
personality: args.personality as string,
|
||||
catchphrase: args.catchphrase as string,
|
||||
background: args.background as string
|
||||
};
|
||||
const puResult = await this.memoryService.personaUpdate(personaParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_update',
|
||||
success: puResult.success,
|
||||
message: puResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaRemove': {
|
||||
const prResult = await this.memoryService.personaRemove(args.attribute as string);
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_remove',
|
||||
success: prResult.success,
|
||||
message: prResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'personaClear': {
|
||||
const pcResult = await this.memoryService.personaClear();
|
||||
const result: ToolCallResult = {
|
||||
name: 'persona_clear',
|
||||
success: pcResult.success,
|
||||
message: pcResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskCreate': {
|
||||
const createParams: TaskCreateParams = {
|
||||
taskId: args.taskId as string,
|
||||
description: args.description as string,
|
||||
infoNodes: args.infoNodes as string[]
|
||||
};
|
||||
const tcResult = await this.memoryService.taskCreate(createParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_create',
|
||||
success: tcResult.success,
|
||||
message: tcResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskSetState': {
|
||||
const setStateParams: TaskSetStateParams = {
|
||||
taskId: args.taskId as string,
|
||||
state: args.state as ToolStateArg
|
||||
};
|
||||
const tsResult = await this.memoryService.taskSetState(setStateParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_set_state',
|
||||
success: tsResult.success,
|
||||
message: tsResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskDelete': {
|
||||
const deleteParams: TaskDeleteParams = {
|
||||
taskId: args.taskId as string,
|
||||
deleteInfoNodes: (args.deleteInfoNodes as boolean) !== false
|
||||
};
|
||||
const tdResult = await this.memoryService.taskDelete(deleteParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_delete',
|
||||
success: tdResult.success,
|
||||
message: tdResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskLinkInfo': {
|
||||
const linkInfoParams: TaskLinkInfoParams = {
|
||||
taskId: args.taskId as string,
|
||||
infoNodeNames: args.infoNodeNames as string[]
|
||||
};
|
||||
const tliResult = await this.memoryService.taskLinkInfo(linkInfoParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_link_info',
|
||||
success: tliResult.success,
|
||||
message: tliResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskArchive': {
|
||||
const archiveParams: TaskArchiveParams = {
|
||||
taskId: args.taskId as string,
|
||||
summary: args.summary as string
|
||||
};
|
||||
const taResult = await this.memoryService.taskArchive(archiveParams);
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_archive',
|
||||
success: taResult.success,
|
||||
message: taResult.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
case 'taskQuery': {
|
||||
const tqResult = await this.memoryService.taskQuery({
|
||||
limit: args.limit as number,
|
||||
stateFilter: args.stateFilter as string
|
||||
});
|
||||
const result: ToolCallResult = {
|
||||
name: 'task_query',
|
||||
success: true,
|
||||
message: `找到 ${tqResult.tasks.length} 个任务`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
default: {
|
||||
const defaultResult: ToolCallResult = {
|
||||
name: name as string,
|
||||
success: false,
|
||||
message: '未实现的工具'
|
||||
};
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const errorMessage: string = (e as Error).message || '';
|
||||
const errorResult: ToolCallResult = {
|
||||
name: name as string,
|
||||
success: false,
|
||||
message: `执行失败: ${errorMessage}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 格式化人设数据为文本
|
||||
*/
|
||||
private formatPersona(persona: Record<string, string>): string {
|
||||
const parts: string[] = [];
|
||||
const keys: string[] = Object.keys(persona);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key: string = keys[i];
|
||||
const val: string = persona[key];
|
||||
parts.push(`${key}: ${val}`);
|
||||
}
|
||||
return parts.length > 0 ? parts.join(';') : '';
|
||||
}
|
||||
/**
|
||||
* 构建上下文注入块
|
||||
*/
|
||||
private buildContextBlock(personaResult: PersonaQueryResult, taskResult: MemoryRecallResult): string {
|
||||
const blocks: string[] = [];
|
||||
if (personaResult.found) {
|
||||
blocks.push(`【当前人设】\n${this.formatPersona(personaResult.persona)}`);
|
||||
}
|
||||
if (taskResult.entities.length > 0) {
|
||||
const entitySample: EntityInfo[] = taskResult.entities.slice(0, 5);
|
||||
const entitiesStr: string = JSON.stringify(entitySample);
|
||||
blocks.push(`【工作记忆】\n${taskResult.message}\n${entitiesStr}`);
|
||||
}
|
||||
return blocks.length > 0 ? blocks.join('\n\n') : '【新对话】';
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,935 @@
|
||||
import type { GraphDatabase, RelationQueryResult, TimeRangeParams } from '../model/GraphDatabase';
|
||||
import { defaultLogger } from "@normalized:N&&&@ohos/common/src/main/ets/util/Logger&1.0.0";
|
||||
// ========= 接口定义 =========
|
||||
export interface SnapshotData {
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
}
|
||||
export interface GraphDataNode {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface GraphDataEdge {
|
||||
from: number;
|
||||
to: number;
|
||||
label: string;
|
||||
weight: number;
|
||||
depth?: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
export interface SnapshotEntity {
|
||||
name: string;
|
||||
type: string;
|
||||
mention_count: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface SnapshotRelation {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
session_id?: string;
|
||||
turn_id?: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface GraphOutput {
|
||||
nodes: GraphDataNode[];
|
||||
edges: GraphDataEdge[];
|
||||
}
|
||||
export interface SnapshotOutput {
|
||||
entities: SnapshotEntity[];
|
||||
relations: SnapshotRelation[];
|
||||
}
|
||||
export interface EntityInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
mentionCount: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface RelationInfo {
|
||||
source: string;
|
||||
target: string;
|
||||
type: string;
|
||||
confidence: number;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
depth?: number;
|
||||
}
|
||||
export interface TripletInput {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
confidence?: number;
|
||||
}
|
||||
export interface CleanupResult {
|
||||
cleaned: number;
|
||||
deletedRelations?: number;
|
||||
deletedOrphans?: number;
|
||||
dryRun?: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface SearchResult {
|
||||
name: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
export interface TaskInfo {
|
||||
taskId: string;
|
||||
description: string;
|
||||
state: string;
|
||||
infoCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
export interface NodeData {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
export interface EdgeData {
|
||||
from: number;
|
||||
to: number;
|
||||
label: string;
|
||||
weight: number;
|
||||
}
|
||||
export interface GraphData {
|
||||
nodes: NodeData[];
|
||||
edges: EdgeData[];
|
||||
}
|
||||
// 内部接口 — 用于替换内联对象类型声明
|
||||
export interface PurgeCriteriaParams {
|
||||
subjectContains?: string;
|
||||
relationType?: string;
|
||||
targetContains?: string;
|
||||
sessionId?: string;
|
||||
sourceType?: string;
|
||||
targetType?: string;
|
||||
sourceHasStatus?: string;
|
||||
}
|
||||
export interface NewRelationParams {
|
||||
relation: string;
|
||||
target: string;
|
||||
}
|
||||
interface TripletData {
|
||||
subject: string;
|
||||
relation: string;
|
||||
object: string;
|
||||
}
|
||||
interface CriteriaData {
|
||||
subject?: string;
|
||||
target?: string;
|
||||
relation?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
export interface MemoryRecallResult {
|
||||
entities: EntityInfo[];
|
||||
relations: RelationInfo[];
|
||||
message: string;
|
||||
}
|
||||
export interface MemoryRecallParams {
|
||||
queryIntent: string;
|
||||
seedEntities?: string[];
|
||||
depth?: number;
|
||||
timeRange?: TimeRangeParams;
|
||||
sessionFilter?: string;
|
||||
}
|
||||
export interface MemoryCommitParams {
|
||||
triplets: TripletInput[];
|
||||
entityTypes?: Record<string, string>;
|
||||
sessionId?: string;
|
||||
turnId?: number;
|
||||
}
|
||||
interface MemoryCommitResult {
|
||||
committedCount: number;
|
||||
details: string[];
|
||||
}
|
||||
export interface MemoryPurgeParams {
|
||||
criteria: PurgeCriteriaParams;
|
||||
mode: 'soft' | 'hard' | 'supersede';
|
||||
newRelation?: NewRelationParams;
|
||||
}
|
||||
interface MemoryPurgeResult {
|
||||
deletedCount: number;
|
||||
message: string;
|
||||
}
|
||||
interface HotNodeInfo {
|
||||
name: string;
|
||||
mentionCount: number;
|
||||
type: string;
|
||||
}
|
||||
interface MemoryIntrospectResult {
|
||||
entityCount: number;
|
||||
relationCount: number;
|
||||
hotNodes: HotNodeInfo[];
|
||||
message: string;
|
||||
}
|
||||
interface ArchiveResult {
|
||||
archived: number;
|
||||
message: string;
|
||||
}
|
||||
interface PersonaResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface PersonaQueryResult {
|
||||
persona: Record<string, string>;
|
||||
found: boolean;
|
||||
}
|
||||
interface TaskCreateResult {
|
||||
success: boolean;
|
||||
taskId: string;
|
||||
message: string;
|
||||
}
|
||||
interface TaskActionResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
export interface TaskQueryResult {
|
||||
tasks: TaskInfo[];
|
||||
message: string;
|
||||
}
|
||||
export interface PersonaUpdateParams {
|
||||
tone?: string;
|
||||
style?: string;
|
||||
personality?: string;
|
||||
catchphrase?: string;
|
||||
background?: string;
|
||||
}
|
||||
export interface TaskCreateParams {
|
||||
taskId: string;
|
||||
description: string;
|
||||
infoNodes?: string[];
|
||||
}
|
||||
export interface TaskSetStateParams {
|
||||
taskId: string;
|
||||
state: '进行中' | '已完成' | '已暂停' | '已取消' | '已归档';
|
||||
}
|
||||
export interface TaskDeleteParams {
|
||||
taskId: string;
|
||||
deleteInfoNodes?: boolean;
|
||||
}
|
||||
export interface TaskLinkInfoParams {
|
||||
taskId: string;
|
||||
infoNodeNames: string[];
|
||||
}
|
||||
export interface TaskArchiveParams {
|
||||
taskId: string;
|
||||
summary?: string;
|
||||
}
|
||||
export interface TaskQueryParams {
|
||||
limit?: number;
|
||||
stateFilter?: string;
|
||||
}
|
||||
// 节点详情连接项接口
|
||||
export interface ConnectionItem {
|
||||
type: string;
|
||||
target_name: string;
|
||||
}
|
||||
// 节点详情返回接口
|
||||
export interface NodeDetailInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
mention_count: number;
|
||||
connection_count: number;
|
||||
connections: ConnectionItem[];
|
||||
}
|
||||
// 图数据统计接口
|
||||
export interface GraphStats {
|
||||
maxDegree: number;
|
||||
avgDegree: number;
|
||||
}
|
||||
// 人设节点的固定名称
|
||||
const PERSONA_NODE_NAME: string = 'trulymem_persona_identity';
|
||||
const PERSONA_NODE_TYPE: string = 'PersonaNode';
|
||||
// ========= GraphMemoryService =========
|
||||
export class GraphMemoryService {
|
||||
private db: GraphDatabase;
|
||||
constructor(db: GraphDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
// ========= 记忆操作 =========
|
||||
/**
|
||||
* 记忆召回 — 关键词搜索 + BFS 扩展
|
||||
* 对应 tools.memory_recall
|
||||
*/
|
||||
async memoryRecall(params: MemoryRecallParams): Promise<MemoryRecallResult> {
|
||||
const depth: number = params.depth ?? 2;
|
||||
const result = await this.db.recall(params.queryIntent, params.seedEntities, depth, undefined, params.sessionFilter);
|
||||
const entities: EntityInfo[] = result.entities.map(e => {
|
||||
const entityItem: EntityInfo = {
|
||||
name: e.name,
|
||||
type: e.type,
|
||||
mentionCount: e.mention_count,
|
||||
depth: e.depth ?? 0
|
||||
};
|
||||
return entityItem;
|
||||
});
|
||||
const relations: RelationInfo[] = result.relations.map(r => {
|
||||
const relationItem: RelationInfo = {
|
||||
source: r.source,
|
||||
target: r.target,
|
||||
type: r.type,
|
||||
confidence: r.confidence,
|
||||
sessionId: r.session_id,
|
||||
turnId: r.turn_id,
|
||||
depth: r.depth ?? 0
|
||||
};
|
||||
return relationItem;
|
||||
});
|
||||
return { entities, relations, message: result.message };
|
||||
}
|
||||
/**
|
||||
* 记忆写入 — 批量三元组
|
||||
* 对应 tools.memory_commit
|
||||
*/
|
||||
async memoryCommit(params: MemoryCommitParams): Promise<MemoryCommitResult> {
|
||||
const details: string[] = [];
|
||||
for (const t of params.triplets) {
|
||||
const subject = t.subject.trim();
|
||||
const relation = t.relation.trim();
|
||||
const object = t.object.trim();
|
||||
if (!subject || !relation || !object) {
|
||||
continue;
|
||||
}
|
||||
const triplets: TripletData[] = [{ subject, relation, object }];
|
||||
await this.db.commit(triplets, params.entityTypes, params.sessionId, params.turnId);
|
||||
details.push(`${subject} -[${relation}]-> ${object}`);
|
||||
}
|
||||
const result: MemoryCommitResult = {
|
||||
committedCount: details.length,
|
||||
details
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 记忆删除
|
||||
* 对应 tools.memory_purge
|
||||
*/
|
||||
async memoryPurge(params: MemoryPurgeParams): Promise<MemoryPurgeResult> {
|
||||
// soft 模式:调用 db.purge 逻辑删除
|
||||
if (params.mode === 'supersede' && params.newRelation) {
|
||||
// supersede: 先软删除旧关系,再新建
|
||||
const softSubject = params.criteria.subjectContains;
|
||||
const softRelation = params.criteria.relationType;
|
||||
const softTarget = params.criteria.targetContains;
|
||||
await this.db.purge({
|
||||
subject: softSubject,
|
||||
relation: softRelation,
|
||||
target: softTarget
|
||||
}, 'soft');
|
||||
// 新建替代关系
|
||||
const newSubj = softSubject || '';
|
||||
if (newSubj) {
|
||||
await this.db.commit([{
|
||||
subject: newSubj,
|
||||
relation: params.newRelation.relation,
|
||||
object: params.newRelation.target
|
||||
}]);
|
||||
}
|
||||
const supersedeResult: MemoryPurgeResult = {
|
||||
deletedCount: 1,
|
||||
message: '已用 supersede 模式替代记忆'
|
||||
};
|
||||
return supersedeResult;
|
||||
}
|
||||
const subjContains = params.criteria.subjectContains;
|
||||
const relType = params.criteria.relationType;
|
||||
const tgtContains = params.criteria.targetContains;
|
||||
const sessId = params.criteria.sessionId;
|
||||
await this.db.purge({
|
||||
subject: subjContains,
|
||||
relation: relType,
|
||||
target: tgtContains,
|
||||
sessionId: sessId,
|
||||
subjectContains: params.criteria.subjectContains,
|
||||
targetContains: params.criteria.targetContains,
|
||||
sourceType: params.criteria.sourceType,
|
||||
targetType: params.criteria.targetType,
|
||||
sourceHasStatus: params.criteria.sourceHasStatus
|
||||
}, params.mode === 'hard' ? 'hard' : 'soft');
|
||||
const purgeResult: MemoryPurgeResult = {
|
||||
deletedCount: subjContains || tgtContains || relType || sessId ? 1 : 0,
|
||||
message: `已${params.mode === 'hard' ? '物理删除' : '软删除'}匹配的记忆`
|
||||
};
|
||||
return purgeResult;
|
||||
}
|
||||
/**
|
||||
* 记忆状态查询
|
||||
*/
|
||||
async memoryIntrospect(sessionId?: string): Promise<MemoryIntrospectResult> {
|
||||
const stats = await this.db.introspect();
|
||||
const hotNodes: HotNodeInfo[] = [];
|
||||
// 从所有节点中获取前10个高频节点
|
||||
const searchAll = await this.db.search('');
|
||||
const sorted = searchAll.sort((a, b) => b.mentions - a.mentions).slice(0, 10);
|
||||
for (const n of sorted) {
|
||||
const nodeInfo: HotNodeInfo = {
|
||||
name: n.name,
|
||||
mentionCount: n.mentions,
|
||||
type: n.type
|
||||
};
|
||||
hotNodes.push(nodeInfo);
|
||||
}
|
||||
const result: MemoryIntrospectResult = {
|
||||
entityCount: stats.entity_count,
|
||||
relationCount: stats.relation_count,
|
||||
hotNodes,
|
||||
message: stats.message
|
||||
};
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* 关键词搜索节点
|
||||
*/
|
||||
async search(keyword: string): Promise<SearchResult[]> {
|
||||
return await this.db.search(keyword);
|
||||
}
|
||||
/**
|
||||
* 归档旧记忆
|
||||
*/
|
||||
async archive(days: number): Promise<ArchiveResult> {
|
||||
return await this.db.archive(days);
|
||||
}
|
||||
/**
|
||||
* 清理已删除的记忆
|
||||
*/
|
||||
async cleanup(dryRun: boolean): Promise<CleanupResult> {
|
||||
const result = await this.db.cleanup(dryRun);
|
||||
const cleanupResult: CleanupResult = {
|
||||
cleaned: result.cleaned,
|
||||
deletedRelations: result.deleted_relations,
|
||||
deletedOrphans: result.deleted_orphans,
|
||||
dryRun: result.dry_run,
|
||||
message: result.message || ''
|
||||
};
|
||||
return cleanupResult;
|
||||
}
|
||||
/**
|
||||
* 记忆图谱 — 在指定时间范围内查询关系
|
||||
* 对应 tools.memory_graph
|
||||
*/
|
||||
async memoryGraph(timeRange: TimeRangeParams, sessionFilter?: string): Promise<GraphOutput> {
|
||||
const dbResult = await this.db.graph(timeRange, sessionFilter);
|
||||
const nodeCount: number = dbResult.nodes.length;
|
||||
const edgeCount: number = dbResult.edges.length;
|
||||
const nodeList: GraphDataNode[] = [];
|
||||
const edgeList: GraphDataEdge[] = [];
|
||||
let idx: number = 0;
|
||||
while (idx < nodeCount) {
|
||||
const n = dbResult.nodes[idx];
|
||||
const id1: number = n.id;
|
||||
const label1: string = n.label;
|
||||
const type1: string = n.type;
|
||||
const mentions1: number = n.mentions;
|
||||
const depth1: number | undefined = n.depth;
|
||||
const graphNode: GraphDataNode = {
|
||||
id: id1,
|
||||
label: label1,
|
||||
type: type1,
|
||||
mentions: mentions1,
|
||||
depth: depth1
|
||||
};
|
||||
nodeList.push(graphNode);
|
||||
idx++;
|
||||
}
|
||||
idx = 0;
|
||||
while (idx < edgeCount) {
|
||||
const e = dbResult.edges[idx];
|
||||
const from1: number = e.from;
|
||||
const to1: number = e.to;
|
||||
const label1: string = e.label;
|
||||
const weight1: number = e.weight;
|
||||
const depth1: number | undefined = e.depth;
|
||||
const sessionId1: string | undefined = e.sessionId;
|
||||
const turnId1: number | undefined = e.turnId;
|
||||
const graphEdge: GraphDataEdge = {
|
||||
from: from1,
|
||||
to: to1,
|
||||
label: label1,
|
||||
weight: weight1,
|
||||
depth: depth1,
|
||||
sessionId: sessionId1,
|
||||
turnId: turnId1
|
||||
};
|
||||
edgeList.push(graphEdge);
|
||||
idx++;
|
||||
}
|
||||
const out: GraphOutput = {
|
||||
nodes: nodeList,
|
||||
edges: edgeList
|
||||
};
|
||||
return out;
|
||||
}
|
||||
/**
|
||||
* 记忆快照 — 在指定时间范围内查询实体和关系
|
||||
* 对应 tools.memory_snapshot
|
||||
*/
|
||||
async memorySnapshot(timeRange: TimeRangeParams, sessionFilter?: string): Promise<SnapshotOutput> {
|
||||
const dbResult = await this.db.snapshot(timeRange, sessionFilter);
|
||||
const entityCount: number = dbResult.entities.length;
|
||||
const relationCount: number = dbResult.relations.length;
|
||||
const entityList: SnapshotEntity[] = [];
|
||||
const relationList: SnapshotRelation[] = [];
|
||||
let idx: number = 0;
|
||||
while (idx < entityCount) {
|
||||
const e = dbResult.entities[idx];
|
||||
const name1: string = e.name;
|
||||
const type1: string = e.type;
|
||||
const mentionCount1: number = e.mention_count;
|
||||
const depth1: number | undefined = e.depth;
|
||||
const entity: SnapshotEntity = {
|
||||
name: name1,
|
||||
type: type1,
|
||||
mention_count: mentionCount1,
|
||||
depth: depth1
|
||||
};
|
||||
entityList.push(entity);
|
||||
idx++;
|
||||
}
|
||||
idx = 0;
|
||||
while (idx < relationCount) {
|
||||
const r = dbResult.relations[idx];
|
||||
const source1: string = r.source;
|
||||
const target1: string = r.target;
|
||||
const type1: string = r.type;
|
||||
const confidence1: number = r.confidence;
|
||||
const sessionId1: string | undefined = r.session_id;
|
||||
const turnId1: number | undefined = r.turn_id;
|
||||
const depth1: number | undefined = r.depth;
|
||||
const relation: SnapshotRelation = {
|
||||
source: source1,
|
||||
target: target1,
|
||||
type: type1,
|
||||
confidence: confidence1,
|
||||
session_id: sessionId1,
|
||||
turn_id: turnId1,
|
||||
depth: depth1
|
||||
};
|
||||
relationList.push(relation);
|
||||
idx++;
|
||||
}
|
||||
const out2: SnapshotOutput = {
|
||||
entities: entityList,
|
||||
relations: relationList
|
||||
};
|
||||
return out2;
|
||||
}
|
||||
/**
|
||||
* 记忆清理 — 归档旧记忆并清理孤立节点
|
||||
* 对应 tools.memory_cleanup
|
||||
*/
|
||||
async memoryCleanup(dryRun: boolean = false): Promise<CleanupResult> {
|
||||
const result = await this.db.cleanup(dryRun);
|
||||
const cleanupResult: CleanupResult = {
|
||||
cleaned: result.cleaned,
|
||||
deletedRelations: result.deleted_relations,
|
||||
deletedOrphans: result.deleted_orphans,
|
||||
dryRun: result.dry_run,
|
||||
message: result.message || ''
|
||||
};
|
||||
return cleanupResult;
|
||||
}
|
||||
/**
|
||||
* 记忆清理 — 删除指定条件的记忆
|
||||
* 对应 tools.memory_purge
|
||||
*/
|
||||
/**
|
||||
* 查询已归档的记忆
|
||||
* 对应 tools.memory_query_archived
|
||||
*/
|
||||
async queryArchived(days?: number, keyword?: string): Promise<RelationQueryResult[]> {
|
||||
try {
|
||||
const result = await this.db.queryArchived(days, keyword);
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
defaultLogger.error('queryArchived error: ' + JSON.stringify(e));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
// ========= 人设管理 =========
|
||||
/**
|
||||
* 更新人设
|
||||
* 对应 tools.persona_update
|
||||
* 使用 PersonaNode + HAS_PERSONA 关系存储属性
|
||||
*/
|
||||
async personaUpdate(params: PersonaUpdateParams): Promise<PersonaResult> {
|
||||
try {
|
||||
// 1. 确保 PersonaNode 存在
|
||||
const personaTriplets: TripletData[] = [];
|
||||
const entityTypes: Record<string, string> = {};
|
||||
entityTypes[PERSONA_NODE_NAME] = PERSONA_NODE_TYPE;
|
||||
// 2. 逐个属性写入(作为关系),不使用 as any
|
||||
const toneVal = params.tone;
|
||||
const styleVal = params.style;
|
||||
const personalityVal = params.personality;
|
||||
const catchphraseVal = params.catchphrase;
|
||||
const backgroundVal = params.background;
|
||||
if (toneVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_TONE',
|
||||
object: toneVal
|
||||
});
|
||||
}
|
||||
if (styleVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_STYLE',
|
||||
object: styleVal
|
||||
});
|
||||
}
|
||||
if (personalityVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_PERSONALITY',
|
||||
object: personalityVal
|
||||
});
|
||||
}
|
||||
if (catchphraseVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_CATCHPHRASE',
|
||||
object: catchphraseVal
|
||||
});
|
||||
}
|
||||
if (backgroundVal) {
|
||||
personaTriplets.push({
|
||||
subject: PERSONA_NODE_NAME,
|
||||
relation: 'HAS_PERSONA_BACKGROUND',
|
||||
object: backgroundVal
|
||||
});
|
||||
}
|
||||
if (personaTriplets.length > 0) {
|
||||
await this.db.commit(personaTriplets, entityTypes);
|
||||
}
|
||||
const successResult: PersonaResult = {
|
||||
success: true,
|
||||
message: `已更新 ${personaTriplets.length} 个人设属性`
|
||||
};
|
||||
return successResult;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: PersonaResult = {
|
||||
success: false,
|
||||
message: `更新人设失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 清除人设
|
||||
* 对应 tools.persona_clear
|
||||
*/
|
||||
async personaClear(): Promise<PersonaResult> {
|
||||
try {
|
||||
await this.db.purge({ subject: PERSONA_NODE_NAME }, 'hard');
|
||||
const result: PersonaResult = { success: true, message: '已清除所有人设信息' };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: PersonaResult = {
|
||||
success: false,
|
||||
message: `清除人设失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除单条人设属性
|
||||
* 对应 tools.persona_remove
|
||||
*/
|
||||
async personaRemove(attribute: string): Promise<PersonaResult> {
|
||||
try {
|
||||
// 将 attribute 转为关系名格式
|
||||
const relationName = 'HAS_PERSONA_' + attribute.toUpperCase();
|
||||
await this.db.purge({ subject: PERSONA_NODE_NAME, relation: relationName }, 'hard');
|
||||
return {
|
||||
success: true,
|
||||
message: `已删除人设属性: ${attribute}`
|
||||
};
|
||||
}
|
||||
catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
message: `删除人设属性失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 查询当前人设
|
||||
*/
|
||||
async personaQuery(): Promise<PersonaQueryResult> {
|
||||
const result = await this.db.recall(PERSONA_NODE_NAME, [PERSONA_NODE_NAME], 2);
|
||||
const persona: Record<string, string> = {};
|
||||
for (const rel of result.relations) {
|
||||
if (rel.source === PERSONA_NODE_NAME && rel.type.startsWith('HAS_PERSONA_')) {
|
||||
const key = rel.type.replace('HAS_PERSONA_', '').toLowerCase();
|
||||
persona[key] = rel.target;
|
||||
}
|
||||
}
|
||||
const queryResult: PersonaQueryResult = {
|
||||
persona,
|
||||
found: Object.keys(persona).length > 0
|
||||
};
|
||||
return queryResult;
|
||||
}
|
||||
// ========= 任务管理 =========
|
||||
/**
|
||||
* 创建任务节点
|
||||
* 对应 tools.task_create
|
||||
*/
|
||||
async taskCreate(params: TaskCreateParams): Promise<TaskCreateResult> {
|
||||
try {
|
||||
const entityTypes: Record<string, string> = {};
|
||||
entityTypes[params.taskId] = 'TaskNode';
|
||||
const triplets: TripletData[] = [
|
||||
{ subject: params.taskId, relation: 'description', object: params.description },
|
||||
{ subject: params.taskId, relation: 'has_state', object: '进行中' }
|
||||
];
|
||||
if (params.infoNodes && params.infoNodes.length > 0) {
|
||||
for (const infoNode of params.infoNodes) {
|
||||
entityTypes[infoNode] = 'InfoNode';
|
||||
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: infoNode });
|
||||
}
|
||||
}
|
||||
await this.db.commit(triplets, entityTypes);
|
||||
const result: TaskCreateResult = {
|
||||
success: true,
|
||||
taskId: params.taskId,
|
||||
message: `已创建任务: ${params.taskId}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskCreateResult = {
|
||||
success: false,
|
||||
taskId: params.taskId,
|
||||
message: `创建任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 设置任务状态
|
||||
* 对应 tools.task_set_state
|
||||
*/
|
||||
async taskSetState(params: TaskSetStateParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 先删旧的 has_state 关系,再新建
|
||||
await this.db.purge({ subject: params.taskId, relation: 'has_state' }, 'soft');
|
||||
await this.db.commit([{ subject: params.taskId, relation: 'has_state', object: params.state }]);
|
||||
const result: TaskActionResult = {
|
||||
success: true,
|
||||
message: `任务 ${params.taskId} 状态已设为: ${params.state}`
|
||||
};
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `设置任务状态失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 删除任务
|
||||
* 对应 tools.task_delete
|
||||
*/
|
||||
async taskDelete(params: TaskDeleteParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 删除所有关联关系
|
||||
await this.db.purge({ subject: params.taskId }, 'hard');
|
||||
if (params.deleteInfoNodes !== false) {
|
||||
await this.db.purge({ target: params.taskId }, 'hard');
|
||||
}
|
||||
const result: TaskActionResult = { success: true, message: `已删除任务: ${params.taskId}` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `删除任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 关联信息节点到任务
|
||||
* 对应 tools.task_link_info
|
||||
*/
|
||||
async taskLinkInfo(params: TaskLinkInfoParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
const triplets: TripletData[] = [];
|
||||
const entityTypes: Record<string, string> = {};
|
||||
for (const nodeName of params.infoNodeNames) {
|
||||
entityTypes[nodeName] = 'InfoNode';
|
||||
triplets.push({ subject: params.taskId, relation: 'CONTAINS_INFO', object: nodeName });
|
||||
}
|
||||
await this.db.commit(triplets, entityTypes);
|
||||
const result: TaskActionResult = { success: true, message: `已关联 ${triplets.length} 个信息节点` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `关联信息节点失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 归档任务
|
||||
* 对应 tools.task_archive
|
||||
*/
|
||||
async taskArchive(params: TaskArchiveParams): Promise<TaskActionResult> {
|
||||
try {
|
||||
// 归档任务:将状态设为已归档(archived),同时写入完成摘要
|
||||
await this.taskSetState({ taskId: params.taskId, state: '已归档' });
|
||||
if (params.summary) {
|
||||
await this.db.commit([
|
||||
{ subject: params.taskId, relation: 'archive_summary', object: params.summary }
|
||||
]);
|
||||
}
|
||||
const result: TaskActionResult = { success: true, message: `已归档任务: ${params.taskId}` };
|
||||
return result;
|
||||
}
|
||||
catch (e) {
|
||||
const errorResult: TaskActionResult = {
|
||||
success: false,
|
||||
message: `归档任务失败: ${(e as Error).message || ''}`
|
||||
};
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 查询任务列表
|
||||
* 对应 tools.task_query
|
||||
*/
|
||||
async taskQuery(params?: TaskQueryParams): Promise<TaskQueryResult> {
|
||||
const limit: number = params?.limit ?? 10;
|
||||
const stateFilter: string | undefined = params?.stateFilter;
|
||||
const queryResult: TaskQueryResult = {
|
||||
tasks: await this.db.getRecentTasks(limit, stateFilter) as TaskInfo[],
|
||||
message: `找到 ${limit} 个任务`
|
||||
};
|
||||
return queryResult;
|
||||
}
|
||||
// ========= 图数据 =========
|
||||
/**
|
||||
* 获取用于 WebView 的完整图数据
|
||||
*/
|
||||
async getGraphDataForView(): Promise<GraphData> {
|
||||
// 用空关键词召回所有数据
|
||||
const recallResult = await this.db.recall('', [], 3);
|
||||
const nodes: NodeData[] = [];
|
||||
const edges: EdgeData[] = [];
|
||||
let nodeIdCounter = 1;
|
||||
const nameToId: Record<string, number> = {};
|
||||
for (const entity of recallResult.entities) {
|
||||
const id = nodeIdCounter;
|
||||
nodeIdCounter++;
|
||||
nameToId[entity.name] = id;
|
||||
const node: NodeData = {
|
||||
id,
|
||||
label: entity.name,
|
||||
type: entity.type,
|
||||
mentions: entity.mention_count
|
||||
};
|
||||
nodes.push(node);
|
||||
}
|
||||
for (const rel of recallResult.relations) {
|
||||
const from = nameToId[rel.source];
|
||||
const to = nameToId[rel.target];
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const edge: EdgeData = {
|
||||
from,
|
||||
to,
|
||||
label: rel.type,
|
||||
weight: rel.confidence
|
||||
};
|
||||
edges.push(edge);
|
||||
}
|
||||
}
|
||||
const graphData: GraphData = { nodes, edges };
|
||||
return graphData;
|
||||
}
|
||||
/**
|
||||
* 查询节点的完整信息 — 自身属性 + 所有相连关系
|
||||
*/
|
||||
async getNodeDetail(nodeName: string): Promise<NodeDetailInfo | null> {
|
||||
try {
|
||||
const recallResult = await this.db.recall(nodeName, [nodeName], 1);
|
||||
if (recallResult.entities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const entity = recallResult.entities[0];
|
||||
const connections: ConnectionItem[] = [];
|
||||
let connectionCount = 0;
|
||||
for (const rel of recallResult.relations) {
|
||||
if (rel.source === nodeName) {
|
||||
const conn: ConnectionItem = {
|
||||
type: rel.type,
|
||||
target_name: rel.target
|
||||
};
|
||||
connections.push(conn);
|
||||
connectionCount++;
|
||||
}
|
||||
else if (rel.target === nodeName) {
|
||||
const conn: ConnectionItem = {
|
||||
type: rel.type + ' (反向)',
|
||||
target_name: rel.source
|
||||
};
|
||||
connections.push(conn);
|
||||
connectionCount++;
|
||||
}
|
||||
}
|
||||
const detail: NodeDetailInfo = {
|
||||
name: entity.name,
|
||||
type: entity.type,
|
||||
mention_count: entity.mention_count,
|
||||
connection_count: connectionCount,
|
||||
connections
|
||||
};
|
||||
return detail;
|
||||
}
|
||||
catch (err) {
|
||||
defaultLogger.error('getNodeDetail error: ' + JSON.stringify(err));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 返回带连接度数的图数据(每个节点增加 degree 字段)
|
||||
*/
|
||||
async getJoinedData(): Promise<GraphData> {
|
||||
const graphData = await this.getGraphDataForView();
|
||||
// 计算每个节点的连接度数
|
||||
const degreeMap: Record<number, number> = {};
|
||||
for (const edge of graphData.edges) {
|
||||
degreeMap[edge.from] = (degreeMap[edge.from] || 0) + 1;
|
||||
degreeMap[edge.to] = (degreeMap[edge.to] || 0) + 1;
|
||||
}
|
||||
// 手动为节点附加 degree(ArkTS 不支持展开运算符)
|
||||
const nodesWithDegree: NodeData[] = [];
|
||||
for (let i = 0; i < graphData.nodes.length; i++) {
|
||||
const orig = graphData.nodes[i];
|
||||
const copy: NodeData = {
|
||||
id: orig.id,
|
||||
label: orig.label,
|
||||
type: orig.type,
|
||||
mentions: orig.mentions
|
||||
};
|
||||
nodesWithDegree.push(copy);
|
||||
}
|
||||
const graphResult: GraphData = {
|
||||
nodes: nodesWithDegree,
|
||||
edges: graphData.edges
|
||||
};
|
||||
return graphResult;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,43 @@
|
||||
export enum WidthBreakpoint {
|
||||
WIDTH_XS = "xs",
|
||||
WIDTH_SM = "sm",
|
||||
WIDTH_MD = "md",
|
||||
WIDTH_LG = "lg",
|
||||
WIDTH_XL = "xl"
|
||||
}
|
||||
export interface BreakpointTypes<T> {
|
||||
xs?: T;
|
||||
sm: T;
|
||||
md: T;
|
||||
lg: T;
|
||||
xl?: T;
|
||||
}
|
||||
export class BreakpointType<T> {
|
||||
private xs: T;
|
||||
private sm: T;
|
||||
private md: T;
|
||||
private lg: T;
|
||||
private xl: T;
|
||||
public constructor(param: BreakpointTypes<T>) {
|
||||
this.xs = param.xs ?? param.sm;
|
||||
this.sm = param.sm;
|
||||
this.md = param.md;
|
||||
this.lg = param.lg;
|
||||
this.xl = param.xl ?? param.lg;
|
||||
}
|
||||
public getValue(currentBreakpoint: WidthBreakpoint): T {
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_XS) {
|
||||
return this.xs;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_SM) {
|
||||
return this.sm;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_MD) {
|
||||
return this.md;
|
||||
}
|
||||
if (currentBreakpoint === WidthBreakpoint.WIDTH_XL) {
|
||||
return this.xl;
|
||||
}
|
||||
return this.lg;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
import hilog from "@ohos:hilog";
|
||||
class Logger {
|
||||
private domain: number;
|
||||
private prefix: string;
|
||||
private format: string = "%{public}s, %{public}s";
|
||||
public constructor(prefix: string) {
|
||||
this.prefix = prefix;
|
||||
this.domain = 0xFF00;
|
||||
}
|
||||
public debug(...args: Object[]): void {
|
||||
hilog.debug(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public info(...args: Object[]): void {
|
||||
hilog.info(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public warn(...args: Object[]): void {
|
||||
hilog.warn(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
public error(...args: Object[]): void {
|
||||
hilog.error(this.domain, this.prefix, this.format, args);
|
||||
}
|
||||
}
|
||||
export const defaultLogger = new Logger("[TrulyMEM]");
|
||||
export default defaultLogger;
|
||||
Binary file not shown.
@ -0,0 +1,42 @@
|
||||
import { WidthBreakpoint } from "@normalized:N&&&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0";
|
||||
export interface VMEvent {
|
||||
}
|
||||
export class BaseViewModel {
|
||||
protected isAttached: boolean = false;
|
||||
protected isDisposed: boolean = false;
|
||||
protected currentBreakpoint: WidthBreakpoint = WidthBreakpoint.WIDTH_MD;
|
||||
attach(): void {
|
||||
if (this.isAttached) {
|
||||
return;
|
||||
}
|
||||
this.isAttached = true;
|
||||
this.onAttach();
|
||||
}
|
||||
detach(): void {
|
||||
if (!this.isAttached) {
|
||||
return;
|
||||
}
|
||||
this.isAttached = false;
|
||||
this.onDetach();
|
||||
}
|
||||
dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this.detach();
|
||||
this.onDispose();
|
||||
}
|
||||
protected onAttach(): void {
|
||||
}
|
||||
protected onDetach(): void {
|
||||
}
|
||||
protected onDispose(): void {
|
||||
}
|
||||
public get attached(): boolean {
|
||||
return this.isAttached;
|
||||
}
|
||||
public get disposed(): boolean {
|
||||
return this.isDisposed;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{"hspPkgNames":[],"compileEntries":["&@ohos/common/Index&1.0.0","&@ohos/common/src/main/ets/component/ImmersiveTabNavigation&1.0.0","&@ohos/common/src/main/ets/constant/TrulyMEMConstants&1.0.0","&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0","&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0","&@ohos/common/src/main/ets/service/AIAgentService&1.0.0","&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0","&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0","&@ohos/common/src/main/ets/util/Logger&1.0.0","&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0","&@ohos/graph/Index&1.0.0","&@ohos/graph/src/main/ets/components/GraphComponents&1.0.0","&@ohos/graph/src/main/ets/pages/GraphPage&1.0.0","&@ohos/chat/Index&1.0.0","&@ohos/chat/src/main/ets/components/ChatComponents&1.0.0","&@ohos/chat/src/main/ets/pages/ChatPage&1.0.0","&@ohos/settings/Index&1.0.0","&@ohos/settings/src/main/ets/components/SettingsComponents&1.0.0","&@ohos/settings/src/main/ets/pages/SettingsPage&1.0.0","&@ohos/phone/src/main/ets/pages/MainPage&","&phone/build/generated/r/ResourceTable&","&@ohos/phone/src/main/ets/entryability/EntryAbility&","&@ohos/phone/src/main/ets/pages/SplashPage&","&@ohos/phone/src/main/ets/pages/Index&"],"updateVersionInfo":{}}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
<EFBFBD>r@<40>
|
||||
Binary file not shown.
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/dep_info.json
vendored
Normal file
1
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/dep_info.json
vendored
Normal file
@ -0,0 +1 @@
|
||||
{"resolveConflictMode":true,"depName2RootPath":{"@ohos/common":"/home/program/TrulyMEM-TrueHumanMEM/common","@ohos/graph":"/home/program/TrulyMEM-TrueHumanMEM/features/graph","@ohos/chat":"/home/program/TrulyMEM-TrueHumanMEM/features/chat","@ohos/settings":"/home/program/TrulyMEM-TrueHumanMEM/features/settings","@ohos/hypium":"/home/program/TrulyMEM-TrueHumanMEM/oh_modules/.ohpm/@ohos+hypium@1.0.24/oh_modules/@ohos/hypium","@ohos/hamock":"/home/program/TrulyMEM-TrueHumanMEM/oh_modules/.ohpm/@ohos+hamock@1.0.0/oh_modules/@ohos/hamock"},"depName2DepInfo":{"@ohos/common":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/common","pkgName":"@ohos/common","pkgVersion":"1.0.0"},"@ohos/graph":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/features/graph","pkgName":"@ohos/graph","pkgVersion":"1.0.0"},"@ohos/chat":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/features/chat","pkgName":"@ohos/chat","pkgVersion":"1.0.0"},"@ohos/settings":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/features/settings","pkgName":"@ohos/settings","pkgVersion":"1.0.0"},"@ohos/hypium":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/oh_modules/.ohpm/@ohos+hypium@1.0.24/oh_modules/@ohos/hypium","pkgName":"@ohos/hypium","pkgVersion":"1.0.24"},"@ohos/hamock":{"dependencyType":"har","isByteCodeHar":false,"pkgRootPath":"/home/program/TrulyMEM-TrueHumanMEM/oh_modules/.ohpm/@ohos+hamock@1.0.0/oh_modules/@ohos/hamock","pkgName":"@ohos/hamock","pkgVersion":"1.0.0"}}}
|
||||
24
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/filesInfo.txt
vendored
Normal file
24
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/filesInfo.txt
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts;&@ohos/common/Index&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|Index.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/constant/TrulyMEMConstants.ts;&@ohos/common/src/main/ets/constant/TrulyMEMConstants&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/constant/TrulyMEMConstants.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/component/ImmersiveTabNavigation.ts;&@ohos/common/src/main/ets/component/ImmersiveTabNavigation&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/component/ImmersiveTabNavigation.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/routermanager/PageContext.ts;&@ohos/common/src/main/ets/routermanager/PageContext&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/routermanager/PageContext.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/AIAgentService.ts;&@ohos/common/src/main/ets/service/AIAgentService&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/service/AIAgentService.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/GraphMemoryService.ts;&@ohos/common/src/main/ets/service/GraphMemoryService&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/service/GraphMemoryService.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/BreakpointSystem.ts;&@ohos/common/src/main/ets/util/BreakpointSystem&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/util/BreakpointSystem.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/Logger.ts;&@ohos/common/src/main/ets/util/Logger&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/util/Logger.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/viewmodel/BaseViewModel.ts;&@ohos/common/src/main/ets/viewmodel/BaseViewModel&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/viewmodel/BaseViewModel.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/model/GraphDatabase.ts;&@ohos/common/src/main/ets/model/GraphDatabase&1.0.0;esm;@ohos/phone|@ohos/common|1.0.0|src/main/ets/model/GraphDatabase.ts;@ohos/common;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/Index.ts;&@ohos/graph/Index&1.0.0;esm;@ohos/phone|@ohos/graph|1.0.0|Index.ts;@ohos/graph;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/pages/GraphPage.ts;&@ohos/graph/src/main/ets/pages/GraphPage&1.0.0;esm;@ohos/phone|@ohos/graph|1.0.0|src/main/ets/pages/GraphPage.ts;@ohos/graph;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/components/GraphComponents.ts;&@ohos/graph/src/main/ets/components/GraphComponents&1.0.0;esm;@ohos/phone|@ohos/graph|1.0.0|src/main/ets/components/GraphComponents.ts;@ohos/graph;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts;&@ohos/chat/Index&1.0.0;esm;@ohos/phone|@ohos/chat|1.0.0|Index.ts;@ohos/chat;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/pages/ChatPage.ts;&@ohos/chat/src/main/ets/pages/ChatPage&1.0.0;esm;@ohos/phone|@ohos/chat|1.0.0|src/main/ets/pages/ChatPage.ts;@ohos/chat;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/components/ChatComponents.ts;&@ohos/chat/src/main/ets/components/ChatComponents&1.0.0;esm;@ohos/phone|@ohos/chat|1.0.0|src/main/ets/components/ChatComponents.ts;@ohos/chat;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/Index.ts;&@ohos/settings/Index&1.0.0;esm;@ohos/phone|@ohos/settings|1.0.0|Index.ts;@ohos/settings;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/components/SettingsComponents.ts;&@ohos/settings/src/main/ets/components/SettingsComponents&1.0.0;esm;@ohos/phone|@ohos/settings|1.0.0|src/main/ets/components/SettingsComponents.ts;@ohos/settings;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/pages/SettingsPage.ts;&@ohos/settings/src/main/ets/pages/SettingsPage&1.0.0;esm;@ohos/phone|@ohos/settings|1.0.0|src/main/ets/pages/SettingsPage.ts;@ohos/settings;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/MainPage.ts;&@ohos/phone/src/main/ets/pages/MainPage&;esm;@ohos/phone|@ohos/phone|1.0.0|src/main/ets/pages/MainPage.ts;@ohos/phone;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/build/default/generated/r/default/ResourceTable.js;&phone/build/generated/r/ResourceTable&;esm;@ohos/phone|@ohos/phone|1.0.0|build/default/generated/r/default/ResourceTable.js;@ohos/phone;false;ts
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/entryability/EntryAbility.ts;&@ohos/phone/src/main/ets/entryability/EntryAbility&;esm;@ohos/phone|@ohos/phone|1.0.0|src/main/ets/entryability/EntryAbility.ts;@ohos/phone;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/SplashPage.ts;&@ohos/phone/src/main/ets/pages/SplashPage&;esm;@ohos/phone|@ohos/phone|1.0.0|src/main/ets/pages/SplashPage.ts;@ohos/phone;false;ets
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/Index.ts;&@ohos/phone/src/main/ets/pages/Index&;esm;@ohos/phone|@ohos/phone|1.0.0|src/main/ets/pages/Index.ts;@ohos/phone;false;ets
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/Index.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/Index.protoBin
vendored
Normal file
Binary file not shown.
@ -0,0 +1 @@
|
||||
export { GraphPage } from "@normalized:N&&&@ohos/graph/src/main/ets/pages/GraphPage&1.0.0";
|
||||
Binary file not shown.
@ -0,0 +1,390 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface GraphWebView_Params {
|
||||
controller?: web_webview.WebviewController;
|
||||
bridge?: NativeBridge;
|
||||
onPageEnd?: () => void;
|
||||
}
|
||||
interface NodeDetailPanel_Params {
|
||||
detail?: NodeDetailInfo;
|
||||
onClose?: () => void;
|
||||
}
|
||||
interface GraphNodeSearchBar_Params {
|
||||
searchText?: string;
|
||||
onSearchInput?: (value: string) => void;
|
||||
}
|
||||
import web_webview from "@ohos:web.webview";
|
||||
import { Logger } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import type { GraphDatabase, RecallEntity, ConnectionItem, NodeDetailInfo } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
export class GraphNodeSearchBar extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__searchText = new SynchedPropertySimpleTwoWayPU(params.searchText, this, "searchText");
|
||||
this.onSearchInput = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: GraphNodeSearchBar_Params) {
|
||||
if (params.onSearchInput !== undefined) {
|
||||
this.onSearchInput = params.onSearchInput;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: GraphNodeSearchBar_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__searchText.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__searchText.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __searchText: SynchedPropertySimpleTwoWayPU<string>;
|
||||
get searchText() {
|
||||
return this.__searchText.get();
|
||||
}
|
||||
set searchText(newValue: string) {
|
||||
this.__searchText.set(newValue);
|
||||
}
|
||||
private onSearchInput?: (value: string) => void;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.position({ x: 0, y: 0 });
|
||||
Column.zIndex(10);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
TextInput.create({ placeholder: '搜索节点...', text: this.searchText });
|
||||
TextInput.width('80%');
|
||||
TextInput.height(40);
|
||||
TextInput.backgroundColor('rgba(10, 10, 26, 0.8)');
|
||||
TextInput.fontColor('#ffffff');
|
||||
TextInput.placeholderColor('#666688');
|
||||
TextInput.borderRadius(8);
|
||||
TextInput.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' });
|
||||
TextInput.margin({ top: 20 });
|
||||
TextInput.onChange((value: string) => {
|
||||
this.onSearchInput?.(value);
|
||||
});
|
||||
}, TextInput);
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class NodeDetailPanel extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.__detail = new SynchedPropertyObjectOneWayPU(params.detail, this, "detail");
|
||||
this.onClose = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: NodeDetailPanel_Params) {
|
||||
if (params.onClose !== undefined) {
|
||||
this.onClose = params.onClose;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: NodeDetailPanel_Params) {
|
||||
this.__detail.reset(params.detail);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__detail.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__detail.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private __detail: SynchedPropertySimpleOneWayPU<NodeDetailInfo>;
|
||||
get detail() {
|
||||
return this.__detail.get();
|
||||
}
|
||||
set detail(newValue: NodeDetailInfo) {
|
||||
this.__detail.set(newValue);
|
||||
}
|
||||
private onClose?: () => void;
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.width('100%');
|
||||
Column.height('100%');
|
||||
Column.backgroundColor('rgba(0, 0, 0, 0.5)');
|
||||
Column.justifyContent(FlexAlign.Center);
|
||||
Column.alignItems(HorizontalAlign.Center);
|
||||
Column.zIndex(20);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Column.create();
|
||||
Column.padding(20);
|
||||
Column.backgroundColor('rgba(10, 10, 26, 0.95)');
|
||||
Column.borderRadius(12);
|
||||
Column.border({ width: 1, color: 'rgba(100, 100, 255, 0.3)' });
|
||||
Column.width(300);
|
||||
}, Column);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(this.detail.name);
|
||||
Text.fontSize(18);
|
||||
Text.fontColor('#44ff88');
|
||||
Text.fontWeight(FontWeight.Bold);
|
||||
Text.margin({ bottom: 10 });
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('类型: ' + this.detail.type);
|
||||
Text.fontSize(14);
|
||||
Text.fontColor('#aaaacc');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('提及次数: ' + this.detail.mention_count);
|
||||
Text.fontSize(14);
|
||||
Text.fontColor('#aaaacc');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('连接数: ' + this.detail.connection_count);
|
||||
Text.fontSize(14);
|
||||
Text.fontColor('#aaaacc');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
if (this.detail.connections && this.detail.connections.length > 0) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create('连接关系:');
|
||||
Text.fontSize(14);
|
||||
Text.fontColor('#8888aa');
|
||||
Text.margin({ top: 10, bottom: 5 });
|
||||
}, Text);
|
||||
Text.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
List.create();
|
||||
List.height(100);
|
||||
}, List);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
ForEach.create();
|
||||
const forEachItemGenFunction = _item => {
|
||||
const conn = _item;
|
||||
{
|
||||
const itemCreation = (elmtId, isInitialRender) => {
|
||||
ViewStackProcessor.StartGetAccessRecordingFor(elmtId);
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
if (!isInitialRender) {
|
||||
ListItem.pop();
|
||||
}
|
||||
ViewStackProcessor.StopGetAccessRecording();
|
||||
};
|
||||
const itemCreation2 = (elmtId, isInitialRender) => {
|
||||
ListItem.create(deepRenderFunction, true);
|
||||
};
|
||||
const deepRenderFunction = (elmtId, isInitialRender) => {
|
||||
itemCreation(elmtId, isInitialRender);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Text.create(conn.type + ': ' + conn.target_name);
|
||||
Text.fontSize(12);
|
||||
Text.fontColor('#aaaacc');
|
||||
}, Text);
|
||||
Text.pop();
|
||||
ListItem.pop();
|
||||
};
|
||||
this.observeComponentCreation2(itemCreation2, ListItem);
|
||||
ListItem.pop();
|
||||
}
|
||||
};
|
||||
this.forEachUpdateFunction(elmtId, this.detail.connections, forEachItemGenFunction);
|
||||
}, ForEach);
|
||||
ForEach.pop();
|
||||
List.pop();
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Button.createWithLabel('关闭');
|
||||
Button.width(80);
|
||||
Button.height(30);
|
||||
Button.margin({ top: 15 });
|
||||
Button.backgroundColor('rgba(100, 100, 255, 0.3)');
|
||||
Button.fontColor('#ffffff');
|
||||
Button.onClick(() => {
|
||||
this.onClose?.();
|
||||
});
|
||||
}, Button);
|
||||
Button.pop();
|
||||
Column.pop();
|
||||
Column.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
export class GraphWebView extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.controller = new web_webview.WebviewController();
|
||||
this.bridge = undefined;
|
||||
this.onPageEnd = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: GraphWebView_Params) {
|
||||
if (params.controller !== undefined) {
|
||||
this.controller = params.controller;
|
||||
}
|
||||
if (params.bridge !== undefined) {
|
||||
this.bridge = params.bridge;
|
||||
}
|
||||
if (params.onPageEnd !== undefined) {
|
||||
this.onPageEnd = params.onPageEnd;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: GraphWebView_Params) {
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private controller: web_webview.WebviewController;
|
||||
private bridge?: NativeBridge;
|
||||
private onPageEnd?: () => void;
|
||||
getController(): web_webview.WebviewController {
|
||||
return this.controller;
|
||||
}
|
||||
setBridge(bridge: NativeBridge): void {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Web.create({ src: { "id": 0, "type": 30000, params: ['graph.html'], "bundleName": "com.trulymem.app", "moduleName": "phone" }, controller: this.controller });
|
||||
Web.javaScriptAccess(true);
|
||||
Web.width('100%');
|
||||
Web.height('100%');
|
||||
Web.zoomAccess(true);
|
||||
Web.onPageEnd(() => {
|
||||
this.onPageEnd?.();
|
||||
});
|
||||
Web.javaScriptProxy({
|
||||
object: this.bridge,
|
||||
name: 'nativeBridge',
|
||||
methodList: ['onNodeClick', 'onSearch'],
|
||||
asyncMethodList: ['requestGraphData'],
|
||||
controller: this.controller
|
||||
});
|
||||
}, Web);
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* NativeBridge — WebView 原生桥接类(移动自 GraphPage)
|
||||
* 负责 ArkTS ↔ WebView JavaScript 双向通信
|
||||
*/
|
||||
export class NativeBridge {
|
||||
private controller: web_webview.WebviewController;
|
||||
private onRequestGraphData: () => void;
|
||||
private onNodeClickCallback: (nodeId: number, nodeName: string) => void;
|
||||
private onSearchCallback: (query: string) => void;
|
||||
constructor(controller: web_webview.WebviewController, onRequestGraphData: () => void, onNodeClickCallback: (nodeId: number, nodeName: string) => void, onSearchCallback: (query: string) => void) {
|
||||
this.controller = controller;
|
||||
this.onRequestGraphData = onRequestGraphData;
|
||||
this.onNodeClickCallback = onNodeClickCallback;
|
||||
this.onSearchCallback = onSearchCallback;
|
||||
}
|
||||
onNodeClick(nodeId: number, nodeName: string): void {
|
||||
Logger.info('Node clicked: id=' + nodeId + ', name=' + nodeName);
|
||||
if (this.onNodeClickCallback) {
|
||||
this.onNodeClickCallback(nodeId, nodeName);
|
||||
}
|
||||
}
|
||||
onSearch(query: string): void {
|
||||
Logger.info('Search from WebView: ' + query);
|
||||
if (this.onSearchCallback) {
|
||||
this.onSearchCallback(query);
|
||||
}
|
||||
}
|
||||
requestGraphData(): void {
|
||||
Logger.info('requestGraphData called from WebView');
|
||||
if (this.onRequestGraphData) {
|
||||
this.onRequestGraphData();
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* GraphDataService — 图数据查询服务
|
||||
* 封装从 GraphDatabase 读取节点和边的逻辑
|
||||
*/
|
||||
export class GraphDataService {
|
||||
private db: GraphDatabase;
|
||||
constructor(db: GraphDatabase) {
|
||||
this.db = db;
|
||||
}
|
||||
async getAllNodes(): Promise<GraphNodeItem[]> {
|
||||
const result = await this.db.search('');
|
||||
return result.map((r, idx): GraphNodeItem => {
|
||||
return {
|
||||
id: idx + 1,
|
||||
label: r.name,
|
||||
type: r.type,
|
||||
mentions: r.mentions
|
||||
};
|
||||
});
|
||||
}
|
||||
async getAllEdges(): Promise<GraphEdgeItem[]> {
|
||||
const recallResult = await this.db.recall('', [], 3);
|
||||
const nameToId: Record<string, number> = {};
|
||||
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
|
||||
nameToId[e.name as string] = idx + 1;
|
||||
});
|
||||
const edgeItems: GraphEdgeItem[] = [];
|
||||
for (let i = 0; i < recallResult.relations.length; i++) {
|
||||
const r = recallResult.relations[i];
|
||||
const sourceId = nameToId[r.source];
|
||||
const targetId = nameToId[r.target];
|
||||
if (sourceId !== undefined && targetId !== undefined) {
|
||||
edgeItems.push({
|
||||
id: i + 1,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
label: r.type,
|
||||
relation: r.type
|
||||
});
|
||||
}
|
||||
}
|
||||
return edgeItems;
|
||||
}
|
||||
}
|
||||
// ========= 内部类型定义 =========
|
||||
interface GraphNodeItem {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
interface GraphEdgeItem {
|
||||
id: number;
|
||||
source: number;
|
||||
target: number;
|
||||
label: string;
|
||||
relation: string;
|
||||
}
|
||||
Binary file not shown.
@ -0,0 +1,341 @@
|
||||
if (!("finalizeConstruction" in ViewPU.prototype)) {
|
||||
Reflect.set(ViewPU.prototype, "finalizeConstruction", () => { });
|
||||
}
|
||||
interface GraphPage_Params {
|
||||
controller?: web_webview.WebviewController;
|
||||
db?: GraphDatabase;
|
||||
nodeCount?: number;
|
||||
edgeCount?: number;
|
||||
selectedNodeDetail?: NodeDetailInfo | null;
|
||||
showNodeDetail?: boolean;
|
||||
searchText?: string;
|
||||
graphService?: GraphMemoryService;
|
||||
bridge?: NativeBridge;
|
||||
}
|
||||
import web_webview from "@ohos:web.webview";
|
||||
import { Logger, GraphMemoryService } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import type { GraphDatabase, NodeDetailInfo, RecallEntity } from "@normalized:N&&&@ohos/common/Index&1.0.0";
|
||||
import { GraphNodeSearchBar, NodeDetailPanel, NativeBridge } from "@normalized:N&&&@ohos/graph/src/main/ets/components/GraphComponents&1.0.0";
|
||||
// ========= GraphPage 组件 =========
|
||||
interface GraphNodeItem {
|
||||
id: number;
|
||||
label: string;
|
||||
type: string;
|
||||
mentions: number;
|
||||
}
|
||||
interface GraphEdgeItem {
|
||||
id: number;
|
||||
source: number;
|
||||
target: number;
|
||||
label: string;
|
||||
relation: string;
|
||||
}
|
||||
export class GraphPage extends ViewPU {
|
||||
constructor(parent, params, __localStorage, elmtId = -1, paramsLambda = undefined, extraInfo) {
|
||||
super(parent, __localStorage, elmtId, extraInfo);
|
||||
if (typeof paramsLambda === "function") {
|
||||
this.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
this.controller = new web_webview.WebviewController();
|
||||
this.__db = new SynchedPropertyObjectOneWayPU(params.db, this, "db");
|
||||
this.__nodeCount = new ObservedPropertySimplePU(0, this, "nodeCount");
|
||||
this.__edgeCount = new ObservedPropertySimplePU(0, this, "edgeCount");
|
||||
this.__selectedNodeDetail = new ObservedPropertyObjectPU(null, this, "selectedNodeDetail");
|
||||
this.__showNodeDetail = new ObservedPropertySimplePU(false, this, "showNodeDetail");
|
||||
this.__searchText = new ObservedPropertySimplePU('', this, "searchText");
|
||||
this.graphService = undefined;
|
||||
this.bridge = undefined;
|
||||
this.setInitiallyProvidedValue(params);
|
||||
this.finalizeConstruction();
|
||||
}
|
||||
setInitiallyProvidedValue(params: GraphPage_Params) {
|
||||
if (params.controller !== undefined) {
|
||||
this.controller = params.controller;
|
||||
}
|
||||
if (params.nodeCount !== undefined) {
|
||||
this.nodeCount = params.nodeCount;
|
||||
}
|
||||
if (params.edgeCount !== undefined) {
|
||||
this.edgeCount = params.edgeCount;
|
||||
}
|
||||
if (params.selectedNodeDetail !== undefined) {
|
||||
this.selectedNodeDetail = params.selectedNodeDetail;
|
||||
}
|
||||
if (params.showNodeDetail !== undefined) {
|
||||
this.showNodeDetail = params.showNodeDetail;
|
||||
}
|
||||
if (params.searchText !== undefined) {
|
||||
this.searchText = params.searchText;
|
||||
}
|
||||
if (params.graphService !== undefined) {
|
||||
this.graphService = params.graphService;
|
||||
}
|
||||
if (params.bridge !== undefined) {
|
||||
this.bridge = params.bridge;
|
||||
}
|
||||
}
|
||||
updateStateVars(params: GraphPage_Params) {
|
||||
this.__db.reset(params.db);
|
||||
}
|
||||
purgeVariableDependenciesOnElmtId(rmElmtId) {
|
||||
this.__db.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__nodeCount.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__edgeCount.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__selectedNodeDetail.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__showNodeDetail.purgeDependencyOnElmtId(rmElmtId);
|
||||
this.__searchText.purgeDependencyOnElmtId(rmElmtId);
|
||||
}
|
||||
aboutToBeDeleted() {
|
||||
this.__db.aboutToBeDeleted();
|
||||
this.__nodeCount.aboutToBeDeleted();
|
||||
this.__edgeCount.aboutToBeDeleted();
|
||||
this.__selectedNodeDetail.aboutToBeDeleted();
|
||||
this.__showNodeDetail.aboutToBeDeleted();
|
||||
this.__searchText.aboutToBeDeleted();
|
||||
SubscriberManager.Get().delete(this.id__());
|
||||
this.aboutToBeDeletedInternal();
|
||||
}
|
||||
private controller: web_webview.WebviewController;
|
||||
private __db: SynchedPropertySimpleOneWayPU<GraphDatabase>;
|
||||
get db() {
|
||||
return this.__db.get();
|
||||
}
|
||||
set db(newValue: GraphDatabase) {
|
||||
this.__db.set(newValue);
|
||||
}
|
||||
private __nodeCount: ObservedPropertySimplePU<number>;
|
||||
get nodeCount() {
|
||||
return this.__nodeCount.get();
|
||||
}
|
||||
set nodeCount(newValue: number) {
|
||||
this.__nodeCount.set(newValue);
|
||||
}
|
||||
private __edgeCount: ObservedPropertySimplePU<number>;
|
||||
get edgeCount() {
|
||||
return this.__edgeCount.get();
|
||||
}
|
||||
set edgeCount(newValue: number) {
|
||||
this.__edgeCount.set(newValue);
|
||||
}
|
||||
private __selectedNodeDetail: ObservedPropertyObjectPU<NodeDetailInfo | null>;
|
||||
get selectedNodeDetail() {
|
||||
return this.__selectedNodeDetail.get();
|
||||
}
|
||||
set selectedNodeDetail(newValue: NodeDetailInfo | null) {
|
||||
this.__selectedNodeDetail.set(newValue);
|
||||
}
|
||||
private __showNodeDetail: ObservedPropertySimplePU<boolean>;
|
||||
get showNodeDetail() {
|
||||
return this.__showNodeDetail.get();
|
||||
}
|
||||
set showNodeDetail(newValue: boolean) {
|
||||
this.__showNodeDetail.set(newValue);
|
||||
}
|
||||
private __searchText: ObservedPropertySimplePU<string>;
|
||||
get searchText() {
|
||||
return this.__searchText.get();
|
||||
}
|
||||
set searchText(newValue: string) {
|
||||
this.__searchText.set(newValue);
|
||||
}
|
||||
private graphService: GraphMemoryService;
|
||||
private bridge: NativeBridge;
|
||||
aboutToAppear() {
|
||||
this.graphService = new GraphMemoryService(this.db);
|
||||
this.bridge = new NativeBridge(this.controller, (): void => { this.pushGraphDataToWebView(); }, (nodeId: number, nodeName: string): void => { this.handleNodeClick(nodeId, nodeName); }, (query: string): void => { this.handleSearchFromWeb(query); });
|
||||
}
|
||||
/**
|
||||
* 外部触发刷新图数据(聊天写入新记忆后调用)
|
||||
*/
|
||||
public async refreshGraphData(): Promise<void> {
|
||||
await this.pushGraphDataToWebView();
|
||||
}
|
||||
/**
|
||||
* 处理节点点击 - 查询详细信息并显示浮层
|
||||
*/
|
||||
private async handleNodeClick(nodeId: number, nodeName: string): Promise<void> {
|
||||
try {
|
||||
if (!this.graphService)
|
||||
return;
|
||||
const detail: NodeDetailInfo | null = await this.graphService.getNodeDetail(nodeName);
|
||||
if (detail) {
|
||||
this.selectedNodeDetail = detail;
|
||||
this.showNodeDetail = true;
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
Logger.error('handleNodeClick error: ' + JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 处理来自 WebView 的搜索请求
|
||||
*/
|
||||
private handleSearchFromWeb(query: string): void {
|
||||
this.searchText = query;
|
||||
}
|
||||
/**
|
||||
* 处理搜索输入 - 通知 WebView 过滤
|
||||
*/
|
||||
private onSearchInput(value: string): void {
|
||||
this.searchText = value;
|
||||
const escapedValue = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');
|
||||
const jsCode = `window.dispatchEvent(new MessageEvent('message', { data: { type: 'search_nodes', query: '${escapedValue}' } }));`;
|
||||
this.controller.runJavaScript(jsCode);
|
||||
}
|
||||
/**
|
||||
* 关闭节点详情浮层
|
||||
*/
|
||||
private closeNodeDetail(): void {
|
||||
this.showNodeDetail = false;
|
||||
this.selectedNodeDetail = null;
|
||||
}
|
||||
/**
|
||||
* 从数据库读取全量图数据,推送给 WebView
|
||||
*/
|
||||
private async getAllNodesData(): Promise<GraphNodeItem[]> {
|
||||
const result = await this.db.search('');
|
||||
return result.map((r, idx): GraphNodeItem => {
|
||||
return {
|
||||
id: idx + 1,
|
||||
label: r.name,
|
||||
type: r.type,
|
||||
mentions: r.mentions
|
||||
};
|
||||
});
|
||||
}
|
||||
private async getAllEdgesData(): Promise<GraphEdgeItem[]> {
|
||||
const recallResult = await this.db.recall('', [], 3);
|
||||
const nameToId: Record<string, number> = {};
|
||||
recallResult.entities.forEach((e: RecallEntity, idx: number): void => {
|
||||
nameToId[e.name as string] = idx + 1;
|
||||
});
|
||||
const edgeItems: GraphEdgeItem[] = [];
|
||||
for (let i = 0; i < recallResult.relations.length; i++) {
|
||||
const r = recallResult.relations[i];
|
||||
const sourceId: number | undefined = nameToId[r.source];
|
||||
const targetId: number | undefined = nameToId[r.target];
|
||||
if (sourceId !== undefined && targetId !== undefined) {
|
||||
edgeItems.push({
|
||||
id: i + 1,
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
label: r.type,
|
||||
relation: r.type
|
||||
});
|
||||
}
|
||||
}
|
||||
return edgeItems;
|
||||
}
|
||||
/**
|
||||
* 从数据库读取全量图数据,推送给 WebView
|
||||
*/
|
||||
private async pushGraphDataToWebView(): Promise<void> {
|
||||
try {
|
||||
const allNodes: GraphNodeItem[] = await this.getAllNodesData();
|
||||
const allEdges: GraphEdgeItem[] = await this.getAllEdgesData();
|
||||
if (this.controller) {
|
||||
const jsCode: string = `window.loadGraphData(${JSON.stringify({ nodes: allNodes, edges: allEdges })});`;
|
||||
this.controller.runJavaScript(jsCode);
|
||||
}
|
||||
this.nodeCount = allNodes.length;
|
||||
this.edgeCount = allEdges.length;
|
||||
}
|
||||
catch (err) {
|
||||
Logger.error('pushGraphDataToWebView error: ' + JSON.stringify(err));
|
||||
}
|
||||
}
|
||||
initialRender() {
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
Stack.create();
|
||||
Stack.width('100%');
|
||||
Stack.height('100%');
|
||||
}, Stack);
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
// WebView 显示 3D 星图
|
||||
Web.create({ src: { "id": 0, "type": 30000, params: ['graph.html'], "bundleName": "com.trulymem.app", "moduleName": "phone" }, controller: this.controller });
|
||||
// WebView 显示 3D 星图
|
||||
Web.javaScriptAccess(true);
|
||||
// WebView 显示 3D 星图
|
||||
Web.width('100%');
|
||||
// WebView 显示 3D 星图
|
||||
Web.height('100%');
|
||||
// WebView 显示 3D 星图
|
||||
Web.zoomAccess(true);
|
||||
// WebView 显示 3D 星图
|
||||
Web.onPageEnd(() => {
|
||||
this.pushGraphDataToWebView();
|
||||
});
|
||||
// WebView 显示 3D 星图
|
||||
Web.javaScriptProxy({
|
||||
object: this.bridge,
|
||||
name: 'nativeBridge',
|
||||
methodList: ['onNodeClick', 'onSearch'],
|
||||
asyncMethodList: ['requestGraphData'],
|
||||
controller: this.controller
|
||||
});
|
||||
}, Web);
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new
|
||||
// 搜索框
|
||||
GraphNodeSearchBar(this, {
|
||||
searchText: this.__searchText,
|
||||
onSearchInput: (value: string): void => { this.onSearchInput(value); }
|
||||
}, undefined, elmtId, () => { }, { page: "features/graph/src/main/ets/pages/GraphPage.ets", line: 189, col: 13 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
searchText: this.searchText,
|
||||
onSearchInput: (value: string): void => { this.onSearchInput(value); }
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {});
|
||||
}
|
||||
}, { name: "GraphNodeSearchBar" });
|
||||
}
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
If.create();
|
||||
// 节点详情浮层
|
||||
if (this.showNodeDetail && this.selectedNodeDetail !== null) {
|
||||
this.ifElseBranchUpdateFunction(0, () => {
|
||||
{
|
||||
this.observeComponentCreation2((elmtId, isInitialRender) => {
|
||||
if (isInitialRender) {
|
||||
let componentCall = new NodeDetailPanel(this, {
|
||||
detail: this.selectedNodeDetail,
|
||||
onClose: (): void => { this.closeNodeDetail(); }
|
||||
}, undefined, elmtId, () => { }, { page: "features/graph/src/main/ets/pages/GraphPage.ets", line: 196, col: 17 });
|
||||
ViewPU.create(componentCall);
|
||||
let paramsLambda = () => {
|
||||
return {
|
||||
detail: this.selectedNodeDetail,
|
||||
onClose: (): void => { this.closeNodeDetail(); }
|
||||
};
|
||||
};
|
||||
componentCall.paramsGenerator_ = paramsLambda;
|
||||
}
|
||||
else {
|
||||
this.updateStateVarsOfChildByElmtId(elmtId, {
|
||||
detail: this.selectedNodeDetail
|
||||
});
|
||||
}
|
||||
}, { name: "NodeDetailPanel" });
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.ifElseBranchUpdateFunction(1, () => {
|
||||
});
|
||||
}
|
||||
}, If);
|
||||
If.pop();
|
||||
Stack.pop();
|
||||
}
|
||||
rerender() {
|
||||
this.updateDirtyElements();
|
||||
}
|
||||
}
|
||||
25
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/modules.cache
vendored
Normal file
25
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/modules.cache
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/Index.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/constant/TrulyMEMConstants.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/constant/TrulyMEMConstants.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/component/ImmersiveTabNavigation.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/component/ImmersiveTabNavigation.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/routermanager/PageContext.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/routermanager/PageContext.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/AIAgentService.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/AIAgentService.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/GraphMemoryService.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/service/GraphMemoryService.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/BreakpointSystem.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/BreakpointSystem.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/Logger.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/util/Logger.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/viewmodel/BaseViewModel.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/viewmodel/BaseViewModel.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/model/GraphDatabase.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/common/src/main/ets/model/GraphDatabase.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/Index.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/pages/GraphPage.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/pages/GraphPage.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/components/GraphComponents.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/graph/src/main/ets/components/GraphComponents.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/Index.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/pages/ChatPage.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/pages/ChatPage.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/components/ChatComponents.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/chat/src/main/ets/components/ChatComponents.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/Index.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/components/SettingsComponents.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/components/SettingsComponents.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/pages/SettingsPage.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/settings/src/main/ets/pages/SettingsPage.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/MainPage.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/MainPage.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/build/default/generated/r/default/ResourceTable.js;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/build/default/generated/r/default/ResourceTable.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/entryability/EntryAbility.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/entryability/EntryAbility.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/SplashPage.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/SplashPage.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/Index.ts;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/phone/src/main/ets/pages/Index.protoBin
|
||||
/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.txt;/home/program/TrulyMEM-TrueHumanMEM/products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin
|
||||
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin
vendored
Normal file
BIN
products/phone/build/default/cache/default/default@CompileArkTS/esmodule/debug/npmEntries.protoBin
vendored
Normal file
Binary file not shown.
@ -0,0 +1,7 @@
|
||||
@system.app:@native.system.app
|
||||
@ohos.app:@native.ohos.app
|
||||
@system.router:@native.system.router
|
||||
@system.curves:@native.system.curves
|
||||
@ohos.curves:@native.ohos.curves
|
||||
@system.matrix4:@native.system.matrix4
|
||||
@ohos.matrix4:@native.ohos.matrix4
|
||||
@ -0,0 +1,2 @@
|
||||
"use strict";
|
||||
//# sourceMappingURL=ResourceTable.js.map
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user