# 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` 完成资源记忆摄入。 - 提供资源列表、详情、软删除。 - 编排记忆搜索,支持当前聊天、资源记忆、全部用户记忆。 - 支持记忆 tombstone 软删除。 - 支持记忆手动 override。 - 搜索结果返回前自动过滤 tombstone 并应用 override。 ## 目录结构 ```text /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 ``` ## 环境配置 复制示例配置: ```bash 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_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 的内部存储目录。 ## 安装依赖 ```bash cd /home/tom/memory-gateway2 uv pip install -e . ``` ## 启动 API 使用 Python 启动: ```bash cd /home/tom/memory-gateway2 python main.py ``` 默认监听: ```text http://127.0.0.1:8010 ``` ## session_id 规范 Gateway 遵循以下 `session_id` 规范: | 场景 | 格式 | |---|---| | 普通聊天 | `chat:{conversation_id}` | | 用户上传资源 | `resource:{user_id}:{resource_id}` | | 用户手动修正 | `memory_edit:{user_id}` | 当前实现中,资源上传会自动生成: ```text 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. 健康检查 ```http GET /health ``` 该接口不需要 `user_id` 或 `user_key`,用于确认 Gateway API 是否可响应,以及上游 EverOS 是否可访问。 请求示例: ```bash curl http://127.0.0.1:8010/health ``` EverOS 正常时响应示例: ```json { "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 故障”: ```json { "status": "degraded", "api": { "status": "ok" }, "everos": { "status": "unavailable", "base_url": "http://127.0.0.1:8000", "error": "Connection refused" } } ``` ### 2. 创建用户 ```http POST /users Content-Type: application/json ``` 请求参数: | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `user_id` | string | 是 | 用户 ID | 请求示例: ```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-10T10:00:00+00:00" } ``` `user_key` 需要由调用方保存,后续上传、查询、搜索、修改和删除都要传入。如果同一个 `user_id` 已存在,接口会返回已有 `user_key`。 ### 3. 上传资源 ```http 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` | 资源描述 | 处理流程: 1. 保存原始文件到 `MEMORY_GATEWAY_STORAGE_DIR`。 2. 计算 `sha256` 和 `size_bytes`。 3. 生成 `resource_id`。 4. 生成 `session_id = resource:{user_id}:{resource_id}`。 5. 写入 `user_resources`,状态为 `ingesting`。 6. 根据 MIME 类型映射 EverOS content type。 7. 调用 EverOS `/api/v1/memory/add`。 8. 调用 EverOS `/api/v1/memory/flush`。 9. 成功后状态改为 `extracted`,失败后状态改为 `failed`。 content type 映射: | 文件类型 | EverOS content type | |---|---| | `image/*` | `image` | | `audio/*` | `audio` | | PDF | `pdf` | | HTML | `html` | | 纯文本、Markdown、CSV、日志 | `text` | | 其他文档 | `doc` | 请求示例: ```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="合同文件" \ -F description="2026 年付款合同" \ -F file=@./contract.pdf ``` 响应示例: ```json { "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. 查询资源列表 ```http GET /resources?user_id={user_id}&user_key={user_key} ``` 参数: | 参数 | 类型 | 必填 | 说明 | |---|---|---|---| | `user_id` | string | 是 | 用户 ID | | `user_key` | string | 是 | 用户 key | 只返回当前用户 `deleted_at IS NULL` 的资源。不同用户的资源彼此隔离。 请求示例: ```bash curl "http://127.0.0.1:8010/resources?user_id=u_123&user_key=uk_xxx" ``` 响应示例: ```json { "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" } ] } ``` 如果用户没有上传过资源,返回: ```json { "resources": [] } ``` ### 5. 查询资源详情 ```http 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 | 请求示例: ```bash curl "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx" ``` 响应示例: ```json { "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、资源不存在、或资源属于其他用户,返回: ```json { "resources": [] } ``` 这种设计避免通过资源 ID 探测其他用户的数据。`uri` 同样只返回公开 `resource://{user_id}/{resource_id}`,不会泄露内部 URI。 ### 6. 删除资源 ```http DELETE /resources/{resource_id}?user_id={user_id}&user_key={user_key} ``` 第一版只做软删除: - 设置 `deleted_at = now()`。 - 设置 `status = deleted`。 - 后续 `resources` scope 搜索会排除该资源的 `session_id`。 - 不物理删除 EverOS 内部记忆或索引。 请求示例: ```bash curl -X DELETE "http://127.0.0.1:8010/resources/r_xxx?user_id=u_123&user_key=uk_xxx" ``` 响应示例: ```json { "resource_id": "r_xxx", "session_id": "resource:u_123:r_xxx", "uri": "resource://u_123/r_xxx", "status": "deleted" } ``` ### 7. 搜索记忆 ```http 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` 过滤 | 请求示例: ```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"], "top_k": 8, "app_id": "default", "project_id": "default" }' ``` 搜索编排逻辑: 1. `current_chat`:调用 EverOS search,过滤 `filters.session_id = chat:{conversation_id}`。 2. `resources`:先查当前用户的 `user_resources`,只取 `status = extracted` 且未删除资源;再按批次调用 EverOS search,过滤这些资源的 `session_id`。 3. `all_user_memory`:调用 EverOS search,不加 `session_id` 过滤。 4. 合并结果。 5. 过滤 `memory_tombstones` 命中的 `memory_id` 或 `session_id`。 6. 应用 active `memory_overrides`,把 `text` 替换为 `override_text`。 响应示例: ```json { "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. 修改记忆 ```http 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 原始文件。后续搜索结果命中该 `memory_id` 时,返回的 `text` 会替换为 `override_text`,但保留原始 memory id。 请求示例: ```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": "resource:u_123:r_xxx", "override_text": "修正后的记忆内容" }' ``` 响应示例: ```json { "memory_id": "mem_abc", "override_id": "o_xxx", "status": "active" } ``` ### 9. 删除记忆 ```http 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 原始文件。后续搜索结果如果命中 tombstone 的 `memory_id` 或 `session_id`,会被过滤。 请求示例: ```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": "resource:u_123:r_xxx", "reason": "用户手动删除" }' ``` 响应示例: ```json { "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/add` - `flush_memory(session_id, app_id, project_id)` -> `POST /api/v1/memory/flush` - `search_memory(payload)` -> `POST /api/v1/memory/search` - `health_check()` -> `GET /health` ## 运行测试 ```bash cd /home/tom/memory-gateway2 python -B -m pytest -q -p no:cacheprovider ```