mirror of
https://github.com/MemeMeow-Studio/MemeMeow.git
synced 2026-10-26 20:12:13 +08:00
- 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.
121 lines
14 KiB
Markdown
121 lines
14 KiB
Markdown
# 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、角色或多租户权限接口。资源包和社区同步功能已从生产入口移除。
|