Skip to main content
Glama

img.png

recall.select

一个极简的智能体记忆系统 —— 向任意智能体提供一个 URL,它就能获得近乎零配置的长期记忆。基于 Qdrant + FastMCP + FastAPI/Bootstrap 构建。

完整设计及增量构建计划请参见 docs/specs/initial_specification.md,重要变更记录请参见 docs/specs/changelog.md。

工作原理

记忆以向量形式存储。每个记忆存储区是一个 Qdrant 集合,与 (用户, 项目) 对一一映射。这些向量相关的元数据(用户、API 密钥、项目以及每个集合的使用/限制统计)存储在 MongoDB 中。

flowchart LR
    agent[Agent] --> web[FastAPI / MCP]
    web <-->qdrant[Qdrant]
    web <--> mongo[MongoDB]
    web <--> embed[Embedding API]

Qdrant 集合采用惰性创建:在首次将记忆存入 (用户, 项目) 对之前,不会对 Qdrant 进行任何操作。

Related MCP server: Firmament Open Brain

架构

  • app/main.py — FastAPI 应用。提供 Bootstrap 着陆页,并在启动时确保 MongoDB 索引存在(容忍冷/远程数据库)。

  • app/mcp_server.py — 记忆链接背后的 MCP 服务器。智能体的 MCP 客户端连接到 {PUBLIC_BASE_URL}/m/{key}(Streamable HTTP,无状态,JSON 响应);路径中的 API 密钥是完整的凭证,并将工具范围限定为密钥所有者的默认项目。基本工具:store_memory / recall_memory / delete_memory。语义层工具(参见 vector_semantics.py):link_memories / unlink_memories / annotate_memory / memory_connections / recall_connected — 连接的智能体在客户端进行关系推理(仅在明确请求时),这些工具负责接收或遍历结果。相同的密钥也可作为 Authorization: Bearer 发送到无密钥的 /mcp 端点,从而将密钥隐藏在 URL/日志之外。{...}/m/{key}.md(位于 app/api/connect.py)提供相应的设置说明(两种形式)。

  • app/dependencies.py — 核心 DI 容器(injector)。构建共享单例(Qdrant 客户端、Mongo 客户端/数据库、远程嵌入器)。FastAPI 依赖项(app/api/deps.py)和启动逻辑从 app_container 解析,而不是自行构建客户端。

  • app/services/ — 服务层(无 HTTP/路由代码,仅 I/O):

    • qdrant_store.py — Qdrant 客户端 + ensure_collection / upsert_memory / search / delete_memory,以及语义层所需的点级原语(neighbors, scroll_points, retrieve_points, set_payload)。

    • vector_semantics.py — 向量记忆工具层:将存储区视为意义图。每个点载荷中保留一个 _semantics 命名空间,用于存放指示词锚点(所有者、存储时间;在存储时写入)、客户端提取的实体以及客户端声明的类型化关系(upsert_relations 进行验证并存储 —— 服务端不调用 LLM)。声明的关系带有两个质量约束:confidence(0-1],缩放边的遍历强度)和 valid_till(ISO 8601;过期的边被所有读取路径忽略,因此陈旧结构会自动退役)。卫生机制:remove_relations 删除错误的边(upsert_relations 的纠正对应项),memory.delete_memory 调用 prune_relations_to 以确保删除记忆后没有悬空边。 可插拔的透镜(topical / temporal / entity / declared)推导类型化边;在此基础上构建 semantic_graph(多重图)、spreading_activation(按连接检索)、concept_clusters(涌现本体)和 infer_relation(声明真理优先,几何启发式次之)。性能说明:入边查找(relations_of(include_incoming=True))目前是有限滚动扫描。如果反向遍历成为热点,解决方法是 Qdrant 载荷索引(_semantics.relations[].target,create_payload_index,关键字模式)和过滤查询替代扫描 —— 同一存储区,仅加索引;模式无需更改。

    • mongo.py — Mongo 客户端,get_db() 和 ensure_indexes()(通过唯一复合索引强制执行 (用户, 项目) 的一对一规则)。

    • users.py — add_user, get_user, get_user_by_email, update_user。

    • api_keys.py — 用户绑定的密钥,以 SHA-256 哈希值存储(明文在 add_api_key 时返回一次,此后从不持久化):add_api_key, delete_api_key, delete_user_keys, list_api_keys, get_labeled_key, get_by_key(对提供的令牌进行哈希,并与摘要匹配;MCP 认证门上的 record_use=True 会标记 last_used_at)。静态存储的每个密钥还保留非秘密显示提示 —— key_prefix + key_last4,通过 masked() 呈现为 rs_ab12…wxyz —— 因此可以列出和区分密钥,而无需重新暴露密钥本身。

    • projects.py — add_project, get_project, list_projects, update_project, delete_project。

    • collections.py — (用户, 项目) ↔ Qdrant 集合 注册表。collection_name(user_id, project_id) 是内部命名标准(rs_{user}_{project});跟踪 points_count / calls_count 用于限制和统计。

    • collection_provisioning.py — 双面 create_collection / destroy_collection 步骤。集合仅在它的 MongoDB 注册行和其支持的 Qdrant 集合都存在时才存在;此函数将 collections 注册表与 qdrant_store 组合成一个原子、幂等的操作,使两个存储区永不脱节。创建是惰性的,因此唯一的创建调用者是首次记忆写入(memory.store_memory);集合 API 的删除使用 destroy_collection。

    • embeddings.py — Embedder 抽象;embeddings_remote.py — 具体的文本→向量后端(远程嵌入 API,例如 DeepInfra)。

    • monobank.py — 极简的 Monobank 收单客户端(create_invoice, fetch_invoice_status)以及 Webhook 认证(fetch_pubkey / verify_signature,ECDSA-SHA256 对原始主体签名)。复用 mcp-api.net 商户令牌;recall.select 拥有自己的发票/重定向/Webhook。

    • billing.py — 计划目录和以 Monobank 的 invoiceId 为键的付款记录。结账时 record_pending;apply_webhook 在 success 时一次性将买家的 tier 翻转(对重试/重复具有幂等性);reconcile 处理 Webhook 遗漏的情况(见下文)。层级是有时间限制的:grant_tier 是权力发放的唯一入口(付费发票或所有者善意),写入 tier_expires_at 以及 tier_grants 中的审计行;effective_tier(user) 是所有检查必须读取的内容,因为存储的 paid_2x 如果日期已过,则属于免费账户。同时,这是每个层级配额的单一事实来源:call_allowance(tier) / project_allowance(tier)(None = 无限;未知层级回退到免费)。

    • usage.py — 月度调用计量器和价格模型门控。每个接受的 store/recall/delete 操作都会计入每个 (用户, 日历月份) 的 usage 行;check_call_allowed 一旦层级月度 call_allowance 耗尽,便会拒绝调用,抛出 QuotaExceeded。在 memory.py 中实施(因此 MCP 工具和 HTTP 记忆 API 均被覆盖),并在 app/main.py 映射为 HTTP 429;MCP 传输将其作为工具错误呈现。与全部时间的 collections.calls_count 分开。

    • account.py — 已登录 /account 页面显示的只读快照(计划、月度使用量、每个项目存储计数以及带创建/上次使用日期的掩码 API 密钥列表),由 billing / usage / projects / collections / api_keys 组合而成。

    • docs.py — 公共 /docs 集成指南的内容。在单个位置构建 MCP 客户端配置(mcp_config / mcp_config_json),文档页面和 app/api/connect.py 的每个密钥 .md 均复用,因此两者永不漂移。INTEGRATIONS 是指南注册表(通过添加条目来添加页面)。

公共页面(由 app/main.py 提供,Bootstrap + Jinja,通过 app/translations/*.yml 实现国际化):/ 着陆页、/plans、/account(已登录)以及 /docs/integrations 指南。FastAPI 的内置 API 文档已从 /docs 移至 /api/docs(/api/redoc、/api/openapi.json),以便公共站点拥有 /docs。

支付在 HTTP 层处理,位于 app/api/payments.py:POST /api/me/checkout(已登录)创建发票并返回 Monobank pay_url;已验证的 POST /webhooks/monobank 授予层级;GET /payment/success|fail 是装饰性浏览器返回页面(权利由 Webhook 驱动,绝不依赖这些页面)。

权利不单独依赖 Webhook。 Monobank 每个状态变化发送一次,且从不重发,因此若发生重启或代理故障导致回调丢失,已付款的客户将停留在旧层级,而我们这边无法察觉。因此,应用还会进行拉取:每 PAYMENT_RECONCILE_MINUTES 一次后台扫掠(billing.reconcile_with_monobank,在 app/main.py 生命周期中启动)获取每个仍在等待状态超过五分钟的付款的真实状态,并通过相同的 apply_webhook 转换进行推送。推送和拉取彼此幂等 —— 先到达的授予层级,另一个是空操作。payments 中的行在结账开始时写入,因此 created 表示“打开付款页面”,而非“已付款”;/admin/payments 明确显示该区别。

一次购买持续一个月(SUBSCRIPTION_DAYS),而非永久:grant_tier 标记 tier_expires_at,同一个后台循环将过期的账户降级为免费(downgrade_expired),账户页面显示计划到期日期。目前没有自动续订 —— 用户需要再次购买,而在信用期内购买会延长窗口期,而非重新开始。权利通过 effective_tier 读取,因此即使扫掠尚未重写存储字段,过期的授权也会立即停止支付。

由于没有自动续订,应用会主动询问:billing.renewal_state(user) 在 /account 上驱动提示 —— 在计划最后 RENEWAL_WARNING_DAYS(7)内显示带一键续订按钮的警告,并在之后 LAPSED_PROMPT_DAYS(30)内显示“您的计划已结束,请续订”提示(降级扫掠记录 lapsed_tier / tier_lapsed_at,以便页面仍能显示已过期的计划)。续订请求发送到计划页面使用的同一个 /api/me/checkout,并预设为其持有的计划。目前没有邮件 —— 提示仅触达访问页面的用户。

每个 CRUD 函数都接受可选的 db= / client= 参数,以便在无需实时后端的情况下在测试中驱动。

配置

通过环境变量设置(本地 .env 会自动加载;切勿提交 —— 参见 .env.example):

变量

默认值

用途

MONGODB_URI

(必填)

远程托管的 MongoDB 连接字符串。

MONGODB_DB

recall_select

数据库名称。

QDRANT_URL

http://qdrant:6333

Qdrant 端点(内部 Compose 网络)。

QDRANT_API_KEY

(本地无;生产环境必填)

应用与 Qdrant 之间的共享密钥。Compose 从该变量设置 Qdrant 的 QDRANT__SERVICE__API_KEY,应用在每次请求中发送该密钥。它是 qdrant.recall.select 仪表盘唯一的访问控制(该仪表盘本身无认证)。

VECTOR_SIZE

768

每个集合的向量维度。远程嵌入器通过 API 的 dimensions 参数被要求返回恰好此大小的向量,以确保两者同步。

EMBEDDING_API_KEY

(必填)

远程嵌入 API 的 API 密钥。

EMBEDDING_BASE_URL

https://api.deepinfra.com/v1

兼容 OpenAI 的嵌入 API 基础 URL。

GOOGLE_CLIENT_ID

(登录必填)

Google OAuth 2.0 Web 客户端 ID。

GOOGLE_CLIENT_SECRET

(登录必填)

Google OAuth 2.0 客户端密钥。

SESSION_SECRET

(开发环境回退)

用于签名会话 Cookie。生产环境请设置一个稳定的值。

PUBLIC_BASE_URL

http://localhost:8000

公共源;用于构建记忆链接和 OAuth 重定向 URI。

FORWARDED_ALLOW_IPS

172.25.0.0/16(Compose)/ 127.0.0.1(uvicorn)

uvicorn 信任的 X-Forwarded-Proto/-For 来源。Compose 默认设置为 caddy_net 子网,以便重定向保持 https 方案,日志显示真实客户端 IP;如果该网络被重建,请使用 docker network inspect caddy_net 检查。

MONOBANK_API_KEY

(支付必填)

Monobank 收单商户令牌。与 mcp-api.net 平台共享——同一商户,同一账户;通过 reference 区分发票。

MONOBANK_REDIRECT_URL

{PUBLIC_BASE_URL}/payment/success

购物者支付后浏览器返回的地址。

MONOBANK_WEBHOOK_URL

{PUBLIC_BASE_URL}/webhooks/monobank

服务器到服务器的回调,用于授予层级。必须可公开访问。

MONOBANK_WEBHOOK_VERIFY

1

验证 webhook 的 X-Sign 与商户公钥。涉及资金流动时请保持开启;仅本地开发时可设为 0。

PAYMENT_RECONCILE_MINUTES

15

从 Monobank 拉取进行中支付真实状态的频率,防止丢失 webhook 导致付费客户无法使用。设为 0 可禁用轮询。

ADMIN_SECRET

(未设置 - 区域禁用)

解锁位于 /admin 的所有者管理区域。未设置时,所有 /admin 路由返回 404。

ADMIN_SESSION_HOURS

12

解锁的管理员会话在重新锁定前持续的时间。

所有者管理区域(/admin)

一个只读窗口,可查看任何用户的个人区域,用于支持和了解用户所见内容。设置 ADMIN_SECRET(生成方式:python -c "import secrets; print(secrets.token_urlsafe(32))"),重新创建 Web 容器,然后打开 {PUBLIC_BASE_URL}/admin 并在每个会话中输入一次密钥。/admin/users 列出所有账户——可按邮箱、姓名或用户 ID 搜索——每行可打开该用户的套餐、当前周期使用量、项目及其记忆数量,以及掩码形式的记忆链接。

边界是经过设计的:密钥通过 POST 提交(绝不作为 URL 参数,因此不会出现在历史记录和访问日志中),重复错误猜测会锁定客户端五分钟,会话在 ADMIN_SESSION_HOURS 后自动重新锁定,并且此处的任何路由都不会写入数据或泄露记忆文本或密钥机密——所有者看到的是账户的轮廓,而非内容。当 ADMIN_SECRET 未设置时,该区域根本不存在。

认证(Google 登录)

登录是记忆链接的入口:用户通过 Google 登录,然后点击 复制记忆链接 来配置其默认项目 + 集合 + API 密钥,并获取供代理使用的 URL。密钥仅显示一次(仅存储其哈希值):之后,登录页面显示掩码形式的链接(通过 GET /api/me/link),按钮变为明确的、需确认的“获取新链接”——重新生成会使旧链接失效,且不会静默执行。密钥在 /account 上管理:掩码列表、创建/最后使用日期、带标签创建(仅显示一次)、以及撤销。设置 Google 凭据的步骤:

  1. Google Cloud 控制台 → API 和服务 → OAuth 同意屏幕 - 进行配置(外部;在未验证状态下添加你的邮箱作为测试用户)。

  2. 凭据 → 创建凭据 → OAuth 客户端 ID → Web 应用程序。

  3. 添加一个已授权的重定向 URI:{PUBLIC_BASE_URL}/auth/callback - 例如本地开发使用 http://localhost:8000/auth/callback,生产环境使用 https://recall.select/auth/callback(如果本地测试,两者都添加)。

  4. 将客户端 ID 和客户端密钥复制到 .env 文件中(GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET),并设置一个稳定的 SESSION_SECRET(python -c "import secrets; print(secrets.token_urlsafe(48))")。

本地运行

通过 Docker Compose 运行完整栈(Web + Qdrant):

cp .env.example .env   # then fill in MONGODB_URI
docker compose up --build
# open http://localhost:8000

或者仅运行应用,使用你自己的 Qdrant/Mongo:

pip install -e ".[dev]"
uvicorn app.main:app --reload

测试

pip install -e ".[dev]"
pytest

CRUD 测试针对内存中的 Mongo(mongomock)运行,Qdrant/嵌入客户端为模拟实现——无需实时后端。

部署

./deploy/deploy.sh

同一命令可在两个位置运行——它会检测运行位置:

  • 从开发机器(或代理的机器):推送本地提交,然后通过 recall-server SSH 别名在服务器上运行部署。

  • 在服务器本身上(setti@setti-server:~/recall_select$ ./deploy/deploy.sh):原地部署,无需 SSH 跳转。

两种路径都运行同一个工作脚本——deploy/_server_deploy.sh:git 同步 master 分支,重建 Compose 栈(FastAPI web + Qdrant),重新加载共享的 Caddy 代理(为 recall.select 自动 HTTPS),清理旧镜像。MongoDB 是远程托管的,因此服务器上必须存在 auth/MONGODB_URI 环境变量(参见 .env)。

在服务器上运行该脚本的用户需要对仓库具有 GitHub 拉取权限(~/.ssh 中有授权的 SSH 密钥)并且是 docker 组的成员——claude-agent 和 setti 都满足这些条件。工作脚本会自动将仓库注册为 git 的 safe.directory,这样非仓库所有者的部署者不会被“可疑所有权”阻止。

自动部署(CI)

每次推送到 master 分支都会通过 GitHub Actions 自动部署(.github/workflows/deploy.yml)——流程与上述相同,只是由 CI 触发而非人工。该作业通过 SSH 连接到服务器,并将 deploy/_server_deploy.sh 通过标准输入管道传输,因此它运行的是推送提交自身的部署逻辑。部署是串行化的(concurrency),并且运行工作流按钮(workflow_dispatch)允许你按需部署。

一次性设置——在 Settings → Secrets and variables → Actions 下添加:

密钥

必填

用途

DEPLOY_SSH_KEY

是

私钥,其公钥部分位于部署用户的 ~/.ssh/authorized_keys 中。

DEPLOY_HOST / DEPLOY_USER

是

服务器地址和要部署的 SSH 用户。

DEPLOY_PORT

否

SSH 端口(默认 22)。

DEPLOY_KNOWN_HOSTS

否

固定服务器主机密钥;如果未设置,CI 通过 ssh-keyscan 在首次使用时信任该主机。

应用密钥(MONGODB_URI、OAuth 等)保留在服务器的 .env 文件中——CI 永远不会看到它们。

许可证

根据 GNU Affero General Public License v3.0 许可。如果你以网络服务形式运行修改版本,AGPL 要求你向用户提供其源代码。版权所有 © 2026 Sergii Setti。

Maintenance

ActivityStale
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables each user to have a private, per-user memory store that persists across AI tools, allowing them to store, recall, search, list, and delete memories via MCP.
    -
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables AI agents to store and semantically retrieve durable memories across sessions via MCP or REST, with tools for remembering, recalling, asking, updating, and forgetting memories.
    24 npm
    MIT