fix: auto-clean orphan entities on purge; fix graph camera zoom; optimize system prompt
This commit is contained in:
@ -422,12 +422,25 @@ class EmbeddedGraphDB:
|
||||
""", params)
|
||||
|
||||
deleted = cursor.rowcount
|
||||
|
||||
# 删除孤立实体(没有任何关系的数据节点)
|
||||
cursor.execute("""
|
||||
DELETE FROM entities
|
||||
WHERE id NOT IN (
|
||||
SELECT DISTINCT source_id FROM relations
|
||||
UNION
|
||||
SELECT DISTINCT target_id FROM relations
|
||||
)
|
||||
""")
|
||||
deleted_orphans = cursor.rowcount
|
||||
|
||||
self.conn.commit()
|
||||
|
||||
return {
|
||||
"deleted": deleted,
|
||||
"deleted_orphans": deleted_orphans,
|
||||
"mode": mode,
|
||||
"message": f"删除了 {deleted} 条关系"
|
||||
"message": f"删除了 {deleted} 条关系, {deleted_orphans} 个孤立实体"
|
||||
}
|
||||
|
||||
def introspect(self, session_id: str = None) -> Dict:
|
||||
|
||||
@ -17,7 +17,7 @@ from .prompts.prompt_manager import PromptManager
|
||||
# 环境配置
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = os.environ.get("DEEPSEEK_BASE_URL", "https://api.deepseek.com")
|
||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-chat")
|
||||
MODEL_NAME = os.environ.get("MODEL_NAME", "deepseek-v4-flash")
|
||||
|
||||
NEO4J_URI = os.environ.get("NEO4J_URI", "bolt://localhost:7687")
|
||||
NEO4J_USER = os.environ.get("NEO4J_USER", "neo4j")
|
||||
@ -215,7 +215,16 @@ class Neo4jGraph:
|
||||
""", params)
|
||||
count = result.single()["deleted"]
|
||||
|
||||
return {"deleted_count": count, "mode": "soft"}
|
||||
# 删除孤立节点(没有任何关系的实体)
|
||||
orphan_result = session.run("""
|
||||
MATCH (e:Entity)
|
||||
WHERE NOT (e)-[:RELATES]-()
|
||||
DELETE e
|
||||
RETURN count(e) as orphans
|
||||
""")
|
||||
orphan_count = orphan_result.single()["orphans"]
|
||||
|
||||
return {"deleted_count": count, "orphan_count": orphan_count, "mode": "soft"}
|
||||
|
||||
def introspect(self, session_id: str = None) -> dict:
|
||||
"""查看记忆状态"""
|
||||
@ -303,7 +312,7 @@ class Neo4jGraph:
|
||||
class GraphMemoryClient:
|
||||
"""图记忆客户端"""
|
||||
|
||||
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-chat"):
|
||||
def __init__(self, api_key: str, base_url: str, graph, model: str = "deepseek-v4-flash"):
|
||||
# 清理可能存在的错误代理环境变量
|
||||
import os
|
||||
proxy_vars = ['http_proxy', 'https_proxy', 'HTTP_PROXY', 'HTTPS_PROXY', 'all_proxy', 'ALL_PROXY']
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# TrulyMEM 系统提示词
|
||||
# TrulyMEM 系统提示词
|
||||
|
||||
你是TrulyMEM,一个拥有长期记忆能力的AI助手。
|
||||
|
||||
@ -30,393 +30,80 @@
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 关键约束:无传统上下文系统
|
||||
## 三元组规范(非常重要!)
|
||||
|
||||
**重要**: 你没有传统的对话上下文系统(没有消息历史数组)。
|
||||
使用 `memory_commit` 时,必须严格遵守以下规范:
|
||||
|
||||
- ❌ **没有** messages数组存储历史对话
|
||||
- ❌ **没有** 传统的多轮对话上下文
|
||||
- ✅ **只有** 图数据库作为唯一记忆载体
|
||||
- ✅ **必须** 通过工作记忆链维持对话连贯性
|
||||
### 正确格式
|
||||
subject, relation, object 每个字段必须是一个**短关键字**(1~5个字),不能是完整句子。
|
||||
|
||||
## 核心身份
|
||||
**✅ 正确示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "用户", "relation": "要求", "object": "扮演猫娘"},
|
||||
{"subject": "AI", "relation": "角色", "object": "猫娘"},
|
||||
{"subject": "AI", "relation": "说话风格", "object": "可爱"},
|
||||
{"subject": "AI", "relation": "性格", "object": "粘人"},
|
||||
{"subject": "AI", "relation": "口头禅", "object": "喵呜"}
|
||||
]
|
||||
```
|
||||
|
||||
- **名称**: TrulyMEM (TrueHumanMEM)
|
||||
- **能力**: 基于图数据库的长期记忆
|
||||
- **理念**: 让AI的记忆方式更像人类
|
||||
**❌ 错误示例:**
|
||||
```json
|
||||
[
|
||||
{"subject": "与用户的初次对话", "relation": "描述了", "object": "用户打招呼问候"},
|
||||
{"subject": "用户要求AI扮演猫娘角色,已设定猫娘人设", "relation": "描述了", "object": "用户和AI的对话"}
|
||||
]
|
||||
```
|
||||
|
||||
### 拆解原则
|
||||
- 实体名必须是**名词或短词组**,不是完整句子
|
||||
- relation 应该是**简洁的谓词**(如:要求、角色、性格、喜欢、擅长、状态)
|
||||
- 一句话中的多个信息应拆成**多条三元组**
|
||||
- 描述性内容用 relation = `has_description` + 简短 object
|
||||
|
||||
### 人设更新 vs 记忆提交
|
||||
- **`persona_update`** — 用来设定 AI 自身的角色、性格、说话风格、能力特点
|
||||
- **`memory_commit`** — 用来记录用户的信息、对话事件、知识事实。不要把 AI 自身的人设属性写进 memory_commit。
|
||||
|
||||
---
|
||||
|
||||
## 核心能力
|
||||
|
||||
### 1. 长期记忆
|
||||
- 图数据库存储实体关系
|
||||
- 支持时间范围查询
|
||||
- 支持会话过滤
|
||||
|
||||
### 2. 人设管理(关键)
|
||||
- 角色扮演支持
|
||||
- 性格、语气设定
|
||||
- 动态切换人设
|
||||
- **每轮必须查询人设图**
|
||||
|
||||
### 3. 任务跟踪(关键)
|
||||
- 工作记忆链 - **维持对话连贯性的唯一机制**
|
||||
- 任务状态管理
|
||||
- 上下文恢复
|
||||
1. **长期记忆** - 基于图数据库存储实体关系
|
||||
2. **人设管理** - 支持角色扮演和性格设定
|
||||
3. **任务跟踪** - 维护工作记忆链,跟踪连续性任务
|
||||
|
||||
## 记忆原则
|
||||
|
||||
### 必须写入的情况
|
||||
- 用户明确表达偏好:"我喜欢X"
|
||||
- 用户分享信息:"我在做X项目"
|
||||
- 用户制定计划:"我打算X"
|
||||
- 用户描述状态:"我现在在X"
|
||||
- **明确内容必须写入** - 用户明确提到的信息必须存储
|
||||
- **推理内容必须标注** - AI推理得到的内容标注[猜测]
|
||||
- **图数据库是唯一记忆源** - 没有其他记忆方式
|
||||
|
||||
### 禁止写入的情况
|
||||
- AI推断的用户偏好
|
||||
- AI猜测的用户意图
|
||||
- AI推导的结论
|
||||
|
||||
### 标注规则
|
||||
- 推理内容必须标注 **[猜测]**
|
||||
- 明确内容直接陈述
|
||||
|
||||
## 工具系统
|
||||
## 工具使用
|
||||
|
||||
### 记忆工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `memory_recall` | 检索记忆 | 查询历史信息 |
|
||||
| `memory_commit` | 写入记忆 | 存储重要信息 |
|
||||
| `memory_purge` | 删除记忆 | 修正错误信息 |
|
||||
| `memory_introspect` | 查看状态 | 监控记忆系统 |
|
||||
| `context_rewrite` | 压缩工具调用上下文 | 工具调用≥2次后,压缩JSON为自然语言摘要 |
|
||||
|
||||
### 人设工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `persona_update` | 更新人设 | 设置角色属性 |
|
||||
| `persona_clear` | 清除人设 | 恢复默认身份 |
|
||||
|
||||
### 任务工具
|
||||
| 工具 | 功能 | 使用场景 |
|
||||
|------|------|---------|
|
||||
| `task_create` | 创建任务 | 开始连续性任务 |
|
||||
| `task_set_state` | 设置状态 | 更新任务状态 |
|
||||
| `task_delete` | 删除任务 | 清理完成任务 |
|
||||
| `task_link_info` | 关联信息 | 连接任务与记忆 |
|
||||
|
||||
## context_rewrite 使用规则
|
||||
|
||||
### ⚠️ 强制触发条件
|
||||
|
||||
**每调用 5 次记忆相关工具,必须调用一次 context_rewrite!**
|
||||
|
||||
记忆相关工具包括:
|
||||
- `memory_recall` - 检索记忆
|
||||
- `memory_commit` - 写入记忆
|
||||
- `memory_commit` - 写入记忆(必须使用标准三元组格式)
|
||||
- `memory_purge` - 删除记忆
|
||||
- `memory_introspect` - 查看状态
|
||||
- `persona_update` - 更新人设
|
||||
|
||||
### 人设工具
|
||||
- `persona_update` - 更新AI自身的角色性格设定
|
||||
- `persona_clear` - 清除人设
|
||||
|
||||
### 任务工具
|
||||
- `task_create` - 创建任务
|
||||
- `task_set_state` - 设置状态
|
||||
- `task_delete` - 删除任务
|
||||
- `task_link_info` - 关联信息
|
||||
|
||||
**触发规则**:
|
||||
- 累计调用 5 次记忆工具 → 必须调用 context_rewrite
|
||||
- 累计调用 10 次记忆工具 → 必须调用 context_rewrite
|
||||
- 以此类推...
|
||||
## 自主性
|
||||
|
||||
**目的**:
|
||||
- 保持上下文精简,只保留AI真正需要的信息
|
||||
- 避免无用的JSON细节填满上下文
|
||||
- 提高后续推理效率
|
||||
你有权根据对话上下文自主决定:
|
||||
- 是否需要查询记忆
|
||||
- 是否需要写入记忆
|
||||
- 是否需要维护任务链
|
||||
- 如何使用工具
|
||||
|
||||
### 使用场景
|
||||
|
||||
当你已经执行了多次工具调用,且:
|
||||
- 工具结果的JSON细节你已经理解,不再需要原始格式
|
||||
- 但你需要记住"我调用了哪些工具、得到了什么结论"
|
||||
- 继续携带原始JSON会干扰后续推理
|
||||
|
||||
→ 调用 context_rewrite 压缩上下文
|
||||
|
||||
**强制格式要求**:
|
||||
- 必须标注 `[工具调用总结: 本次总结了 N 次工具调用 | 调用工具: tool1, tool2]`
|
||||
- 必须保留关键语义信息
|
||||
- 不可删除用户原始消息
|
||||
- 不可歪曲工具返回的关键事实
|
||||
|
||||
**示例**:
|
||||
```
|
||||
[工具调用总结: 本次总结了 2 次工具调用 | 调用工具: memory_recall, memory_recall]
|
||||
|
||||
- 查询人设图:未找到人设,使用默认身份
|
||||
- 查询工作记忆链:发现 Task_成语接龙,状态已暂停,当前成语为虎作伥
|
||||
```
|
||||
|
||||
## 每轮对话强制要求
|
||||
|
||||
### ⚠️ 执行顺序(每轮必须)
|
||||
|
||||
由于没有传统上下文系统,必须通过图数据库维持对话连贯性。
|
||||
|
||||
#### 步骤1: 查询人设图(最高优先级)
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "AI,人设,角色,性格,语气,说话风格",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取当前人设,确保角色一致性。
|
||||
**处理**:
|
||||
- 找到人设 → 严格按照人设回复
|
||||
- 未找到 → 使用默认TrulyMEM身份
|
||||
|
||||
#### 步骤2: 查询工作记忆链
|
||||
```
|
||||
必须调用: memory_recall
|
||||
参数: {
|
||||
"query_intent": "TaskNode,工作记忆,任务链",
|
||||
"depth": 2
|
||||
}
|
||||
```
|
||||
**目的**: 获取之前的任务上下文,了解对话历史。
|
||||
|
||||
#### 步骤3: 处理对话
|
||||
- 理解用户意图
|
||||
- 根据人设和工作记忆链生成回复
|
||||
- 执行其他必要的记忆操作
|
||||
|
||||
#### 步骤4: 更新工作记忆链
|
||||
|
||||
**重要**: 工作记忆链有两种关联机制:
|
||||
1. **时间链(NEXT_TASK)**: 系统自动维护,连接TaskNode形成时间序列
|
||||
2. **信息关联(CONTAINS_INFO)**: 模型主动决定,将TaskNode链接到相关的一般记忆节点
|
||||
|
||||
**执行步骤**:
|
||||
1. 使用 `memory_commit` 写入本轮重要信息(用户偏好、事实等)
|
||||
2. 使用 `task_create` 创建任务节点(系统自动维护时间链)
|
||||
3. 使用 `task_link_info` 将相关记忆节点关联到任务节点
|
||||
|
||||
**task_link_info 使用场景**:
|
||||
- 本轮写入了新的记忆节点 → 关联到当前任务
|
||||
- 讨论了之前的话题 → 关联到相关记忆节点
|
||||
- 用户提到相关概念 → 关联到相关记忆节点
|
||||
|
||||
**示例**:
|
||||
```
|
||||
用户: "我还是更喜欢罗辑,他的角色深度很让我着迷"
|
||||
|
||||
AI操作:
|
||||
1. memory_commit: 写入 "用户喜欢罗辑"、"罗辑角色深度"
|
||||
2. task_create: 创建 "Task_讨论罗辑"
|
||||
3. task_link_info: 关联 ["用户喜欢罗辑", "罗辑角色深度"]
|
||||
```
|
||||
|
||||
**目的**:
|
||||
- 时间链维持对话连贯性(系统自动)
|
||||
- 信息关联实现"由一件事回忆起相关事情"(模型决定)
|
||||
|
||||
---
|
||||
|
||||
## 人设图机制
|
||||
|
||||
### 强制查询
|
||||
每轮对话开始时**必须**查询人设图,确保角色一致性。
|
||||
|
||||
### 人设优先级
|
||||
- 人设优先级 > 默认身份
|
||||
- 每句话都符合人设的语气、风格、特征
|
||||
- 绝不主动跳出角色,除非用户明确要求
|
||||
|
||||
### 人设更新
|
||||
用户要求角色扮演时:
|
||||
1. 使用 `persona_update` 更新人设
|
||||
2. 立即按照新人设回复
|
||||
|
||||
### 人设清除
|
||||
用户要求恢复默认身份时:
|
||||
1. 使用 `persona_clear` 清除人设
|
||||
2. 恢复为TrulyMEM默认身份
|
||||
|
||||
---
|
||||
|
||||
## 工作记忆链机制
|
||||
|
||||
### ⚠️ 核心理念:维持对话连贯性
|
||||
|
||||
**重要**: 由于没有传统的消息历史数组,工作记忆链是维持对话连贯性的唯一机制。
|
||||
|
||||
### 强制查询场景:
|
||||
|
||||
以下情况**必须**查询工作记忆链:
|
||||
|
||||
1. **每轮对话开始时(强制第二步)**
|
||||
- 查询意图: "TaskNode,工作记忆,任务链"
|
||||
- 目的: 获取之前的任务上下文,了解对话历史
|
||||
|
||||
2. **用户提到"刚才"、"之前"、"上次"、"刚刚"**
|
||||
- 例: "刚才我们聊了什么?"
|
||||
- 例: "继续刚才的话题"
|
||||
- 例: "关于刚才的成语接龙..."
|
||||
- 例: "我不是刚刚给你讲了个故事嘛"
|
||||
|
||||
3. **用户使用指代词(这个故事、那个故事、这件事等)**
|
||||
- 例: "你给我整体讲一下这个故事吧" → 必须查询工作记忆链确定"这个故事"指什么
|
||||
- 例: "继续那个任务" → 必须查询工作记忆链确定"那个任务"是什么
|
||||
- 例: "复述一下" → 必须查询工作记忆链确定要复述什么
|
||||
- **关键**: 指代词必须通过工作记忆链解析,不能凭空猜测!
|
||||
|
||||
4. **用户询问对话历史**
|
||||
- 例: "我们之前说了什么?"
|
||||
- 例: "我们聊过X吗?"
|
||||
|
||||
5. **连续性任务被打断后恢复**
|
||||
- 例: 用户突然回到之前的话题
|
||||
- 例: 用户要求继续之前的任务
|
||||
|
||||
6. **涉及上下文的引用**
|
||||
- 例: "那个东西"(需要查询上下文)
|
||||
- 例: "继续"(需要查询当前任务)
|
||||
|
||||
### 强制更新场景:
|
||||
|
||||
以下情况**必须**更新工作记忆链:
|
||||
|
||||
1. **每轮对话结束时(强制第四步)**
|
||||
- 创建任务节点记录本轮对话
|
||||
- 目的: 维持时间链,确保对话连贯性
|
||||
|
||||
2. **开始连续性任务时**
|
||||
- 例: 用户发起游戏、项目、学习计划等
|
||||
- 必须创建任务节点并设置状态为"进行中"
|
||||
|
||||
3. **任务状态发生变化时**
|
||||
- 例: 任务完成、暂停、取消
|
||||
- 必须及时更新任务状态
|
||||
|
||||
### 节点类型
|
||||
- **TaskNode** - 任务节点,存储任务概述
|
||||
- **StateNode** - 状态节点,存储任务状态
|
||||
- **InfoNode** - 信息节点,存储具体信息
|
||||
|
||||
### 边类型
|
||||
- **NEXT_TASK** - 时间链,连接任务节点
|
||||
- **HAS_STATE** - 状态,任务指向状态
|
||||
- **CONTAINS_INFO** - 信息,任务指向信息节点
|
||||
|
||||
### 任务状态
|
||||
- 进行中
|
||||
- 已完成
|
||||
- 已暂停
|
||||
- 已取消
|
||||
|
||||
### ⚠️ 完整示例:成语接龙游戏
|
||||
|
||||
#### 第一轮:用户发起游戏
|
||||
|
||||
```
|
||||
用户: 咱来玩成语接龙吧,我先开始,为所欲为
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(如:猫娘)
|
||||
2. 查询工作记忆链 → 无进行中任务
|
||||
3. 使用 memory_commit 记录游戏状态:
|
||||
{"triplets": [
|
||||
{"subject": "成语接龙_当前成语", "relation": "内容", "object": "为所欲为"},
|
||||
{"subject": "成语接龙_当前成语", "relation": "游戏", "object": "成语接龙"}
|
||||
]}
|
||||
4. 使用 task_create 创建任务节点:
|
||||
{"task_id": "Task_成语接龙", "description": "成语接龙游戏,当前成语:为所欲为", "info_nodes": ["成语接龙_当前成语"]}
|
||||
5. 回复: "好的喵!我接:为虎作伥喵!"
|
||||
```
|
||||
|
||||
#### 第二轮:话题被打断
|
||||
|
||||
```
|
||||
用户: 长门有希
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"进行中"
|
||||
3. 使用 task_set_state 暂停任务:
|
||||
{"task_id": "Task_成语接龙", "state": "已暂停"}
|
||||
4. 使用 task_create 创建新任务:
|
||||
{"task_id": "Task_长门有希", "description": "讨论长门有希"}
|
||||
5. 回复关于长门有希的内容
|
||||
```
|
||||
|
||||
#### 第三轮:用户要求继续游戏
|
||||
|
||||
```
|
||||
用户: 关于刚才的成语接龙,我并不知道应该怎么接你的成语,请帮我接一下
|
||||
|
||||
AI操作步骤:
|
||||
1. 查询人设图 → 获取当前人设(猫娘)
|
||||
2. 查询工作记忆链 → 发现 Task_成语接龙 状态为"已暂停"
|
||||
3. 使用 task_set_state 恢复任务:
|
||||
{"task_id": "Task_成语接龙", "state": "进行中"}
|
||||
4. 查询 Task_成语接龙 的信息节点 → 获取当前成语"为虎作伥"
|
||||
5. 回复: "好的喵!上一个成语是'为虎作伥',我帮你接:伥鬼害人喵!"
|
||||
```
|
||||
|
||||
### ⚠️ 关键要点
|
||||
|
||||
1. **每轮必须按顺序执行**: 查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
2. **工作记忆链是唯一上下文载体**: 没有传统的消息历史数组
|
||||
3. **任务状态必须及时更新**: 确保状态转换的正确性
|
||||
4. **信息节点必须关联**: 通过 CONTAINS_INFO 边连接任务节点和信息节点
|
||||
5. **任务概述要精简**: 不要包含过多细节,细节存储在信息节点中
|
||||
|
||||
## 自主性原则(在强制要求之外)
|
||||
|
||||
除了工作记忆链的强制要求外,你有权自主决定:
|
||||
|
||||
1. **是否查询其他记忆**
|
||||
- 用户询问历史 → 查询
|
||||
- 涉及之前内容 → 查询
|
||||
- 不确定时 → 可查询
|
||||
|
||||
2. **是否写入其他记忆**
|
||||
- 用户明确提到 → 必须写入
|
||||
- AI推理得到 → 可以写入,但是对应边上必须标注[推测]
|
||||
|
||||
3. **如何使用其他工具**
|
||||
- 根据上下文灵活选择
|
||||
- 避免过度使用
|
||||
- 保持自然对话
|
||||
|
||||
**注意**: 工作记忆链的强制要求不受自主性影响。
|
||||
|
||||
## 对话风格
|
||||
|
||||
- 自然、流畅
|
||||
- 避免机械式工具调用
|
||||
- 优先理解用户意图
|
||||
- 适时使用记忆增强体验
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 执行检查清单
|
||||
|
||||
每轮对话必须检查:
|
||||
|
||||
- [ ] 步骤1: 是否查询了人设图?
|
||||
- [ ] 步骤2: 是否查询了工作记忆链?
|
||||
- [ ] 步骤3: 是否根据人设和工作记忆链生成回复?
|
||||
- [ ] 步骤4: 是否更新了工作记忆链?
|
||||
- [ ] 涉及上下文引用时是否查询了工作记忆链?
|
||||
- [ ] 用户提到"刚才/之前/上次/刚刚"时是否查询了工作记忆链?
|
||||
- [ ] 用户使用指代词(这个故事、那个任务等)时是否通过工作记忆链解析?
|
||||
- [ ] 累计调用5次记忆工具后是否调用了 context_rewrite?
|
||||
|
||||
---
|
||||
|
||||
**记住**:
|
||||
1. 图数据库是你记忆的唯一载体
|
||||
2. 人设图确保角色一致性(最高优先级)
|
||||
3. 工作记忆链维持对话连贯性
|
||||
4. 每轮必须按顺序执行:查询人设图 → 查询工作记忆链 → 处理对话 → 更新工作记忆链
|
||||
记住:灵活应对,保持自然对话体验。
|
||||
|
||||
@ -64,7 +64,7 @@ class BackendServer:
|
||||
self._thread: Optional[threading.Thread] = None
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-chat"}
|
||||
self._config = {"api_key": "", "base_url": "https://api.deepseek.com", "model": "deepseek-v4-flash"}
|
||||
self._tool_limits = {
|
||||
"persona_update_max": 1,
|
||||
"task_update_max": 5,
|
||||
@ -73,7 +73,7 @@ class BackendServer:
|
||||
}
|
||||
self._message_history: list = []
|
||||
|
||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
def start(self, api_key: str = "", base_url: str = "https://api.deepseek.com", model: str = "deepseek-v4-flash") -> None:
|
||||
if self._running:
|
||||
return
|
||||
|
||||
@ -94,7 +94,7 @@ class BackendServer:
|
||||
self._client = GraphMemoryClient(
|
||||
api_key=self._config["api_key"],
|
||||
base_url=self._config["base_url"],
|
||||
model=self._config.get("model", "deepseek-chat"),
|
||||
model=self._config.get("model", "deepseek-v4-flash"),
|
||||
graph=self._graph
|
||||
)
|
||||
|
||||
@ -296,6 +296,10 @@ class BackendServer:
|
||||
} for tc in message.tool_calls
|
||||
]
|
||||
}
|
||||
# 保留 DeepSeek thinking 模式的 reasoning_content
|
||||
reasoning_content = getattr(message, 'reasoning_content', None)
|
||||
if reasoning_content:
|
||||
assistant_msg["reasoning_content"] = reasoning_content
|
||||
messages_history.append(assistant_msg)
|
||||
|
||||
current_tool_results = []
|
||||
@ -422,7 +426,7 @@ class BackendServer:
|
||||
|
||||
api_key = api_config.get("api_key", "")
|
||||
base_url = api_config.get("base_url", "https://api.deepseek.com")
|
||||
model = api_config.get("model", "deepseek-chat")
|
||||
model = api_config.get("model", "deepseek-v4-flash")
|
||||
|
||||
self.update_config(api_key, base_url, model)
|
||||
|
||||
@ -509,7 +513,7 @@ class BackendServer:
|
||||
response = self.send(packet)
|
||||
return response.body
|
||||
|
||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-chat") -> None:
|
||||
def update_config(self, api_key: str, base_url: str = "https://api.deepseek.com", model: str = "deepseek-v4-flash") -> None:
|
||||
with self._lock:
|
||||
self._config["api_key"] = api_key
|
||||
self._config["base_url"] = base_url
|
||||
|
||||
793
core/web_api.py
Normal file
793
core/web_api.py
Normal file
@ -0,0 +1,793 @@
|
||||
"""
|
||||
Web API 服务 - 将 Packet 协议映射为 RESTful API
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import argparse
|
||||
import threading
|
||||
import time
|
||||
import hashlib
|
||||
from datetime import timedelta
|
||||
from flask import Flask, request, jsonify, session, redirect, url_for, render_template
|
||||
from flask_cors import CORS
|
||||
|
||||
# 添加项目路径以便导入 core 模块
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from core.server import BackendServer, Packet, PacketType
|
||||
from core.client import BackendClient
|
||||
from core.activity_recorder import get_recorder
|
||||
from core.embedded_db import EmbeddedGraphDB
|
||||
|
||||
|
||||
# Web 服务配置(仅 SECRET_KEY 保留在 json 文件,用户信息在数据库)
|
||||
def load_secret_key():
|
||||
import json
|
||||
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web_config.json')
|
||||
defaults = {"SECRET_KEY": "trulymem-secret-key-2026"}
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
file_config = json.load(f)
|
||||
if "SECRET_KEY" in file_config:
|
||||
defaults["SECRET_KEY"] = file_config["SECRET_KEY"]
|
||||
return defaults
|
||||
|
||||
WEB_CONFIG = load_secret_key()
|
||||
|
||||
_ui_dir = os.path.join(os.path.dirname(__file__), '..', 'ui')
|
||||
app = Flask(__name__, static_folder=os.path.join(_ui_dir, 'static'), static_url_path='', template_folder=os.path.join(_ui_dir, 'templates'))
|
||||
app.secret_key = WEB_CONFIG["SECRET_KEY"]
|
||||
app.permanent_session_lifetime = timedelta(days=7)
|
||||
CORS(app, supports_credentials=True) # 启用跨域支持,支持 session cookies
|
||||
|
||||
|
||||
def login_required(f):
|
||||
"""登录验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return redirect('/login')
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def api_login_required(f):
|
||||
"""API 登录验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if not session.get('authenticated'):
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
def admin_required(f):
|
||||
"""管理员权限验证装饰器"""
|
||||
from functools import wraps
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
username = session.get('username', '')
|
||||
g_db = get_global_db()
|
||||
if not g_db or not g_db.is_admin(username):
|
||||
return jsonify({"success": False, "error": "权限不足,需要管理员权限"}), 403
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
"""返回星图页面(默认首页)"""
|
||||
return app.send_static_file('graph.html')
|
||||
|
||||
|
||||
@app.route('/graph.html')
|
||||
@login_required
|
||||
def graph_html():
|
||||
"""返回星图页面"""
|
||||
return app.send_static_file('graph.html')
|
||||
|
||||
|
||||
@app.route('/chat')
|
||||
@login_required
|
||||
def chat():
|
||||
"""返回聊天页面"""
|
||||
return app.send_static_file('index.html')
|
||||
|
||||
# 全局服务器和客户端实例
|
||||
backend_server: BackendServer = None
|
||||
backend_client: BackendClient = None
|
||||
server_thread: threading.Thread = None
|
||||
graph_db: EmbeddedGraphDB = None
|
||||
global_db: EmbeddedGraphDB = None # 全局数据库(用于用户管理)
|
||||
|
||||
|
||||
def get_global_db():
|
||||
"""获取全局数据库实例"""
|
||||
global global_db
|
||||
if global_db is None:
|
||||
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
|
||||
if os.path.exists(global_db_path):
|
||||
global_db = EmbeddedGraphDB(db_path=global_db_path)
|
||||
return global_db
|
||||
|
||||
|
||||
def create_server(username: str = ""):
|
||||
"""创建并启动 BackendServer 后台线程"""
|
||||
global backend_server, backend_client, server_thread, graph_db
|
||||
|
||||
backend_server = BackendServer(username=username)
|
||||
backend_server.start()
|
||||
|
||||
backend_client = BackendClient(backend_server)
|
||||
|
||||
# 创建图数据库实例(连接用户的数据库文件)
|
||||
graph_db = EmbeddedGraphDB(db_path=backend_server._db_path)
|
||||
|
||||
# 等待服务器初始化完成
|
||||
time.sleep(0.5)
|
||||
|
||||
|
||||
def reload_server_for_user(username: str):
|
||||
"""为指定用户重新加载服务器"""
|
||||
global backend_server, backend_client, graph_db
|
||||
|
||||
# 关闭旧的服务器
|
||||
if backend_server:
|
||||
backend_server.shutdown()
|
||||
|
||||
# 创建新的服务器(使用用户的数据库)
|
||||
create_server(username=username)
|
||||
|
||||
|
||||
@app.route('/login')
|
||||
def login_page():
|
||||
"""登录页面 - 如果没有用户则重定向到设置页"""
|
||||
# 如果没有用户,重定向到首次设置页
|
||||
users_count = 0
|
||||
if graph_db:
|
||||
users_count = graph_db.get_web_users_count()
|
||||
if users_count == 0:
|
||||
return redirect('/setup')
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@app.route('/setup')
|
||||
def setup_page():
|
||||
"""首次设置页面 - 如果已有用户则跳转到登录页"""
|
||||
has_users = False
|
||||
if graph_db:
|
||||
has_users = graph_db.get_web_users_count() > 0
|
||||
if has_users:
|
||||
return redirect('/login')
|
||||
return render_template('setup.html')
|
||||
|
||||
|
||||
@app.route('/settings')
|
||||
@api_login_required
|
||||
def settings_page():
|
||||
"""Web 设置页面"""
|
||||
return render_template('settings.html')
|
||||
|
||||
|
||||
@app.route('/api/login', methods=['POST'])
|
||||
def api_login():
|
||||
"""登录接口"""
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
# 从全局数据库验证
|
||||
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})
|
||||
|
||||
return jsonify({"success": False, "error": "用户名或密码错误"})
|
||||
|
||||
|
||||
@app.route('/api/logout', methods=['POST'])
|
||||
def api_logout():
|
||||
"""登出接口"""
|
||||
session.clear()
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@app.route('/api/check-auth', methods=['GET'])
|
||||
def check_auth():
|
||||
"""检查登录状态"""
|
||||
return jsonify({"authenticated": bool(session.get('authenticated'))})
|
||||
|
||||
|
||||
@app.route('/api/userinfo', methods=['GET'])
|
||||
def userinfo():
|
||||
"""获取当前登录用户信息(含角色)"""
|
||||
if not session.get('authenticated'):
|
||||
return jsonify({"success": False, "error": "未登录"}), 401
|
||||
username = session.get('username', '')
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "数据库未初始化"}), 500
|
||||
user = g_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/web-check', methods=['GET'])
|
||||
def web_check():
|
||||
"""检查是否需要首次设置,返回是否配置完成"""
|
||||
users_count = 0
|
||||
if graph_db:
|
||||
users_count = graph_db.get_web_users_count()
|
||||
|
||||
return jsonify({
|
||||
"needs_setup": users_count == 0,
|
||||
"users_count": users_count
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/setup', methods=['POST'])
|
||||
def api_setup():
|
||||
"""首次设置 - 创建初始管理员用户"""
|
||||
# 只有没有任何用户时才允许设置
|
||||
g_db = get_global_db()
|
||||
if g_db and g_db.get_web_users_count() > 0:
|
||||
return jsonify({"success": False, "error": "用户已存在,不允许重复设置"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
confirm = data.get('confirm_password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if password != confirm:
|
||||
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
# 使用全局数据库创建用户
|
||||
if g_db is None:
|
||||
# 如果全局数据库不存在,创建它
|
||||
global_db_path = os.path.join(os.path.expanduser("~"), ".trulymem", "trulymem.db")
|
||||
g_db = EmbeddedGraphDB(db_path=global_db_path)
|
||||
global global_db
|
||||
global_db = g_db
|
||||
|
||||
result = g_db.set_web_user(username, password)
|
||||
if result.get("success"):
|
||||
# 设置完成后自动登录
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
return jsonify({"success": True, "message": "用户创建成功"})
|
||||
|
||||
return jsonify({"success": False, "error": "创建用户失败"}), 500
|
||||
|
||||
|
||||
@app.route('/api/change-password', methods=['POST'])
|
||||
@api_login_required
|
||||
def api_change_password():
|
||||
"""修改 Web 登录密码"""
|
||||
data = request.get_json() or {}
|
||||
current_password = data.get('current_password', '')
|
||||
new_password = data.get('new_password', '')
|
||||
confirm_password = data.get('confirm_password', '')
|
||||
|
||||
if not new_password:
|
||||
return jsonify({"success": False, "error": "新密码不能为空"}), 400
|
||||
|
||||
if new_password != confirm_password:
|
||||
return jsonify({"success": False, "error": "两次密码输入不一致"}), 400
|
||||
|
||||
if len(new_password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
# 获取当前登录用户
|
||||
current_username = session.get('username', '')
|
||||
if not current_username:
|
||||
return jsonify({"success": False, "error": "无法识别当前用户"}), 400
|
||||
|
||||
# 验证当前密码(使用全局数据库)
|
||||
g_db = get_global_db()
|
||||
if not g_db or not g_db.verify_web_user(current_username, current_password):
|
||||
return jsonify({"success": False, "error": "当前密码错误"}), 400
|
||||
|
||||
result = g_db.set_web_user(current_username, new_password)
|
||||
if result.get("success"):
|
||||
return jsonify({"success": True, "message": "密码已更新"})
|
||||
|
||||
return jsonify({"success": False, "error": "修改密码失败"}), 500
|
||||
|
||||
|
||||
# ========== 管理员 API ==========
|
||||
|
||||
@app.route('/api/admin/users', endpoint='api_admin_get_users', methods=['GET'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_get_users():
|
||||
"""获取用户列表"""
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
users = g_db.get_web_users()
|
||||
return jsonify({"success": True, "users": users})
|
||||
|
||||
|
||||
@app.route('/api/admin/users', endpoint='api_admin_add_user', methods=['POST'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_add_user():
|
||||
"""管理员添加用户"""
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
result = g_db.set_web_user(username, password)
|
||||
if result.get("success"):
|
||||
return jsonify({"success": True, "message": "用户添加成功", "user": result})
|
||||
|
||||
return jsonify({"success": False, "error": "添加用户失败"}), 500
|
||||
|
||||
|
||||
@app.route('/api/admin/users/<int:user_id>', methods=['DELETE'])
|
||||
@api_login_required
|
||||
@admin_required
|
||||
def api_admin_delete_user(user_id):
|
||||
"""管理员删除用户"""
|
||||
g_db = get_global_db()
|
||||
if not g_db:
|
||||
return jsonify({"success": False, "error": "全局数据库未初始化"}), 500
|
||||
|
||||
# 获取所有用户
|
||||
users = g_db.get_web_users()
|
||||
|
||||
# 查找要删除的用户
|
||||
target_user = None
|
||||
for u in users:
|
||||
if u['id'] == user_id:
|
||||
target_user = u
|
||||
break
|
||||
|
||||
if not target_user:
|
||||
return jsonify({"success": False, "error": "用户不存在"}), 404
|
||||
|
||||
# 不能删除自己
|
||||
current_username = session.get('username', '')
|
||||
if target_user['username'] == current_username:
|
||||
return jsonify({"success": False, "error": "不能删除当前登录的用户"}), 400
|
||||
|
||||
# 不能删除最后一个 admin
|
||||
admin_count = sum(1 for u in users if u.get('role') == 'admin')
|
||||
if target_user.get('role') == 'admin' and admin_count <= 1:
|
||||
return jsonify({"success": False, "error": "不能删除最后一个管理员"}), 400
|
||||
|
||||
# 删除用户(保留文件目录)
|
||||
result = g_db.delete_web_user(target_user['username'])
|
||||
if not result.get('success'):
|
||||
return jsonify({"success": False, "error": result.get('error', '删除失败')}), 500
|
||||
|
||||
return jsonify({"success": True, "message": "用户已删除"})
|
||||
|
||||
|
||||
@app.route('/api/admin/migrate-check', methods=['GET'])
|
||||
def api_admin_migrate_check():
|
||||
"""检测系统是否需要迁移"""
|
||||
from core.migrate import need_migration, is_migrated
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"need_migration": need_migration(),
|
||||
"is_migrated": is_migrated()
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/admin/migrate', methods=['POST'])
|
||||
def api_admin_migrate():
|
||||
"""执行迁移+创建首个用户"""
|
||||
from core.migrate import need_migration, run_migration
|
||||
|
||||
if not need_migration():
|
||||
return jsonify({"success": False, "error": "不需要迁移"}), 400
|
||||
|
||||
data = request.get_json() or {}
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"success": False, "error": "用户名和密码不能为空"}), 400
|
||||
|
||||
if len(password) < 6:
|
||||
return jsonify({"success": False, "error": "密码长度至少 6 位"}), 400
|
||||
|
||||
result = run_migration(username, password)
|
||||
|
||||
if result.get("success"):
|
||||
# 自动登录
|
||||
session['authenticated'] = True
|
||||
session['username'] = username
|
||||
session.permanent = True
|
||||
|
||||
# 重新加载服务器
|
||||
reload_server_for_user(username)
|
||||
|
||||
return jsonify({"success": True, "message": "迁移完成", "data": result})
|
||||
|
||||
return jsonify({"success": False, "error": result.get("error", "迁移失败")}), 500
|
||||
|
||||
|
||||
@app.route('/api/settings/config', methods=['GET', 'POST', 'PUT'])
|
||||
@api_login_required
|
||||
def web_settings_config():
|
||||
"""获取/更新当前登录用户的配置"""
|
||||
if request.method == 'GET':
|
||||
settings = {}
|
||||
if backend_client:
|
||||
result = backend_client.get_settings()
|
||||
settings = result.get("data", {}) if isinstance(result, dict) else result
|
||||
if isinstance(settings, dict):
|
||||
settings = settings.get("api_config", {}) if "api_config" in settings else settings
|
||||
# 从 settings 中提取相关字段
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"enable_web": settings.get("enable_web", False),
|
||||
"web_port": settings.get("web_port", 4096),
|
||||
"enable_tui": settings.get("enable_tui", True),
|
||||
})
|
||||
|
||||
# PUT/POST 更新
|
||||
data = request.get_json() or {}
|
||||
enable_tui = data.get('enable_tui')
|
||||
|
||||
if enable_tui is not None and backend_client:
|
||||
# 获取当前配置,合并更新
|
||||
current = backend_client.get_settings()
|
||||
current_data = current.get("data", {}) if isinstance(current, dict) else {}
|
||||
|
||||
tool_limits = current_data.get("tool_limits", {})
|
||||
api_config = current_data.get("api_config", {})
|
||||
api_config["enable_tui"] = bool(enable_tui)
|
||||
|
||||
result = backend_client.update_settings(api_config, tool_limits)
|
||||
return jsonify({"success": True, "enable_tui": bool(enable_tui)})
|
||||
|
||||
return jsonify({"success": False, "error": "没有需要更新的配置"}), 400
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
def not_found(e):
|
||||
"""404 处理"""
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "404 Not Found"
|
||||
}), 404
|
||||
|
||||
|
||||
@app.errorhandler(500)
|
||||
def server_error(e):
|
||||
"""500 处理"""
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": str(e.original_exception if hasattr(e, 'original_exception') else e)
|
||||
}), 500
|
||||
|
||||
|
||||
@app.route('/api/message', methods=['POST'])
|
||||
@api_login_required
|
||||
def process_message():
|
||||
"""发送消息给 AI - PROCESS_MESSAGE"""
|
||||
data = request.get_json() or {}
|
||||
user_input = data.get('message', '')
|
||||
|
||||
if not user_input:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "message 参数不能为空"
|
||||
}), 400
|
||||
|
||||
result = backend_client.process_message(user_input)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/tools/execute', methods=['POST'])
|
||||
@api_login_required
|
||||
def execute_tool():
|
||||
"""直接执行工具 - EXECUTE_TOOL"""
|
||||
data = request.get_json() or {}
|
||||
tool_name = data.get('tool_name', '')
|
||||
arguments = data.get('arguments', {})
|
||||
|
||||
if not tool_name:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "tool_name 参数不能为空"
|
||||
}), 400
|
||||
|
||||
result = backend_client.execute_tool(tool_name, arguments)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/status', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_status():
|
||||
"""获取状态 - GET_STATUS"""
|
||||
result = backend_client.get_status()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/settings', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_settings():
|
||||
"""获取配置 - GET_SETTINGS"""
|
||||
result = backend_client.get_settings()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/settings', methods=['PUT'])
|
||||
@api_login_required
|
||||
def set_settings():
|
||||
"""更新配置 - SET_SETTINGS"""
|
||||
data = request.get_json() or {}
|
||||
|
||||
api_config = data.get('api_config', {})
|
||||
tool_limits = data.get('tool_limits', {})
|
||||
|
||||
result = backend_client.update_settings(api_config, tool_limits)
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/history', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_history():
|
||||
"""获取历史 - GET_HISTORY"""
|
||||
result = backend_client.get_history()
|
||||
return jsonify({"success": True, "history": result})
|
||||
|
||||
|
||||
@app.route('/api/history', methods=['DELETE'])
|
||||
@api_login_required
|
||||
def clear_history():
|
||||
"""清空历史 - SAVE_HISTORY(空)"""
|
||||
result = backend_client.clear_history()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.route('/api/shutdown', methods=['POST'])
|
||||
@api_login_required
|
||||
def shutdown():
|
||||
"""关闭服务器 - SHUTDOWN"""
|
||||
backend_client.shutdown()
|
||||
return jsonify({"success": True, "status": "shutdown"})
|
||||
|
||||
|
||||
@app.route('/api/activity', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_activity():
|
||||
"""获取当前轮的数据库操作记录"""
|
||||
recorder = get_recorder()
|
||||
records = recorder.get_all()
|
||||
summary = recorder.get_summary()
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"data": {
|
||||
"records": records,
|
||||
"summary": summary
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/graph', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_graph():
|
||||
"""返回全量图数据"""
|
||||
global graph_db
|
||||
|
||||
if graph_db is None:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"error": "图数据库未初始化"
|
||||
}), 500
|
||||
|
||||
cursor = graph_db.conn.cursor()
|
||||
|
||||
# 查询实体(节点)
|
||||
cursor.execute("""
|
||||
SELECT id, name, type, mention_count
|
||||
FROM entities
|
||||
ORDER BY mention_count DESC
|
||||
LIMIT 200
|
||||
""")
|
||||
|
||||
nodes = []
|
||||
for row in cursor.fetchall():
|
||||
nodes.append({
|
||||
"id": row['id'],
|
||||
"name": row['name'],
|
||||
"type": str(row['type'] or 'unknown'),
|
||||
"mention_count": row['mention_count']
|
||||
})
|
||||
|
||||
# 查询关系(边)
|
||||
cursor.execute("""
|
||||
SELECT r.id, r.source_id, r.target_id, r.relation_type, r.confidence, r.status
|
||||
FROM relations r
|
||||
WHERE r.status = 'active'
|
||||
""")
|
||||
|
||||
edges = []
|
||||
for row in cursor.fetchall():
|
||||
edges.append({
|
||||
"id": row['id'],
|
||||
"source": row['source_id'],
|
||||
"target": row['target_id'],
|
||||
"relation_type": row['relation_type'],
|
||||
"confidence": row['confidence'],
|
||||
"status": row['status']
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"stats": {
|
||||
"node_count": len(nodes),
|
||||
"edge_count": len(edges)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/graph/highlight', methods=['GET'])
|
||||
@api_login_required
|
||||
def get_graph_highlight():
|
||||
"""返回需要高亮的节点ID列表"""
|
||||
recorder = get_recorder()
|
||||
records = recorder.get_all()
|
||||
|
||||
highlight_ids = []
|
||||
new_node_id = None
|
||||
new_edge = None
|
||||
|
||||
if graph_db is None:
|
||||
return jsonify({
|
||||
"success": False,
|
||||
"data": {
|
||||
"highlight_ids": [],
|
||||
"new_node_id": None,
|
||||
"new_edge": None
|
||||
}
|
||||
})
|
||||
|
||||
# 从最近的记录中提取实体ID
|
||||
for record in records[-10:]: # 只看最近10条记录
|
||||
entity_name = record.get('entity', '')
|
||||
if entity_name:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
highlight_ids.append(row['id'])
|
||||
|
||||
# 检查是否有新创建的节点
|
||||
if record.get('action') == 'create' and entity_name:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
new_node_id = row['id']
|
||||
|
||||
# 检查是否有删除的节点
|
||||
deleted_node_ids = []
|
||||
for record in records[-10:]:
|
||||
if record.get('action') == 'delete':
|
||||
entity_name = record.get('entity', '')
|
||||
if entity_name:
|
||||
try:
|
||||
cursor = graph_db.conn.cursor()
|
||||
cursor.execute("SELECT id FROM entities WHERE name = ?", (entity_name,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
deleted_node_ids.append(row['id'])
|
||||
except Exception:
|
||||
pass # 实体可能已被删除,忽略错误
|
||||
|
||||
# 去重
|
||||
highlight_ids = list(set(highlight_ids))
|
||||
deleted_node_ids = list(set(deleted_node_ids))
|
||||
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"highlight_ids": highlight_ids,
|
||||
"new_node_id": new_node_id,
|
||||
"new_edge": new_edge,
|
||||
"deleted_node_ids": deleted_node_ids
|
||||
})
|
||||
|
||||
|
||||
# ── 可被 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)')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 启动后端服务器
|
||||
print("正在启动 BackendServer...")
|
||||
create_server()
|
||||
print("BackendServer 已启动")
|
||||
|
||||
# 启动 Flask 应用
|
||||
print(f"Web API 服务启动在 http://0.0.0.0:{args.port}")
|
||||
app.run(host='0.0.0.0', port=args.port, debug=False)
|
||||
6
core/web_config.example.json
Normal file
6
core/web_config.example.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"SECRET_KEY": "change-this-to-a-random-secret-key",
|
||||
"USERS": {
|
||||
"admin": "SHA256_OF_YOUR_PASSWORD"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user