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

121 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MemeMeow API
FastAPI 是唯一业务入口。模型密钥、Base URL 和路径只从服务端 `.env` 读取,前端不能修改配置或获得绝对路径。
## Scope 与部署
应用工厂必须显式注入可信 scope resolver:
```python
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`
请求体:
```json
{"query":"开会时忘记准备材料","n_results":8,"llm_enhance":false}
```
`query` 必须非空,`n_results` 为 1 到 30 的整数,`llm_enhance` 默认 `false`。成功响应:
```json
{"results":["/media/2f3a2a6d-93f6-4cd0-a4c8-1578c5b929b2"]}
```
结果只包含受控媒体 URL,按相关性和稳定 `meme_id` 排序并去重。语义、向量和索引 generation 均由 PostgreSQL + pgvector 保存;缺少或处于 `pending`/`repair_required` 的记录会跳过,不再使用文件名回退。缓存不存在或正在生成时返回 `503`:
```json
{"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`](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`](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`](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、角色或多租户权限接口。资源包和社区同步功能已从生产入口移除。