Memory Gateway 2
Memory Gateway 2 是一个轻量级 FastAPI 服务,用于在 EverOS 现有
/api/v1/memory/add、/api/v1/memory/flush、/api/v1/memory/search
能力之上构建用户资源记忆层。
它只维护 Gateway 自己的 SQLite 元数据表、软删除记录和手动覆盖记录, 不会直接修改 EverOS 的 Markdown、SQLite 或 LanceDB 内部文件。
功能范围
- 上传用户资源:文件、图片、音频、PDF、HTML、普通文档、纯文本。
- 保存资源元数据到 SQLite。
- 为每个资源生成独立 EverOS
session_id。 - 调用 EverOS
add和flush完成资源记忆摄入,并对临时失败做轻量重试。 - 提供资源列表、详情、软删除。
- 支持上传大小限制、MIME 白名单、同用户同 app/project 下按 sha256 幂等复用资源。
- 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。
- 支持记忆 tombstone 软删除。
- 支持记忆手动 override。
- 搜索结果返回前自动过滤 tombstone 并应用 override。
目录结构
/home/tom/memory-gateway2
├── core/ # Gateway 核心代码
│ ├── api.py # FastAPI 路由
│ ├── config.py # 环境变量配置
│ ├── db.py # SQLite schema 初始化
│ ├── everos_client.py # EverOS HTTP client
│ ├── repository.py # SQLite 读写
│ └── service.py # 业务编排
├── main.py # Python 启动入口
├── tests/ # 测试
├── .env.example # 环境变量示例
└── pyproject.toml
环境配置
复制示例配置:
cd /home/tom/memory-gateway2
cp .env.example .env
配置项说明:
| 变量 | 默认值 | 说明 |
|---|---|---|
EVEROS_BASE_URL |
http://127.0.0.1:8000 |
EverOS API 服务地址 |
MEMORY_GATEWAY_DB_PATH |
./data/memory_gateway.sqlite3 |
Gateway 自己的 SQLite 数据库路径 |
MEMORY_GATEWAY_STORAGE_DIR |
./data/storage |
用户上传原始文件保存路径 |
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE |
50 |
resources scope 搜索时每批 session_id 数量 |
MEMORY_GATEWAY_MAX_UPLOAD_BYTES |
26214400 |
单个上传文件最大字节数,默认 25MB |
MEMORY_GATEWAY_ALLOWED_MIME_TYPES |
常见图片、音频、PDF、HTML、文本和 Office 文档 | 逗号分隔的上传 MIME 白名单,支持 image/* 这类前缀匹配 |
MEMORY_GATEWAY_EVEROS_INGEST_ATTEMPTS |
3 |
EverOS add 和 flush 各自最多重试次数 |
MEMORY_GATEWAY_EVEROS_RETRY_DELAY_SECONDS |
0.25 |
EverOS 摄入重试间隔秒数 |
MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS |
120 |
单次 EverOS HTTP 请求超时秒数 |
MEMORY_GATEWAY_HOST |
127.0.0.1 |
Gateway API 监听地址 |
MEMORY_GATEWAY_PORT |
8010 |
Gateway API 监听端口 |
MEMORY_GATEWAY_RELOAD |
false |
是否启用 uvicorn reload,开发时可设为 true |
注意:MEMORY_GATEWAY_DB_PATH 和 MEMORY_GATEWAY_STORAGE_DIR 是 Gateway
自己的存储位置,不要配置成 EverOS 的内部存储目录。
安装依赖
cd /home/tom/memory-gateway2
uv pip install -e .
启动 API
使用 Python 启动:
cd /home/tom/memory-gateway2
python main.py
默认监听:
http://127.0.0.1:8010
session_id 规范
Gateway 遵循以下 session_id 规范:
| 场景 | 格式 |
|---|---|
| 普通聊天 | chat:{conversation_id} |
| 用户上传资源 | resource:{user_id}:{resource_id} |
| 用户手动修正 | memory_edit:{user_id} |
当前实现中,资源上传会自动生成:
resource:{user_id}:{resource_id}
数据库表
启动时会自动创建以下 SQLite 表:
user_resources:资源元数据、内部 URI、状态、软删除时间。users:用户 ID 和 Gateway 生成的user_key。memory_tombstones:用户删除的 memory id 或 session_id。memory_overrides:用户手动修正后的 memory 文本。
API 使用说明
除 POST /users 外,所有业务 API 都需要携带 user_id 和 user_key。认证失败返回 401。
1. 健康检查
GET /health
该接口不需要 user_id 或 user_key,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。
请求示例:
curl http://127.0.0.1:8010/health
EverOS 正常时响应示例:
{
"status": "ok",
"api": {
"status": "ok"
},
"everos": {
"status": "ok",
"base_url": "http://127.0.0.1:8000",
"data": {
"status": "ok"
}
}
}
EverOS 不可访问时仍返回 HTTP 200,但 status 会变成 degraded,便于区分“Gateway API 活着”和“上游 EverOS 故障”:
{
"status": "degraded",
"api": {
"status": "ok"
},
"everos": {
"status": "unavailable",
"base_url": "http://127.0.0.1:8000",
"error": "Connection refused"
}
}
2. 创建用户
POST /users
Content-Type: application/json
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
请求示例:
curl -X POST http://127.0.0.1:8010/users \
-H 'Content-Type: application/json' \
-d '{"user_id":"u_123"}'
响应示例:
{
"user_id": "u_123",
"user_key": "uk_xxx",
"created_at": "2026-06-10T10:00:00+00:00"
}
user_key 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 user_id 已存在,接口会返回已有 user_key。
3. 上传资源
POST /resources
Content-Type: multipart/form-data
表单参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
user_id |
string | 是 | 无 | 用户 ID |
user_key |
string | 是 | 无 | 用户 key |
app_id |
string | 否 | default |
EverOS app scope |
project_id |
string | 否 | default |
EverOS project scope |
file |
file | 是 | 无 | 上传资源文件 |
title |
string | 否 | null |
资源标题 |
description |
string | 否 | null |
资源描述 |
处理流程:
- 保存原始文件到
MEMORY_GATEWAY_STORAGE_DIR。 - 计算
sha256和size_bytes。 - 生成
resource_id。 - 生成
session_id = resource:{user_id}:{resource_id}。 - 写入
user_resources,状态为ingesting。 - 根据 MIME 类型映射 EverOS content type。
- 调用 EverOS
/api/v1/memory/add。 - 调用 EverOS
/api/v1/memory/flush。 - 成功后状态改为
extracted,失败后状态改为failed。
上传策略:
- 文件会按流式方式写入磁盘,超过
MEMORY_GATEWAY_MAX_UPLOAD_BYTES会返回413,不会写入资源记录。 - MIME 类型不在
MEMORY_GATEWAY_ALLOWED_MIME_TYPES白名单内会返回415。 - 同一用户在同一
app_id、project_id下重复上传相同 sha256 的活跃资源,会直接返回已有资源,避免重复调用 EverOS 摄入。 - EverOS
add和flush临时失败时会分别按配置重试;单次请求受MEMORY_GATEWAY_EVEROS_TIMEOUT_SECONDS控制;全部失败后资源状态为failed,并记录error_message。
content type 映射:
| 文件类型 | EverOS content type |
|---|---|
image/* |
image |
audio/* |
audio |
pdf |
|
| HTML | html |
| 纯文本、Markdown、CSV、日志 | text |
| 其他文档 | doc |
请求示例:
curl -X POST http://127.0.0.1:8010/resources \
-F user_id=u_123 \
-F user_key=uk_xxx \
-F app_id=default \
-F project_id=default \
-F title="合同文件" \
-F description="2026 年付款合同" \
-F file=@./contract.pdf
响应示例:
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "extracted"
}
对外返回的 uri 永远是 resource://{user_id}/{resource_id},不会泄露内部 file:// 路径。
4. 查询资源列表
GET /resources?user_id={user_id}&user_key={user_key}
参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
只返回当前用户 deleted_at IS NULL 的资源。不同用户的资源彼此隔离。
请求示例:
curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"
响应示例:
{
"resources": [
{
"resource_id": "r_xxx",
"user_id": "u_123",
"filename": "contract.pdf",
"content_type": "pdf",
"mime_type": "application/pdf",
"uri": "resource://u_123/r_xxx",
"session_id": "resource:u_123:r_xxx",
"status": "extracted",
"title": "合同文件",
"description": "2026 年付款合同",
"created_at": "2026-06-10T10:00:00+00:00",
"updated_at": "2026-06-10T10:00:05+00:00"
}
]
}
如果用户没有上传过资源,返回:
{
"resources": []
}
5. 查询资源详情
GET /resources/{resource_id}?user_id={user_id}&user_key={user_key}
路径参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
resource_id |
string | 是 | 资源 ID |
Query 参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
请求示例:
curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
响应示例:
{
"resources": [
{
"resource_id": "r_xxx",
"user_id": "u_123",
"filename": "contract.pdf",
"content_type": "pdf",
"mime_type": "application/pdf",
"uri": "resource://u_123/r_xxx",
"session_id": "resource:u_123:r_xxx",
"status": "extracted",
"title": "合同文件",
"description": "2026 年付款合同",
"created_at": "2026-06-10T10:00:00+00:00",
"updated_at": "2026-06-10T10:00:05+00:00"
}
]
}
如果当前用户没有上传过 resources、资源不存在、或资源属于其他用户,返回:
{
"resources": []
}
这种设计避免通过资源 ID 探测其他用户的数据。uri 同样只返回公开 resource://{user_id}/{resource_id},不会泄露内部 URI。
6. 删除资源
DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key}
第一版只做软删除:
- 设置
deleted_at = now()。 - 设置
status = deleted。 - 后续
resourcesscope 搜索会排除该资源的session_id。 - 清理 Gateway 自己在
MEMORY_GATEWAY_STORAGE_DIR下保存的原始上传文件。 - 不物理删除 EverOS 内部记忆或索引。
请求示例:
curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
响应示例:
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "deleted"
}
7. 搜索记忆
POST /memories/search
Content-Type: application/json
请求参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
user_id |
string | 是 | 无 | 用户 ID |
user_key |
string | 是 | 无 | 用户 key |
query |
string | 是 | 无 | 搜索问题 |
conversation_id |
string | 否 | null |
scope 包含 current_chat 时使用 |
scope |
string[] | 否 | ["current_chat", "resources"] |
搜索范围 |
top_k |
integer | 否 | 8 |
每次 EverOS 搜索返回数量,范围 1..100 |
app_id |
string | 否 | default |
EverOS app scope |
project_id |
string | 否 | default |
EverOS project scope |
scope 支持:
| scope | 说明 |
|---|---|
current_chat |
只搜索 chat:{conversation_id} |
resources |
搜索当前用户未删除且已提取完成的上传资源 |
all_user_memory |
搜索用户全部记忆,不加 session_id 过滤 |
请求示例:
curl -X POST http://127.0.0.1:8010/memories/search \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"conversation_id": "c_456",
"query": "合同里的付款条款是什么?",
"scope": ["current_chat", "resources"],
"top_k": 8,
"app_id": "default",
"project_id": "default"
}'
搜索编排逻辑:
current_chat:调用 EverOS search,过滤filters.session_id = chat:{conversation_id}。resources:先查当前用户的user_resources,只取status = extracted且未删除资源;再按批次调用 EverOS search,过滤这些资源的session_id。all_user_memory:调用 EverOS search,不加session_id过滤。- 合并结果。
- 过滤
memory_tombstones命中的memory_id或session_id。 - 应用 active
memory_overrides,把text替换为override_text。
响应示例:
{
"results": [
{
"id": "mem_abc",
"session_id": "resource:u_123:r_xxx",
"text": "付款期限为收到发票后 30 天内。",
"score": 0.82,
"source_scope": "resources",
"resource_id": "r_xxx",
"resource_uri": "resource://u_123/r_xxx",
"raw": {
"id": "mem_abc",
"session_id": "resource:u_123:r_xxx",
"episode": "原始 EverOS 返回内容"
}
}
]
}
8. 修改记忆
PATCH /memories/{memory_id}
Content-Type: application/json
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
session_id |
string | 是 | memory 所属 session,必须属于当前用户 |
override_text |
string | 是 | 修正后的记忆文本 |
该接口只写入或更新 memory_overrides,不会修改 EverOS 原始文件。写入前会校验 session_id 属于当前用户:当前版本支持当前用户的 resource:{user_id}:{resource_id} 和 memory_edit:{user_id}。后续搜索结果命中该 memory_id 时,返回的 text 会替换为 override_text,但保留原始 memory id。
请求示例:
curl -X PATCH http://127.0.0.1:8010/memories/mem_abc \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "resource:u_123:r_xxx",
"override_text": "修正后的记忆内容"
}'
响应示例:
{
"memory_id": "mem_abc",
"override_id": "o_xxx",
"status": "active"
}
9. 删除记忆
DELETE /memories/{memory_id}
Content-Type: application/json
请求参数:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
session_id |
string | 是 | memory 所属 session,必须属于当前用户 |
reason |
string | 否 | 删除原因 |
该接口只写入 memory_tombstones,不会修改 EverOS 原始文件。写入前会校验 session_id 属于当前用户:当前版本支持当前用户的 resource:{user_id}:{resource_id} 和 memory_edit:{user_id}。后续搜索结果如果命中 tombstone 的 memory_id 或 session_id,会被过滤。
请求示例:
curl -X DELETE http://127.0.0.1:8010/memories/mem_abc \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "resource:u_123:r_xxx",
"reason": "用户手动删除"
}'
响应示例:
{
"memory_id": "mem_abc",
"tombstone_id": "t_xxx",
"status": "deleted"
}
EverOS client 封装
Gateway 内部通过 core/everos_client.py 调用 EverOS:
add_memory(payload)->POST /api/v1/memory/addflush_memory(session_id, app_id, project_id)->POST /api/v1/memory/flushsearch_memory(payload)->POST /api/v1/memory/searchhealth_check()->GET /health
运行测试
cd /home/tom/memory-gateway2
python -B -m pytest -q -p no:cacheprovider