# Memory Gateway Memory Gateway 是一个轻量级 FastAPI 服务,位于调用方和上游 memory service 之间。它负责用户鉴权、文件接收、资源元数据管理、 附件映射、软删除、手动覆盖,以及按范围编排记忆搜索。 Gateway 不直接修改上游记忆服务的 Markdown、SQLite 或向量索引文件。它通过 上游现有 API 完成记忆写入、flush 和搜索: - `POST /api/v1/memory/add` - `POST /api/v1/memory/flush` - `POST /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` | ## 项目结构 ```text 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 ``` ## 安装和启动 安装依赖: ```bash cd /home/tom/memory-gateway uv pip install -e . ``` 启动 API: ```bash python main.py ``` 默认服务地址: ```text http://127.0.0.1:8010 ``` 健康检查: ```bash curl http://127.0.0.1:8010/health ``` 健康响应中: - `status: ok` 表示 Gateway 和上游服务都可访问。 - `status: degraded` 表示 Gateway 可访问,但上游服务不可访问。 ## 配置 配置来自环境变量。常用 `.env` 示例: ```bash 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`。 创建用户: ```bash curl -X POST http://127.0.0.1:8010/users \ -H 'Content-Type: application/json' \ -d '{"user_id":"u_123"}' ``` 响应: ```json { "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://`。 请求类型: ```http 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` 示例: ```json [ { "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 示例: ```bash 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' ``` 处理流程: 1. Gateway 校验用户。 2. 解析 `messages`。 3. 找到 content item 中的 `upload_id`。 4. 从 multipart 中读取同名文件字段。 5. 校验 MIME 和大小。 6. 保存文件到 `MEMORY_GATEWAY_STORAGE_DIR/{user_id}/memory_attachments/{sha256}/`。 7. 将 item 转成上游可消费的 `text` 或 `base64`,不透传客户端本机路径。 8. 调用上游 memory add。 9. 上游 add 成功后写入 `memory_attachments`,`source = memory_add_upload`。 该接口只追加消息,不自动 flush。需要抽取和索引时继续调用 `/memories/flush`。 ## 追加 JSON 记忆:`/memories/add` 用于调用方已经有纯文本、base64,或明确有上游可读取 URI 的场景。 请求: ```http POST /memories/add Content-Type: application/json ``` 示例: ```bash 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` 中: - `uri` item 会原样转发给上游,并登记附件映射。 - `base64` item 会保存到 Gateway storage,并登记生成的内部 `file://`。 - 如果 URI 是客户端本机路径,且上游读不到该路径,请改用 `/memories/add/multipart`。 ## Flush:`/memories/flush` ```bash 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" }' ``` 响应示例: ```json { "session_id": "chat:c_456", "backend": { "request_id": "request_id", "data": { "status": "extracted" } } } ``` ## 上传独立资源:`/resources` 用于把文件作为长期资源纳入 `resources` 搜索范围。 请求类型: ```http 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 | 是 | 上传文件 | 示例: ```bash 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' ``` 响应: ```json { "resource_id": "r_xxx", "session_id": "resource:u_123:r_xxx", "uri": "resource://u_123/r_xxx", "status": "extracted" } ``` `/resources` 会: 1. 保存文件到 Gateway storage。 2. 写入 `user_resources`。 3. 写入 `memory_attachments`。 4. 构造上游 content item;文本用 `text`,二进制用 `base64`。 5. 自动调用上游 add 和 flush。 6. 成功后状态为 `extracted`。 同一用户、同一 app/project 下相同 sha256 的活跃资源会复用已有资源。 ## 登记外部资源:`/resources/external` 用于文件已经在外部存储中,Gateway 只保存元数据和 URI 映射。 示例: ```bash 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`。 ## 资源读取和删除 列出资源: ```bash curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx" ``` 读取资源详情: ```bash curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx" ``` 软删除资源: ```bash 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` 请求: ```bash 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 过滤 | 返回结果会标准化为: ```json { "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 文本: ```bash 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: ```bash 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: ```bash python skill/memory-gateway-agent/scripts/memory_gateway.py --help ``` 常用环境变量: ```bash 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 ``` 示例: ```bash 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://` 路径。 ## 测试 运行自动化测试: ```bash uv run pytest -q ``` 当前验证结果: ```text 56 passed, 2 skipped ``` 运行聚焦测试: ```bash uv run pytest tests/test_gateway.py -k "add_memory or upload_resource or attachment" -q ``` 当前验证结果: ```text 16 passed, 33 deselected ``` 真实部署命令记录: ```text tests/test_command.md ``` 该文件记录了 2026-06-22 对已部署 Gateway 和上游 memory service 执行的真实 curl 命令,包括: - `GET /health` - `POST /memories/add/multipart` - `POST /memories/flush` - `POST /memories/search` - `POST /resources` - `GET /resources` - resources scope 搜索 ## 开发注意事项 - 保持 Gateway 与上游服务职责分离;不要直接写上游内部文件。 - 新上传入口应优先使用 multipart 或外部存储 URI,避免要求客户端构造 base64。 - 不要在响应中泄露内部 `file://`,除搜索附件映射 `attachments[].internal_uri` 外。 - 资源详情和列表返回公开 `resource://{user_id}/{resource_id}`。 - 删除资源只做 Gateway 层软删除和本地文件清理,不直接删除上游记忆。 - 搜索必须保持用户隔离,尤其是附件映射和资源 session。