Files
2026-10-01 18:36:48 +08:00

160 lines
22 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 不依赖请求上下文。宿主 non-local scope 必须提供受控 `agent_input_provider`;未配置或返回符号链接/非普通文件时任务稳定失败为 `agent_input_provider_unavailable`。内部 callback 由 API 独立验证短期任务凭据,网络隔离和内部路由暴露范围仍由部署宿主负责;本 API 不新增身份管理接口。
进程内只运行一个 scope-aware Worker manager,所有 scope 共享线程池、handler registry、全局/资源 lane slot、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"],
"result_media":[
{
"meme_id":"2f3a2a6d-93f6-4cd0-a4c8-1578c5b929b2",
"media_url":"/media/2f3a2a6d-93f6-4cd0-a4c8-1578c5b929b2",
"thumbnail":{
"status":"available",
"media_url":"/media/2f3a2a6d-93f6-4cd0-a4c8-1578c5b929b2/thumbnail",
"width":320,
"height":180,
"media_type":"image/jpeg"
}
}
]
}
```
`results` 保持原有的受控原图 URL 数组契约;`result_media` 是按结果顺序对应的可选富媒体旁路,条目包含稳定 `meme_id`、原图 `media_url` 和 `thumbnail`。缩略图处于 `pending`、`failed` 或 `stale` 时其 `media_url` 为 `null`,客户端应继续使用原图。结果按相关性和稳定 `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`、文件名、原图 `media_url`、`thumbnail`、`metadata.status`、文本向量 `embedding_status` 和图片视觉向量 `visual_embedding_status`。`thumbnail` 形状为 `{ "status": "available|pending|failed|stale", "media_url": "..."|null, "width": 320, "height": 180, "media_type": "image/jpeg" }`;只有 `available` 返回宽高、媒体类型和受控缩略图 URL。
- `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 字段 `reverse_image_policy=forbid|auto`、`auto_name=true|false` 和多个 `files`,单请求最多 20 个文件,逐文件返回成功或失败,图片直接写入当前请求 scope 的受控图片根;成功入库后自动创建或复用逐图处理 job;不接受目标目录、`scope_id` 或 `user_id`。部署可通过 `MEMEMEOW_MAX_REQUEST_BYTES` 启用总请求字节预算,未配置时不启用总量限制;响应丢失后的同名、同 SHA、同大小提交会返回既有 `meme_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`。响应使用 `Cache-Control: private, no-store` 和 `Vary: Cookie`。
- `GET /media/{meme_id}/thumbnail`:按当前请求 scope 和稳定 Meme ID 读取已完成且仍通过源版本、输出 SHA/大小、尺寸及 `image/jpeg` 校验的缩略图;不存在、未就绪、失效、损坏或跨 scope 的派生对象均按 `404 meme_not_found` 处理。响应同样使用 `Cache-Control: private, no-store` 和 `Vary: Cookie`,不接受客户端路径或内部 `output_key`。
### 分析信息
图片详情响应中的 `provenance.agent_analysis` 是只读摘要,仅包含 `model` 和 `completed_at`,从最近一次已保存的 Agent 语境分析记录派生,不使用当前模型配置或元数据更新时间代替。示例:
```json
{"model":"provider/analysis-model","completed_at":"2026-09-16T08:30:00.128901+00:00"}
```
缺少有效模型或完成时间时不返回该摘要。它不包含任务 ID、配置哈希等内部诊断字段;现有其他 provenance 字段保持不变。API 保留完整完成时间,图片详情的“分析日期”按浏览器本地时区只显示年月日。该时间表示语境分析结果的保存时间,不代表后续自动命名、文本向量生成或整个处理 Job 已成功。接口不会写回数据库或修改图片文件。
### 缩略图派生
原图成功入库后,服务端在独立的 `derived_thumbnail_generation` 任务 lane 中幂等提交缩略图任务;该任务不会改变既有图片处理 Job、搜索索引或原图状态。缩略图固定使用 `thumbnail-v1` profile,最长边为 320 像素,保持比例、不裁剪且不放大;GIF 只取首帧并输出高压缩静态 JPEG(质量 70),透明区域铺白。生成会复用原图预检的格式、解码、帧数和像素边界,并受输出字节、临时空间、执行超时、并发和背压配置限制。派生文件位于独立 scope-bound 存储,不进入图片列表、搜索索引、合集成员关系或合集导出,也不计入图片数量、存储容量和上传操作额度。
`POST /images/thumbnails/reconcile?page=1&page_size=100&limit=100`:在当前可信 scope 内分页扫描并幂等提交存量图片的缩略图回填/失败重建,返回 `{ "scanned": 100, "submitted": 80, "available": 15, "failed": 5 }`。该入口受 Settings 管理 token 保护,优先使用 `X-Settings-Admin-Token`,兼容 `X-MemeMeow-Settings-Token` 和大小写不敏感的 `Authorization: Bearer <token>`;token 由服务端 `MEMEMEOW_SETTINGS_ADMIN_TOKEN` 配置。`page`、`page_size` 和 `limit` 之外的查询参数会被拒绝,scope 从可信请求上下文派生。错误语义为:未授权 `403 settings_forbidden`,非法查询 `400 invalid_request`,任务背压 `429 thumbnail_backpressure`,回填服务或数据库不可用 `503 thumbnail_reconcile_unavailable`。
- `POST /images/context`:请求 `{ "meme_id": "...", "reverse_image_policy": "forbid|auto", "auto_name": false }`,异步创建或复用单图处理 job,返回 `processing_job_id` 和当前 Agent 阶段摘要。缺省策略为 `forbid`,自动命名缺省关闭。
- `POST /images/context/batch`:请求 `{ "items": [{"meme_id":"..."}], "include_unready": true, "reverse_image_policy": "forbid|auto", "auto_name": false }`,逐图返回处理 job 结果;省略 `items` 时不隐式扫描孤立文件。
- 视觉向量生成使用 `POST /images/stages`,请求为 `{ "meme_id": "...", "stage": "visual" }`;它只创建视觉阶段 Task,不创建 Agent、自动命名或文本向量 Task。视觉阶段不得携带 `reverse_image_policy`,携带该字段直接返回参数错误。
- `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", "auto_name": false }`。单图处理按 `visual -> agent -> auto_rename -> text_embedding` 推进;父 Job 创建时会保存 `processing_mode` 和每个阶段的 `planned/skip_reason`。`auto_name=false` 时自动重命名阶段为 `skipped/disabled` 且不创建叶子 Task,每个计划阶段拥有独立叶子 Task,视觉和文本阶段不消耗 Agent operation grant。同一图片已有活动 Job 或阶段 Task 时返回 `409 image_processing_active`,不会复用活动执行。
`POST /images/processing/unready` 只接受上述两项选项,由服务端枚举当前 scope 全部未就绪图片,不接受前端图片列表、筛选或分页参数;产品名称为“修复所有未就绪”,服务端按图片固定最小修复范围。响应逐图返回 `submitted`、`processing_active`、`not_needed` 或 `failed`,汇总包含 `target_count`、`submitted_count`、固定为零的 `reused_count`、`conflict_count`、`not_needed_count` 和 `failed_count`。
`GET /images/processing/{job_id}` 返回有限状态:`queued`、`running`、`succeeded`、`failed`、`blocked` 或 `unknown_execution`,以及 `current_stage`、每阶段 `task_id`/attempt/error、`auto_name`、`has_warnings` 和有限 warning 摘要。跨 scope 或不存在的 ID 都返回 `404 image_processing_job_not_found`。
`GET /images/processing` 返回当前 scope 的完整 Job 父项及 visual、agent、auto_rename、text_embedding 四个阶段;Job 和叶子 Task 的 `submission_mode` 明确为 `pipeline`。阶段项包含 `planned` 和 `skip_reason`,用于区分本次执行和本次未执行。历史三阶段 Job 读取时只读合成 `auto_name=false`、`auto_rename=skipped/disabled`,不回写历史。
`POST /images/stages` 请求接受 `{ "meme_id": "...", "stage": "visual|agent|auto_rename|text_embedding", "reverse_image_policy": "forbid|auto" }`,创建一个无父 Job 的独立阶段 Task。视觉阶段不得提供 `reverse_image_policy`;Agent 阶段的策略由服务端校验。scope、图片 SHA、配置、grant、标题指纹和目标文件名均由服务端派生;返回 `submission_mode=standalone` 与 `processing_job_id=null`。同一图片已有活动 Job 或阶段 Task 时返回 `409 image_processing_active`,不会复用活动 Task。`image_auto_rename` 不得通过通用 `/tasks/{task_id}/retry` 重试。
`POST /images/stages/batch` 请求 `{ "items": [{"meme_id":"..."}], "stages": ["visual", "agent", "text_embedding"], "reverse_image_policy": "forbid|auto", "auto_name": false }`,为每个选中图片和所选阶段创建或复用独立 Task;阶段列表至少一项、最多三项且不得重复,只接受三个核心阶段。完整重试仍使用 `/images/context/batch` 的完整流水线契约。
`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`、名称、成员数量、原图 `cover_media_url`、`cover_meme_id`、`cover_thumbnail` 和时间戳。存在封面 Meme 时返回 `cover_media_url`、`cover_meme_id` 和 `cover_thumbnail`;无封面时后三个字段为空或缺省,不伪造封面身份。封面缩略图沿用上述有限状态;原图封面地址始终保留。
- `POST /collections`:请求 `{ "name": "工作" }` 创建空合集;名称会去除首尾空白并限制为 1 至 100 个字符。
- `GET /collections/{collection_id}?page=1&page_size=50`:返回合集元数据、成员总数和按加入时间稳定排序的成员。成员包含当前文件名、大小、状态、原图 `/media/{meme_id}` 和 `thumbnail`;存在封面 Meme 时合集摘要同时包含 `cover_media_url`、`cover_meme_id` 和 `cover_thumbnail`,无封面时这些字段为空或缺省。
- `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`、进度、消息、时间、错误和有限结果。Agent 失败任务额外返回 `resume_available`、`resume_reason`、`session_id`、`executor_attempt_id`、`resume_attempts`、`resume_started_at`、`first_error` 和有限 `error_history`;这些字段只保留脱敏稳定诊断,不返回 prompt、工具参数、密钥或 transcript。自动续跑由 `MEMEMEOW_AGENT_RESUME_ENABLED` 控制且默认关闭;关闭时已持久化的 session 也显示为 `resume_available=false`、`resume_reason=resume_disabled`,但既有任务级 retry 仍按原策略执行。达到续跑次数或累计时间上限时显示 `resume_reason=resume_budget_exhausted`;额度未耗尽的失败会暂时显示为 `queued`,表示等待自动恢复。
- `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。grant association 还会比较服务端 resource、Task、source、units 和 input digest 指纹;已提交、已释放或未知的关联只能供恢复观察,不能再次作为执行授权。策略拒绝、额度限制和策略不可用分别映射为 `403 operation_forbidden`、`429 operation_limit_exceeded` 和 `503 operation_policy_unavailable`。
## 内部 Agent callback
`POST /internal/reverse-image/search` 是唯一的内部联网反向图片检索 callback,不是公共接口。它在读取 multipart 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、策略或任意图片。反向图片请求的 `request_id` 和 `input_digest` 都可省略;服务端在 Task/目标校验和受控裁剪后规范化 search type、language、country、query、布尔值和实际图片 SHA,返回持久化的权威 request ID。视觉候选不会通过 callback 查询,而是在 Agent 启动前由服务端冻结候选清单。
Agent 只获得当前任务 token、内部地址和 executor token,不获得 callback 根 secret、`SERPAPI_API_KEY` 或数据库凭据,也不得绕过 callback 直接访问 provider。callback secret 由 `MEMEMEOW_AGENT_CALLBACK_SECRET` 配置,生产部署必须通过独立服务身份和网络隔离保护;`MEMEMEOW_AGENT_CALLBACK_VERIFICATION_KEYS` 可用 `kid=secret,kid=secret` 提供轮换期间的旧 key 验证窗口。根 secret 缺失、格式错误、必需 callback 复合索引未就绪或 verifier 异常时 callback 保持不可用,不回退到 request-ID-only 逻辑。凭据轮换后旧 claim 应收束并显式重试。无凭据、旧 claim、租约过期、目标 SHA 变化和超限 body 都以稳定错误拒绝。迁移会先扫描历史逻辑重复或字段缺失并 fail-closed,不删除、合并或猜测覆盖既有事实;无 callback 的 local 直连仍使用原有缓存和显式 request ID 语义。
## 配置与访问策略
- `GET /config`:只返回模型名、provider 是否配置和 `*_api_key_configured` 布尔状态;完整 URL、路径和密钥不返回。上传能力字段包含服务端强制的 `max_files_per_request: 20`、仅供客户端调度提示的 `max_concurrent_upload_requests: 2` 和可选 `max_request_bytes: null|正整数`;服务端不以并发提示字段执行在途请求 admission,文件数和总字节预算仍由服务端执行。
- 本地视觉活动模型固定为 `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 只读取任务启动前由服务端写入的候选图片清单。清单已经固定 scope、图片指纹、候选数量、排序和可读取的图片引用,Agent 不能提交 `scope` 或 `top_k` 重新查询图片库。
- `.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。
- Agent 运行模式只接受 `auto`、`executor` 和 `host`。`auto` 在 executor URL 与 token 同时可用时选择 executor,否则选择 host;显式 `executor` 缺少配置时失败关闭。旧 Docker runtime、容器名和运行时字段不能启用任何执行分支;回滚使用显式 host,运维诊断使用当前 project 的 `docker compose exec <service>`。
- `MEMEMEOW_PROTECTED_MODE=true` 时仅放行 `MEMEMEOW_ALLOWED_ENDPOINTS`;限流由 `MEMEMEOW_RATE_LIMIT_*` 控制,超限返回 `429` 和 `Retry-After`。
Agent lane 的 `MEMEMEOW_OPENCODE_CONCURRENCY`、`MEMEMEOW_AGENT_SCOPE_CONCURRENCY` 和资源池容量必须为正整数,scope 容量不得超过全局容量。资源槽位只限制运行,不限制排队数量;任务的 `lane_resource_key` 在提交时冻结,公平状态按资源池分别维护。容量由部署环境决定,设置页保存的并发值需要重启后生效。
系统不提供注册、JWT、角色或多租户权限接口。资源包和社区同步功能已从生产入口移除。