diff --git a/docs/superpowers/specs/2026-06-15-memory-attachment-path-mapping-design.md b/docs/superpowers/specs/2026-06-15-memory-attachment-path-mapping-design.md new file mode 100644 index 0000000..5db655b --- /dev/null +++ b/docs/superpowers/specs/2026-06-15-memory-attachment-path-mapping-design.md @@ -0,0 +1,101 @@ +# Memory 附件真实路径映射设计 + +## 目标 + +让 `/resources` 和 `/memories/add` 两种摄入方式都保存附件与 session 的映射。 +`/memories/search` 返回结果时,根据结果 `session_id` 查询当前用户附件,并且只有 +当附件完整文件名出现在结果 `raw` 的字符串字段中时,才返回该附件真实 URI。 + +## 数据模型 + +新增 SQLite 表 `memory_attachments`: + +- `id TEXT PRIMARY KEY` +- `user_id TEXT NOT NULL` +- `app_id TEXT NOT NULL DEFAULT 'default'` +- `project_id TEXT NOT NULL DEFAULT 'default'` +- `session_id TEXT NOT NULL` +- `resource_id TEXT` +- `content_type TEXT NOT NULL` +- `name TEXT NOT NULL` +- `internal_uri TEXT NOT NULL` +- `source TEXT NOT NULL` +- `sha256 TEXT` +- `created_at TIMESTAMP NOT NULL` +- `deleted_at TIMESTAMP` + +以 `(user_id, session_id, internal_uri)` 建立唯一索引,避免幂等上传产生重复映射; +以 `(user_id, session_id, deleted_at)` 建立查询索引。 + +数据库初始化时,将现有未删除 `user_resources` 回填为附件映射。历史 +`/memories/add` 请求没有保存在 Gateway 数据库中,因此无法自动回填。 + +## 摄入规则 + +### `/resources` + +资源记录创建后,为保存的真实 `file://` URI 创建附件映射: + +- `session_id` 使用 `resource:{user_id}:{resource_id}`; +- `resource_id` 指向资源; +- `source` 为 `resource_upload`; +- `content_type`、文件名、SHA256 复用资源元数据。 + +重复资源上传时确保已有资源对应的附件映射存在。 + +### `/memories/add` + +API 将已鉴权的 `user_id` 一并传给 service。逐条检查 message 的 content item: + +- 只有字符串 content 或纯文本 item 时不创建附件; +- 有 `uri` 时记录该 URI,`source=memory_add_uri`; +- 没有 `uri` 但有 `base64` 时,解码并保存到 + `storage/{user_id}/memory_attachments/{attachment_id}/{safe_name}`,记录生成的 + `file://` URI,`source=memory_add_base64`; +- 同时存在 `uri` 和 `base64` 时优先使用 `uri`,不重复落盘; +- 文件名优先使用 `name`,否则从 URI 路径或 `ext` 生成安全名称。 + +上游 add 调用失败时,删除本次 base64 生成的文件,不写入映射。调用成功后写入 +附件映射。上游请求体保持原样,不修改现有 add 行为。 + +## 搜索匹配规则 + +对每条标准化搜索结果: + +1. 根据已鉴权 `user_id` 和结果 `session_id` 查询未删除附件; +2. 递归遍历 `raw` 中 dict、list 的字符串值; +3. 跳过键名为 `base64` 的值,避免扫描大块编码数据; +4. 使用附件完整文件名做不区分大小写的子串匹配; +5. 仅命中的附件进入 `attachments`,按 `internal_uri` 去重; +6. 没有 session 或没有命中时返回 `attachments: []`。 + +响应附件格式: + +```json +{ + "type": "image", + "name": "simple-multimodal-image.png", + "internal_uri": "file:///home/tom/memory-gateway/tests/simple-multimodal-image.png" +} +``` + +episode 是 session 级记忆,因此只能在同一 session 的附件中按文件名匹配,不能 +证明具体附件是向量召回的直接来源。 + +## 删除与隔离 + +- 所有附件查询必须同时匹配 `user_id` 和 `session_id`; +- 删除 `/resources` 时,对应附件映射设置 `deleted_at`; +- 真实路径按用户明确要求直接出现在搜索结果中; +- 不改变资源列表和详情现有的 `resource://` 对外 URI。 + +## 测试 + +- 资源上传创建附件映射; +- 资源搜索仅在 raw 出现文件名时返回真实 URI; +- raw 不含文件名时返回空附件数组; +- `/memories/add` 的 URI content 创建映射; +- `/memories/add` 的 base64 content 落盘并创建映射; +- 不扫描 raw 中的 base64 字段; +- 不返回其他用户同 session 的附件; +- 现有测试继续通过。