Files
infstellar 5ecda39843 Add unit tests for image processing worker, operation policy, reverse image provider, and scope handling
- Implement tests for the ImageProcessingWorker to validate task deduplication and policy interactions.
- Create tests for operation policy to ensure proper grant handling and error responses.
- Add tests for reverse image provider response structure to validate empty and malformed results.
- Develop comprehensive tests for scope resolution and service validation to ensure correct behavior in various scenarios.
2026-08-17 12:50:29 +08:00

14 KiB
Raw Permalink Blame History

MemeMeow API

FastAPI 是唯一业务入口。模型密钥、Base URL 和路径只从服务端 .env 读取,前端不能修改配置或获得绝对路径。

Scope 与部署

应用工厂必须显式注入可信 scope resolver:

from api import create_app
from backend.scope import LocalScopeResolver

app = create_app(scope_resolver=LocalScopeResolver("local"))

模块级 api:app 入口已经显式安装上述 local 适配器;适配宿主应从已完成认证的请求上下文提供自己的 resolver,可按需通过 service_factory 与 agent_input_provider 注入 scope 服务和受控 Agent 输入,也可以省略 service_factory 使用核心默认 factory。只要 resolver 不是显式 LocalScopeResolver("local"),默认或宿主 factory 启动都不会调用 for_scope("local")、执行 local storage preflight 或创建 local 业务 service;默认 factory 在请求或任务认领后按可信 scope 懒创建 facade,宿主数据库可以没有 local scope。漏配 resolver、resolver 返回空值/非法值或 scope 服务装配失败时,应用或请求以稳定的 scope_resolution_failed / scope_unavailable 错误失败,不回退到 local。

请求中的 scope_id、user_id、路径前缀和普通业务字段不会改变 resolver 结果。上传、媒体、图片元数据、搜索、合集和公共任务 API 只访问当前请求 scope;属于其他 scope 的资源统一按不存在处理,不返回其存在性、文件路径或物理 namespace。

任务创建时服务端把请求 scope 写入不可为空的 Task.scope_id,payload 不承担授权 scope。Worker、重试、视觉/反向图片 callback 和 Agent 子任务从持久任务或有效 claim 恢复 scope;两个既有 /internal/... callback 不依赖用户 request scope。宿主 non-local scope 必须提供受控 agent_input_provider;未配置或返回符号链接/非普通文件时任务稳定失败为 agent_input_provider_unavailable。内部 callback 由 API 独立验证短期任务凭据,网络隔离和内部路由暴露范围仍由部署宿主负责;本 API 不新增用户登录或 scope 管理接口。

进程内只运行一个 scope-aware Worker manager,所有 scope 共享线程池、handler registry、lane 背压、claim owner 和恢复扫描;请求侧 task facade 不启动 scope 专属 Worker。

检索

POST /search

请求体:

{"query":"开会时忘记准备材料","n_results":8,"llm_enhance":false}

query 必须非空,n_results 为 1 到 30 的整数,llm_enhance 默认 false。成功响应:

{"results":["/media/2f3a2a6d-93f6-4cd0-a4c8-1578c5b929b2"]}

结果只包含受控媒体 URL,按相关性和稳定 meme_id 排序并去重。语义、向量和索引 generation 均由 PostgreSQL + pgvector 保存;缺少或处于 pending/repair_required 的记录会跳过,不再使用文件名回退。缓存不存在或正在生成时返回 503:

{"error":"cache_not_ready","message":"检索缓存尚未就绪"}

旧的 GET /search?q=... 不是兼容入口,返回 405。

图片库和媒体

  • GET /images?search=&page=1&page_size=50:分页列出当前请求 scope 的扁平图片;每个图片项包含 meme_id、文件名、媒体 URL、metadata.status、文本索引 embedding_status 和图片视觉向量 visual_embedding_status。

  • GET /images/metadata?meme_id=:读取当前 scope 指定 Meme 的完整数据库语境;不接受路径式资源标识。

  • POST /images/rename:请求 { "meme_id": "...", "new_name": "new" },保留原扩展名且拒绝覆盖。

  • POST /images/delete:请求 { "meme_id": "..." },隔离并删除图片及数据库 Meme。

  • POST /images/upload:multipart 字段 auto_name、多个 files,逐文件返回成功或失败,图片直接写入当前请求 scope 的受控图片根;成功入库后自动创建或复用逐图处理 job;不接受目标目录、scope_id 或 user_id。

  • 上传成功会在 PostgreSQL 创建稳定 meme_id 和 pending 元数据记录;meme_context.title 初始为 null。数据库是唯一结构化事实,运行时不读取或写入 sidecar。

  • GET /media/{meme_id}:按当前请求 scope 稳定 Meme ID 受控读取 PNG/JPG/JPEG/GIF;跨 scope ID 返回 404 meme_not_found。

  • POST /images/context:请求 { "meme_id": "...", "reverse_image_policy": "forbid|auto" },异步创建或复用单图处理 job,返回 processing_job_id 和当前 Agent 阶段摘要。缺省策略为 forbid。

  • POST /images/context/batch:请求 { "items": [{"meme_id":"..."}], "include_unready": true, "reverse_image_policy": "forbid|auto" },逐图返回处理 job 结果;省略 items 时不隐式扫描孤立文件。

  • POST /images/visual-embedding 和 /images/visual-embedding/batch:为既有图片提交完整图片处理 Job 的视觉前置;视觉任务失败必须使用完整 Job 重试或受限独立阶段入口。

  • POST /images/metadata/repair:异步执行数据库记录、图片文件和指纹完整性扫描;不读取 sidecar、不默认调用模型或外部搜索。

  • 图片库的“选择图片”“重试选中”和“重试所有未就绪”会调用上述逐图处理接口;有效文本向量写回后会立即具备当前 scope 的搜索资格,不需要为每次上传重建全库缓存。模型切换和存量迁移的显式回填见 docs/image-processing-migration.md。

图片处理 job

POST /images/processing?page=1&page_size=100 按当前 scope 分页枚举图片并提交或复用逐图处理 job,请求体为 { "reverse_image_policy": "forbid|auto" }。单图处理固定为 visual -> agent -> text_embedding 三个阶段;每个阶段拥有独立叶子 Task,视觉和文本阶段不消耗 Agent operation grant。活动 job 的配置、目标 SHA、metadata hash 或策略不一致时返回 409 generation_policy_conflict。

GET /images/processing/{job_id} 返回有限状态:queued、running、succeeded、failed、blocked 或 unknown_execution,以及 current_stage、每阶段 task_id/attempt/error。跨 scope 或不存在的 ID 都返回 404 image_processing_job_not_found。

GET /images/processing 返回当前 scope 的完整 Job 父项及 visual、agent、text_embedding 三个阶段;Job 和叶子 Task 的 submission_mode 明确为 pipeline。

POST /images/stages 请求仅接受 { "meme_id": "...", "stage": "visual|agent|text_embedding", "reverse_image_policy": "forbid|auto" },创建或复用无父 Job 的独立阶段 Task。scope、图片 SHA、配置、grant 和 callback 均由服务端派生;返回 submission_mode=standalone 与 processing_job_id=null。

POST /images/processing/{job_id}/retry 只接受 failed、blocked 或 unknown_execution job,并创建新的 revision、叶子 Task 和必要的 grant;旧 job、Task 和 grant 保持终态。unknown_execution 表示外部执行窗口已经开始但结果无法证明,恢复流程不会自动重放,必须由人工确认后显式 retry。兼容路径 /image-processing/... 仍可用但不在 OpenAPI 中展示。

合集

合集是当前请求 scope 内的逻辑图片分组,使用稳定 meme_id 建立成员关系,不复制或移动图片文件。接口不接受 scope_id 或 user_id。

  • GET /collections?page=1&page_size=50:按更新时间和合集 ID 稳定分页列出合集,返回 collection_id、名称、成员数量、封面媒体 URL 和时间戳。
  • POST /collections:请求 { "name": "工作" } 创建空合集;名称会去除首尾空白并限制为 1 至 100 个字符。
  • GET /collections/{collection_id}?page=1&page_size=50:返回合集元数据、成员总数和按加入时间稳定排序的成员。成员包含当前文件名、大小、状态和 /media/{meme_id}。
  • PATCH /collections/{collection_id}:请求 { "name": "新名称" } 重命名,不改变成员关系。
  • DELETE /collections/{collection_id}:删除合集及成员关系,不删除 Meme 或图片文件。
  • POST /collections/{collection_id}/items:请求 { "meme_ids": ["..."] } 原子批量加入图片;返回 added_count、existing_count 和最终 member_count。
  • DELETE /collections/{collection_id}/items/{meme_id}:幂等移除单个成员。

同一 scope 内名称精确唯一,重名返回 409 collection_exists;未知合集或图片返回 404;非法名称和空成员数组返回 422。合集删除或图片删除都会由数据库级联清理关系,但不会影响其他图片。

长任务

  • POST /generate-cache:返回 202、task_id、status=queued;同类任务不会并发执行。
  • GET /tasks?status=running&task_type=meme_context_generation&cursor=...&limit=50:按状态、类型和 cursor 分页返回安全任务摘要。
  • GET /tasks/{task_id}:返回任务类型、queued/running/succeeded/failed、进度、消息、时间、错误和有限结果。
  • POST /tasks/{task_id}/retry:图片阶段 Task 一律返回稳定错误 image_stage_retry_forbidden;完整 Job 必须调用 Job retry,独立阶段必须调用 /images/stages。普通非图片任务仍可使用该入口。

任务记录、去重、claim generation 和租约持久化在 PostgreSQL;服务重启后 queued 可继续执行,过期 running 按租约重新认领或失败。三类图片处理 Task 只由逐图 ImageProcessingWorker 扫描、认领和执行;进程级通用 Worker manager 会排除它们,也不会为图片链创建旧的批次 cache_generation finalizer。

Operation policy

GET /operations/availability 或传入单个 operation 参数只执行非权威 probe,返回可用状态、稳定拒绝原因和可选 retry_at,不会创建 reservation 或返回 grant。首期 operation 名称为 image.upload、analysis.agent、analysis.reverse_image_search 和 image.delete。适配宿主通过服务端 policy 注入额度和订阅规则;grant 只保存在服务端并绑定 scope、稳定幂等键和真实 Task,客户端字段不能伪造或覆盖它。

图片上传、合集导入、删除和 Agent/反向图片外部副作用分别在 durable 副作用边界执行 acquire/commit;只有能够证明副作用尚未开始时才 release。策略拒绝、额度限制和策略不可用分别映射为 403 operation_forbidden、429 operation_limit_exceeded 和 503 operation_policy_unavailable。

内部 Agent callback

POST /internal/reverse-image/search 与 POST /internal/visual-search/match 不是公共接口。它们在读取 multipart/JSON body 前要求 Runner 注入的短期 HMAC callback token,通过 X-MemeMeow-Callback、X-MemeMeow-Callback-Token 或 Bearer 头传递;token 绑定当前 Task、scope、claim generation、owner、attempt、目标 SHA、operation、issuer/audience、key id 和过期时间。路由还会从 PostgreSQL 复核 Task 类型、运行状态、租约和目标图片,调用方不能自报 scope、策略或任意图片。发布、轮换、旧任务收束和禁用式回滚见 docs/agent-callback-migration.md。

Agent 只获得当前任务 token、内部地址和 executor token,不获得 callback 根 secret、SERPAPI_API_KEY 或数据库凭据。callback secret 由 MEMEMEOW_AGENT_CALLBACK_SECRET 配置,生产部署必须通过独立服务身份和网络隔离保护;MEMEMEOW_AGENT_CALLBACK_VERIFICATION_KEYS 可用 kid=secret,kid=secret 提供轮换期间的旧 key 验证窗口。根 secret 缺失、格式错误或 verifier 异常时 callback 保持不可用,不回退到无认证。凭据轮换后旧 claim 应收束并显式重试。无凭据、旧 claim、租约过期、目标 SHA 变化和超限 body 都以稳定错误拒绝。

配置与访问策略

  • GET /config:只返回模型名、provider 是否配置和 *_api_key_configured 布尔状态;完整 URL、路径和密钥不返回。
  • 本地视觉活动模型固定为 dinov2_vitb14、768 维和 dinov2_vitb14-rgb224-first-frame-v1;Compose API 从 mememeow-visual:8276/health 读取真实模型状态,权重只在视觉容器内只读挂载并按 MEMEMEOW_VISUAL_WEIGHTS_SHA256 校验,未配置时任务返回 visual_model_not_configured。历史 DINOv3 向量表保留但不会参与活动匹配。
  • 视觉源码目录由 MEMEMEOW_VISUAL_MODEL_REPO 配置,仅服务端读取;源码提交和权重许可要求见 docs/visual-model-baseline.md。
  • Agent 通过内部 POST /internal/visual-search/match 发送 { "task_id": "...", "top_k": 20, "exclude_self": true }。scope、查询图片和向量空间均从运行中的 Agent 任务推导;候选必须同 scope、当前 SHA 有效并有成功 research provenance。
  • .env 关键字段:EMBEDDING_API_KEY、EMBEDDING_BASE_URL、EMBEDDING_MODEL、MEMEMEOW_OPENCODE_BASE_URL、MEMEMEOW_OPENCODE_API_KEY、MEMEMEOW_OPENCODE_MODEL、MEMEMEOW_OPENCODE_RUNTIME_ROOT、MEMEMEOW_IMAGE_ROOT、MEMEMEOW_AGENT_CALLBACK_SECRET。Compose 首次启动会在 mememeow-agent-executor-secret named volume 中生成 0600 的随机 executor token,API 以只读方式读取;旧版 host 运维模式仍可显式设置 MEMEMEOW_AGENT_EXECUTOR_TOKEN。生产 API 通过 Compose DNS 调用 executor,不使用 Docker CLI 或 socket。
  • MEMEMEOW_PROTECTED_MODE=true 时仅放行 MEMEMEOW_ALLOWED_ENDPOINTS;限流由 MEMEMEOW_RATE_LIMIT_* 控制,超限返回 429 和 Retry-After。

系统不提供用户登录、注册、JWT、角色或多租户权限接口。资源包和社区同步功能已从生产入口移除。