Memory Gateway
Memory Gateway 是一个轻量级 FastAPI 服务,位于调用方和上游 memory service 之间。它负责用户鉴权、文件接收、资源元数据管理、 附件映射、软删除、手动覆盖,以及按范围编排记忆搜索。
Gateway 不直接修改上游记忆服务的 Markdown、SQLite 或向量索引文件。它通过 上游现有 API 完成记忆写入、flush 和搜索:
POST /api/v1/memory/addPOST /api/v1/memory/flushPOST /api/v1/memory/search
适用场景
- 客户端要把聊天消息写入记忆。
- 客户端要在同一个聊天 session 中上传图片、音频或文件,但不能自行转 base64。
- 用户要上传独立资源,如图片、音频、PDF、HTML、文档、文本文件。
- 文件已经在外部存储中,需要登记长期 URI 和本次摄入 URI。
- 搜索时需要同时覆盖当前聊天、资源记忆、全部用户记忆。
- 需要在 Gateway 层实现用户隔离、资源软删除、记忆 tombstone 和人工覆盖。
关键原则
file:// 不是上传协议。它只表示“某台机器上的本地路径”。如果客户端和 Gateway
不在同一台机器、同一个容器、或同一个挂载路径下,直接把客户端本机 file://
传给 Gateway 或上游服务会失败。
正确选择:
| 需求 | 推荐接口 |
|---|---|
| 聊天消息里带本地文件,且客户端不能转 base64 | POST /memories/add/multipart |
| 上传一个独立、长期可检索资源 | POST /resources |
| 文件已经在 MinIO/S3/Beaver user_files 等外部存储 | POST /resources/external |
| 调用方已经有 base64 或上游可访问 URI | POST /memories/add |
项目结构
memory-gateway/
├── core/
│ ├── api.py # FastAPI 路由、请求校验、HTTP 错误映射
│ ├── backend_client.py # 上游 memory service HTTP client
│ ├── config.py # 环境变量配置
│ ├── db.py # SQLite schema 初始化
│ ├── repository.py # SQLite 读写
│ └── service.py # 业务编排、上传、附件、搜索、软删除
├── main.py # uvicorn 启动入口
├── skill/ # Memory Gateway agent skill 和 CLI
├── tests/ # 单元、集成、实测命令记录
├── pyproject.toml
└── uv.lock
安装和启动
安装依赖:
cd /home/tom/memory-gateway
uv pip install -e .
启动 API:
python main.py
默认服务地址:
http://127.0.0.1:8010
健康检查:
curl http://127.0.0.1:8010/health
健康响应中:
status: ok表示 Gateway 和上游服务都可访问。status: degraded表示 Gateway 可访问,但上游服务不可访问。
配置
配置来自环境变量。常用 .env 示例:
MEMORY_GATEWAY_BACKEND_BASE_URL=http://127.0.0.1:1995
MEMORY_GATEWAY_DB_PATH=./data/memory_gateway.sqlite3
MEMORY_GATEWAY_STORAGE_DIR=./data/storage
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE=50
MEMORY_GATEWAY_MAX_UPLOAD_BYTES=26214400
MEMORY_GATEWAY_ALLOWED_MIME_TYPES=image/*,audio/*,application/pdf,text/html,application/xhtml+xml,text/plain,text/markdown,text/csv,application/json,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation
MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS=3
MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS=0.25
MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS=120
MEMORY_GATEWAY_HOST=127.0.0.1
MEMORY_GATEWAY_PORT=8010
MEMORY_GATEWAY_RELOAD=false
配置说明:
| 变量 | 默认值 | 说明 |
|---|---|---|
MEMORY_GATEWAY_BACKEND_BASE_URL |
http://127.0.0.1:1995 |
上游 memory service 地址 |
MEMORY_GATEWAY_DB_PATH |
./data/memory_gateway.sqlite3 |
Gateway SQLite 数据库 |
MEMORY_GATEWAY_STORAGE_DIR |
./data/storage |
Gateway 保存上传文件的位置 |
MEMORY_GATEWAY_RESOURCE_SEARCH_BATCH_SIZE |
50 |
resources 搜索每批 session 数量 |
MEMORY_GATEWAY_MAX_UPLOAD_BYTES |
26214400 |
单文件上传大小限制,默认 25 MiB |
MEMORY_GATEWAY_ALLOWED_MIME_TYPES |
常见图片、音频、PDF、HTML、文本、Office 文档 | MIME 白名单,支持 image/* |
MEMORY_GATEWAY_BACKEND_INGEST_ATTEMPTS |
3 |
上游 add/flush 重试次数 |
MEMORY_GATEWAY_BACKEND_RETRY_DELAY_SECONDS |
0.25 |
上游重试间隔 |
MEMORY_GATEWAY_BACKEND_TIMEOUT_SECONDS |
120 |
上游请求超时 |
MEMORY_GATEWAY_HOST |
127.0.0.1 |
Gateway 监听地址 |
MEMORY_GATEWAY_PORT |
8010 |
Gateway 监听端口 |
MEMORY_GATEWAY_RELOAD |
false |
是否启用 uvicorn reload |
不要把 Gateway 的数据库或 storage 配到上游服务内部目录。Gateway 只管理自己的状态。
数据模型
Gateway 启动时自动创建 SQLite 表:
| 表 | 用途 |
|---|---|
users |
用户 ID 和 Gateway 生成的 user_key |
user_resources |
独立资源元数据、状态、内部 URI、软删除时间 |
memory_attachments |
聊天/资源 session 到真实附件 URI 的映射 |
memory_tombstones |
用户删除的 memory id 或 session_id |
memory_overrides |
用户人工修正后的 memory 文本 |
附件来源 source:
| source | 来源 |
|---|---|
resource_upload |
/resources 上传 |
external_resource |
/resources/external 登记 |
memory_add_uri |
/memories/add 中的 URI item |
memory_add_base64 |
/memories/add 中的 base64 item |
memory_add_upload |
/memories/add/multipart 中的上传文件 |
Session 约定
| 场景 | session_id |
|---|---|
| 普通聊天 | chat:{conversation_id} |
| 独立上传资源 | resource:{user_id}:{resource_id} |
| 手动修正 | memory_edit:{user_id} |
/resources 会自动生成资源 session。/memories/add 和
/memories/add/multipart 使用调用方传入的 session。
鉴权
除 POST /users 和 GET /health 外,所有业务接口都需要 user_id 和
user_key。认证失败返回 401。
创建用户:
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-22T06:54:35.823262+00:00"
}
同一个 user_id 重复创建会返回已有用户记录。
API 概览
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/health |
Gateway 和上游健康检查 |
POST |
/users |
创建或读取用户 key |
POST |
/resources |
multipart 上传独立资源 |
POST |
/resources/external |
登记外部资源 |
GET |
/resources |
列出用户资源 |
GET |
/resources/{resource_id} |
读取资源详情 |
DELETE |
/resources/{resource_id} |
软删除资源 |
POST |
/memories/add |
JSON 追加记忆消息 |
POST |
/memories/add/multipart |
multipart 追加消息并上传附件 |
POST |
/memories/flush |
flush 指定 session |
POST |
/memories/search |
编排搜索 |
PATCH |
/memories/{memory_id} |
人工覆盖 memory 文本 |
DELETE |
/memories/{memory_id} |
tombstone 删除 memory |
上传聊天附件:/memories/add/multipart
用于“同一个聊天 session 中发送消息并上传附件”。客户端不需要转 base64,也不需要
传 file://。
请求类型:
POST /memories/add/multipart
Content-Type: multipart/form-data
表单字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
session_id |
string | 是 | 通常是 chat:{conversation_id} |
app_id |
string | 否 | 默认 default |
project_id |
string | 否 | 默认 default |
messages |
string 或 JSON file | 是 | JSON array,结构同 /memories/add |
| 动态文件字段 | file | 条件必填 | 如果 content item 包含 upload_id,必须上传同名文件字段 |
upload_id 由调用方自定义。Gateway 不会随机生成,也不要求固定格式
(例如 user_id_filetype_number)。它只需要在同一次请求里非空、唯一,并且和
multipart 文件字段名一致。推荐使用简单可读的值,如 image_1、audio_1、
doc_1。
messages 示例:
[
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1782111275810,
"content": [
{"type": "text", "text": "请记住这张图片和音频"},
{
"type": "image",
"upload_id": "image_1",
"name": "simple-multimodal-image.png",
"ext": "png"
},
{
"type": "audio",
"upload_id": "audio_1",
"name": "simple-tone.wav",
"ext": "wav"
}
]
}
]
curl 示例:
curl -X POST http://127.0.0.1:8010/memories/add/multipart \
-F user_id=u_123 \
-F user_key=uk_xxx \
-F session_id=chat:c_456 \
-F app_id=default \
-F project_id=default \
-F 'messages=@messages.json;type=application/json' \
-F 'image_1=@tests/simple-multimodal-image.png;type=image/png' \
-F 'audio_1=@tests/simple-tone.wav;type=audio/wav'
处理流程:
- Gateway 校验用户。
- 解析
messages。 - 找到 content item 中的
upload_id。 - 从 multipart 中读取同名文件字段。
- 校验 MIME 和大小。
- 保存文件到
MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/。 - 将 item 转成上游可消费的
text或base64,不透传客户端本机路径。 - 调用上游 memory add。
- 上游 add 成功后写入
memory_attachments,source = memory_add_upload。
该接口只追加消息,不自动 flush。需要抽取和索引时继续调用 /memories/flush。
追加 JSON 记忆:/memories/add
用于调用方已经有纯文本、base64,或明确有上游可读取 URI 的场景。
请求:
POST /memories/add
Content-Type: application/json
示例:
curl -X POST http://127.0.0.1:8010/memories/add \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "chat:c_456",
"app_id": "default",
"project_id": "default",
"messages": [
{
"sender_id": "u_123",
"role": "user",
"timestamp": 1782111275810,
"content": [
{"type": "text", "text": "用户喜欢简洁的中文回答"},
{
"type": "audio",
"base64": "BASE64_DATA",
"ext": "wav",
"name": "tone.wav"
}
]
}
]
}'
/memories/add 中:
uriitem 会原样转发给上游,并登记附件映射。base64item 会保存到 Gateway storage,并登记生成的内部file://。- 如果 URI 是客户端本机路径,且上游读不到该路径,请改用
/memories/add/multipart。
Flush:/memories/flush
curl -X POST http://127.0.0.1:8010/memories/flush \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"session_id": "chat:c_456",
"app_id": "default",
"project_id": "default"
}'
响应示例:
{
"session_id": "chat:c_456",
"backend": {
"request_id": "request_id",
"data": {
"status": "extracted"
}
}
}
上传独立资源:/resources
用于把文件作为长期资源纳入 resources 搜索范围。
请求类型:
POST /resources
Content-Type: multipart/form-data
表单字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
user_id |
string | 是 | 用户 ID |
user_key |
string | 是 | 用户 key |
app_id |
string | 否 | 默认 default |
project_id |
string | 否 | 默认 default |
title |
string | 否 | 资源标题 |
description |
string | 否 | 资源描述 |
file |
file | 是 | 上传文件 |
示例:
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="Gateway image resource" \
-F description="Example image resource" \
-F 'file=@tests/simple-multimodal-image.png;type=image/png'
响应:
{
"resource_id": "r_xxx",
"session_id": "resource:u_123:r_xxx",
"uri": "resource://u_123/r_xxx",
"status": "extracted"
}
/resources 会:
- 保存文件到 Gateway storage。
- 写入
user_resources。 - 写入
memory_attachments。 - 构造上游 content item;文本用
text,二进制用base64。 - 自动调用上游 add 和 flush。
- 成功后状态为
extracted。
同一用户、同一 app/project 下相同 sha256 的活跃资源会复用已有资源。
登记外部资源:/resources/external
用于文件已经在外部存储中,Gateway 只保存元数据和 URI 映射。
示例:
curl -X POST http://127.0.0.1:8010/resources/external \
-H 'Content-Type: application/json' \
-d '{
"user_id": "u_123",
"user_key": "uk_xxx",
"app_id": "default",
"project_id": "default",
"filename": "chart.png",
"mime_type": "image/png",
"size_bytes": 12345,
"sha256": "abc123",
"source_uri": "minio://bucket/users/u_123/chart.png",
"ingest_uri": "https://minio.example/presigned/chart.png",
"title": "chart.png",
"description": "External image"
}'
字段含义:
source_uri:长期保存的真实映射 URI。ingest_uri:上游本次摄入可读取的 URI,通常是短期 presigned URL。
Gateway 不会下载 source_uri 或 ingest_uri。
资源读取和删除
列出资源:
curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx"
读取资源详情:
curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
软删除资源:
curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx"
删除资源会:
- 设置资源
status = deleted。 - 设置资源
deleted_at。 - 软删除相关附件映射。
- 清理 Gateway storage 中属于该资源的本地文件。
- 不直接删除上游记忆服务内部文件或索引。
搜索:/memories/search
请求:
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"],
"method": "hybrid",
"top_k": 8,
"include_profile": true,
"enable_llm_rerank": true,
"app_id": "default",
"project_id": "default"
}'
搜索 scope:
| scope | 行为 |
|---|---|
current_chat |
搜索 chat:{conversation_id},需要传 conversation_id |
resources |
搜索当前用户已提取且未删除的资源 session |
all_user_memory |
搜索用户全部记忆,不加 session 过滤 |
返回结果会标准化为:
{
"results": [
{
"id": "memory_id",
"session_id": "chat:c_456",
"text": "memory text",
"score": 0.61,
"source_scope": "current_chat",
"resource_id": null,
"resource_uri": null,
"attachments": [
{
"type": "image",
"name": "image.png",
"internal_uri": "file:///..."
}
],
"raw": {}
}
]
}
附件匹配规则:
- Gateway 只返回当前用户、当前 session 的附件映射。
- Gateway 根据搜索结果 raw/text 中出现的文件名匹配附件。
base64字段内容不会参与匹配。- 未匹配到附件时不返回
attachments。
手动覆盖和删除记忆
覆盖 memory 文本:
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": "chat:c_456",
"override_text": "修正后的记忆文本"
}'
删除 memory:
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": "chat:c_456",
"reason": "用户要求删除"
}'
Gateway 会校验 session 归属,防止用户覆盖或删除其他用户资源 session 中的记忆。
/memories/add/multipart 与 /resources 的区别
| 对比项 | /memories/add/multipart |
/resources |
|---|---|---|
| 目标 | 聊天 session 中的消息附件 | 独立长期资源 |
| session | 调用方传入,如 chat:c_456 |
Gateway 生成 resource:{user_id}:{resource_id} |
| 文件字段 | 动态字段名匹配 upload_id |
固定字段 file |
| 元数据表 | 不写 user_resources |
写 user_resources |
| 附件 source | memory_add_upload |
resource_upload |
| 上游调用 | 只 add,调用方按需 flush | Gateway 自动 add + flush |
| 常用搜索 | current_chat 或 all_user_memory |
resources |
错误码
| 状态码 | 常见原因 |
|---|---|
400 |
messages 不是合法 JSON array |
401 |
user_id 或 user_key 无效 |
403 |
操作的 session 不属于当前用户 |
404 |
删除资源时资源不存在 |
413 |
上传文件超过 MEMORY_GATEWAY_MAX_UPLOAD_BYTES |
415 |
MIME 类型不在白名单 |
422 |
请求字段缺失、upload_id 缺文件、重复文件字段、无效 base64 |
API 日志
Gateway 使用 memory_gateway.api logger 输出 JSON 日志:
- 请求时间、耗时、方法、路径、URL、客户端。
- Query 参数和小型请求体。
user_key、token、password、secret、API key 等敏感字段会遮蔽。- multipart 请求只记录 content type 和大小,不记录文件内容。
- 响应体会按同样规则遮蔽敏感字段。
Agent CLI
项目包含 Memory Gateway agent skill CLI:
python skill/memory-gateway-agent/scripts/memory_gateway.py --help
常用环境变量:
export MEMORY_GATEWAY_BASE_URL=http://127.0.0.1:8010
export MEMORY_GATEWAY_USER_ID=u_123
export MEMORY_GATEWAY_USER_KEY=uk_xxx
示例:
CLI="python skill/memory-gateway-agent/scripts/memory_gateway.py"
$CLI health
$CLI create-user u_123
$CLI upload-resource ./document.pdf --title "Document"
$CLI search "payment terms" --scope resources
$CLI add-memory --session-id chat:c_456 --messages ./messages.json
$CLI flush-memory --session-id chat:c_456
CLI 的 upload-resource 使用 multipart,不会暴露本地 file:// 路径。
测试
运行自动化测试:
uv run pytest -q
当前验证结果:
56 passed, 2 skipped
运行聚焦测试:
uv run pytest tests/test_gateway.py -k "add_memory or upload_resource or attachment" -q
当前验证结果:
16 passed, 33 deselected
真实部署命令记录:
tests/test_command.md
该文件记录了 2026-06-22 对已部署 Gateway 和上游 memory service 执行的真实 curl 命令,包括:
GET /healthPOST /memories/add/multipartPOST /memories/flushPOST /memories/searchPOST /resourcesGET /resources- resources scope 搜索
开发注意事项
- 保持 Gateway 与上游服务职责分离;不要直接写上游内部文件。
- 新上传入口应优先使用 multipart 或外部存储 URI,避免要求客户端构造 base64。
- 不要在响应中泄露内部
file://,除搜索附件映射attachments[].internal_uri外。 - 资源详情和列表返回公开
resource://{user_id}/{resource_id}。 - 删除资源只做 Gateway 层软删除和本地文件清理,不直接删除上游记忆。
- 搜索必须保持用户隔离,尤其是附件映射和资源 session。