feat: 优化图片资源删除管理,生图任务列表管理

This commit is contained in:
2026-08-30 14:50:33 +08:00
parent 4ee667142b
commit 957289f717
46 changed files with 4180 additions and 524 deletions
+19 -11
View File
@@ -1,8 +1,8 @@
# 工作交接说明(HANDOFF)
> 更新:2026-08-29 晚
> 更新:2026-08-30
> 读者:下一个接手工作的 AI / 开发者(工具无关)。先读本文再动手,避免重复探索。
> 配套文档:[`README`](../../README.md)(项目总览)、[`collect.md`](./collect.md)(采集)、[`trial-page.md`](./trial-page.md)(试算页)、[`image-suite.md`](./image-suite.md)(生图)、[`api.md`](./api.md)(契约)、[`backend-structure.md`](./backend-structure.md)(后端结构)
> 配套文档:[`README`](../../README.md)(项目总览)、[`collect.md`](./collect.md)(采集)、[`trial-page.md`](./trial-page.md)(试算页)、[`image-suite.md`](./image-suite.md)(生图)、[`background-jobs.md`](./background-jobs.md)(**后台任务中心方案、实施状态与路线图**)、[`image-asset-lifecycle.md`](./image-asset-lifecycle.md)(图片与水印存储规则)、[`api.md`](./api.md)(契约)、[`backend-structure.md`](./backend-structure.md)(后端结构)
---
@@ -32,7 +32,8 @@ extensions/collector 扩展采集(Ozon/1688/淘宝/天猫)
pnpm -C extensions/collector build # 产物 .output/chrome-mv3
```
- **start.command 新增**:启动时自动构建采集插件,并起后台监听(3s 轮询 `src/**` + `wxt.config.ts`,改动自动重建;BSD find 语法,无新依赖)。Ctrl+C 会连同监听进程一起清理。
- **start.command 新增**:启动时会先执行 `alembic upgrade head`,再自动构建采集插件并起后台监听(3s 轮询 `src/**` + `wxt.config.ts`,改动自动重建;BSD find 语法,无新依赖)。Ctrl+C 会连同监听进程一起清理。
- **后台任务 Worker**:`start.command` 会额外启动 `python server/worker.py`。单独启动 server 时也要另开终端运行该命令,否则生图任务只会停在“排队中”。
- **运行态提醒**:交接时 server 是上个会话用 nohup 起的后台进程(日志 `/tmp/osk-server.log`),原 start.command 终端已被停掉。新会话建议:`kill $(lsof -ti :8800)` 后用 `./start.command` 正常接管。
- **`uvicorn --reload` 不监控 `.env`**:改 `.env` 后必须手动重启 server(Kill 8800 端口进程再起)。
- 密钥在根目录 `.env`:`DEEPSEEK_API_KEY`(规划+文案)、`DASHSCOPE_API_KEY`(通义,可用 ✅)、`RIGHTAPI_API_KEY`(RightCode 中转,**通道当前故障** ❌)、`PCORE_API_KEY`(pcore.ai,**可用 ✅**)、`ARK_API_KEY` 未配(豆包未用)。`.env.example` 有全部键位说明。
@@ -48,7 +49,7 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
- **默认生图模型 = `gpt-image-2@pcore`**(`studio/src/services/suite.ts` 的 `DEFAULT_IMAGE_MODEL`)。
- **同名模型多中转的路由约定**:前端模型 value 用「api模型@provider」后缀区分(目前仅 pcore 用到),服务端 `schemas/suite.py` 的 `MODEL_PROVIDERS` + `api_model()`(剥后缀)处理;白名单 `PCORE_MODELS`。
- **pcore 协议要点**(`generator.py` 的 `_pcore_request`/`_pcore_poll_task`):无参考图走 `POST /v1/images/generations`,带参考图走 `/v1/images/edits` 且 images 必须是 **`[{image_url: dataURI}]` 对象数组**(纯字符串报错);均为异步:提交拿 `id` → 轮询 `GET /v1/images/{kind}/{task_id}` → `completed` 后 `data[0].url` 取图。`size` 是枚举(1024x1536/1024x1024),不接受自定义尺寸。new_api 系中转常见「HTTP 200/503 + 错误体」,错误解析已兼容嵌套 `error.{code,message}`;「No available channel」类错误不重试直接失败。
- **速率**:image2 分组 RPM 70 / RPD 1.2K(日限);生图全局串行(`generator.py` 的 `asyncio.Lock`),远碰不到 RPM。pcore 账户余额 $5,按张扣费。
- **速率**:image2 分组 RPM 70 / RPD 1.2K(日限);当前独立 Worker 单进程逐图领取任务,远碰不到 RPM。pcore 账户余额 $5,按张扣费。
## 4. AI 规划张数策略(`planner.py`)
@@ -64,12 +65,16 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
|---|---|
| 试算页 | 五区块页面、计价、俄文文案、采集图片/出图方案/生成结果、登记导出 |
| 采集对接 | ISS 扩展并入 `extensions/collector/`,「上报商品」入库 + 自动打开试算页 |
| 生图服务端 | planner/generator/prompts/watermark/tasks,套图 + 单张 AI 生图 + 水印 + ZIP 导出 |
| 生图服务端 | planner/generator/prompts/watermark/jobs + 独立 Worker,套图 + 单张 AI 生图 + 水印 + ZIP 导出 |
| **本轮:Pcore 接入** | 新 provider 全链路(生成/编辑/轮询/重试/错误处理),`@pcore` 路由约定,下拉带供应商名,默认模型切 Pcore |
| **本轮:规划器修复** | thinking 禁用 + 重试 + 张数策略(AI 自主,建议 8 张左右,生图要求可指定) |
| **本轮:采集箱** | 勾选列 + 全选/取消全选 + 批量删除(硬删、Popconfirm 二次确认、删空页自动回退) |
| **本轮:采集图片区改版** | 8 列网格;AI 生图按钮半透明悬浮图片右下角;图名显示在图片下方(取 URL 末段);SKU 名并入图名行(不再悬浮遮挡勾选钮);勾选钮在右上角;默认全部不勾选 |
| **本轮:生成结果区** | 「导出图片」按钮(水印按钮右侧,复用 POST /api/export/images,ZIP 内分「套图结果/单张生图」文件夹);「单张 AI 生图」小节改名「生图记录」(落库历史,刷新后保留) |
| **本轮:图片生命周期** | 采集、上传与 AI 图片统一保存为 `original + current`;只保留当前水印版;重加/移除水印自动回收旧对象;删除单图/硬删商品会清理本地或七牛对象;具体规则见 [`image-asset-lifecycle.md`](./image-asset-lifecycle.md) |
| **本轮:图片资源上限** | 单张上传/下载默认 20 MB;采集最多 100 张;导出最多 50 张、总量 200 MB;ZIP 超过 2 MB 自动落临时磁盘并分块返回,配置见 `.env.example` |
| **本轮:生图任务与轮询** | 单张 AI 编辑改为提交统一异步任务并立即返回 `suite_id`;套图与单张弹窗均采用无重叠递归轮询、取消在途请求与失败退避;套图任务 ID 可在刷新页面后从浏览器会话恢复 |
| **本轮:持久化图片任务** | 套图、单张编辑与批量添加水印已迁入数据库任务表和独立 Worker;Pcore/RightAPI/通义万相保存第三方任务 ID 并可在重启后恢复轮询;水印逐图进度可刷新恢复、取消并按失败项重试,详见 [`background-jobs.md`](./background-jobs.md) |
| **本轮:关键 bug 修复** | ① axios 全局 Content-Type 导致 FormData 被 JSON 化 → 上传 422(`api.ts` 移除全局头,**今后勿设**);② TrialPage `load()` 置 loading 导致整页闪 Spin、卸载面板(杀生图轮询/丢勾选)→ 改静默刷新;③ web v1 页面因 cdn.tailwindcss.com 被墙整页无样式 → tailwind 运行时固化到 `web/vendor/tailwind.js` |
| **本轮:杂项** | 采买地址「打开地址」改图标按钮(与复制图标同款);start.command 自动构建+监听插件 |
@@ -80,7 +85,7 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
## 6. ⚠️ 未提交改动清单(当前最大事项)
分支 `feat-2.1.0`,工作区有 19 个文件改动 + 1 个新目录,**建议按模块分 3-4 个 commit**:
分支 `feat-2.1.0`,工作区仍有多项未提交改动,**建议按模块分 4-5 个 commit**:
1. **Pcore 生图接入 + 规划器修复 + 张数策略**
`server/services/{generator,planner,suite_service,prompts/__init__}.py`、`server/{api/suite.py,schemas/suite.py,config/settings.py}`、`studio/src/services/suite.ts`、`.env.example`、`docs/v2.1/{image-suite,api}.md`
@@ -88,7 +93,8 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
`studio/src/pages/trial/{TrialSuitePanel,TrialInfoPanel,TrialPage}.tsx`、`studio/src/pages/collection/CollectionPage.tsx`、`studio/src/services/api.ts`
3. **web v1 样式修复(CDN 固化)+ start.command 插件自动构建**
`web/ozonSeller.html`、`web/vendor/tailwind.js`(新目录)、`start.command`
4. **交接文档**:`docs/v2.1/HANDOFF.md`
4. **图片生命周期与水印回收**:`server/services/image_assets.py`、`server/services/storage.py`、`server/{api/materials,api/products,models/asset}.py`、迁移 `a41e8c7f4b20_add_asset_image_lifecycle.py`、试算页水印入口
5. **交接文档**:`docs/v2.1/{HANDOFF,image-asset-lifecycle,optimization-audit}.md`
提交前:`git checkout -- studio/tsconfig.app.tsbuildinfo`(构建产物,勿提交)。
@@ -105,9 +111,9 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
| # | 坑 | 对策 |
|---|---|---|
| 1 | `uvicorn --reload` 不监控 `.env` | 改 `.env` 后手动重启 server |
| 2 | 套图任务在内存(`services/tasks.py`) | server 重启丢任务状态(前端已提示重新生成);已落盘图片不丢(在「生图记录」区) |
| 3 | 生图全局串行(`generator.py` 的 `asyncio.Lock`) | 一次只跑一个生成队列,测试时别并发提交 |
| 4 | 单张 image-edit 是同步接口 | 慢模型单张 1-5 分钟,前端 axios 超时 120s 可能先断(后端仍在跑,图会出现在生图记录) |
| 2 | 生图任务依赖独立 Worker | `start.command` 会启动;若手动运行 uvicorn,必须另开 `python server/worker.py`,否则任务会一直排队 |
| 3 | SQLite 仅适合单机 Worker | 当前通过租约恢复异常退出的任务;未来多实例前先迁移 PostgreSQL,再做多 Worker 竞争领取 |
| 4 | 外部模型任务可恢复范围不同 | Pcore/RightAPI/通义万相保存 provider task id 后可继续轮询;豆包/同步千问没有外部任务 id,进程恰在请求中断时只能重试 |
| 5 | **axios 实例勿设全局 Content-Type** | axios 1.x 遇「默认 JSON 头 + FormData」会把表单转 JSON 发送(`formDataToJSON`),multipart 接口全 422。已从 `api.ts` 移除;对象请求体 axios 会自动补 JSON 头 |
| 6 | `web/` 样式依赖公网 CDN | tailwind 已固化 `web/vendor/tailwind.js`;font-awesome/jsdelivr 若也断,同样方式下载到 `web/vendor/` 改引用 |
| 7 | 试算页水印文本存 localStorage(`trialWatermark`) | 改默认值不影响已保存设置;登记表同理存 localStorage `trialRegisterRecords` |
@@ -116,6 +122,7 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
| 10 | AI 规划/生图花钱 | pcore 按 $ 扣费、RightCode 按张计费;联调时 plan 张数设 1-2、优先低成本模型 |
| 11 | TrialPage `load()` 勿再置 loading | 置 loading = 整页 Spin + 卸载面板 = 杀生图轮询/丢状态(已修,保持静默刷新) |
| 12 | 浏览器自动化调试怪癖(如 ZCode IAB) | 截图偶发超时(重试一次多半仍失败,改用几何测量/计算样式验证);文件上传需用 DataTransfer+input change 事件模拟;改代码后 React 事件偶发失灵 → reload 页面即恢复 |
| 13 | 图片上传/导出存在大小限制 | 默认单张 20 MB、采集 100 张、导出 50 张、ZIP 200 MB;超限接口返回 413。需要调整时改 `.env` 的 `IMAGE_*` / `EXPORT_*` 配置并重启服务 |
## 9. 关键文件速查
@@ -125,7 +132,8 @@ pnpm -C extensions/collector build # 产物 .output/chrome-mv3
| 01 商品信息(采买地址图标、型号/货号联动) | `pages/trial/TrialInfoPanel.tsx` | `api/materials.py` |
| 02 价格试算 | `pages/trial/TrialPricingPanel.tsx` + `pricing/pricing.ts` | — |
| 03 俄文文案 | `pages/product/CopyPanel.tsx`(共用) | `api/ai.py` + `services/deepseek.py` + `prompts/copy_ru.py` |
| 04 图片与生图(8 列网格/悬浮按钮/图名) | `pages/trial/TrialSuitePanel.tsx` + `AiImageGenModal.tsx` | `api/suite.py` + `services/{suite_service,generator,planner,prompts/,tasks,watermark}.py` |
| 04 图片与生图(7 列网格/悬浮按钮/图名) | `pages/trial/TrialSuitePanel.tsx` + `AiImageGenModal.tsx` | `api/suite.py` + `services/{jobs,job_worker,suite_service,generator,planner,prompts/,watermark,image_assets}.py` |
| 图片原图/水印/删除回收 | `TrialSuitePanel.tsx`(水印设置) | `api/materials.py`、`api/products.py`、`services/{image_assets,storage}.py` |
| 生图供应商路由 | `services/suite.ts`(IMAGE_MODEL_OPTIONS 带供应商名;DEFAULT_IMAGE_MODEL) | `schemas/suite.py`(MODEL_PROVIDERS/`@pcore` 约定)+ `services/generator.py`(generate_pcore 等) |
| 05 登记与导出 | `pages/trial/TrialExportPanel.tsx`(localStorage `trialRegisterRecords`) | — |
| 生成结果导出 | `TrialSuitePanel.tsx` handleExportResult(套图+单张分组 ZIP) | `api/export.py` |
+9 -6
View File
@@ -1,7 +1,7 @@
# Ozon Seller Kit V2.1 方案总览(转向:放弃 API 直传,聚焦试算工作流)
> 状态:方案定稿 + 分阶段实施(Phase A 前端先行,进行中)
> 最后更新:2026-08-26
> 状态:主链路已落地,持续打磨;后台任务中心第一、二阶段已完成
> 最后更新:2026-08-30
> 定位:本文是 V2.1 全部设计文档的入口与决策总表。先读本文,再按需读分册。
> 前置:`docs/v2/`(V2 方案)。V2.1 是对 V2 主链路的一次「降本转向」,不推翻 V2 的基础设施决策(数据库/存储/鉴权/工作台化)。
@@ -90,7 +90,7 @@ V2(原计划) V2.1(现在)
|---|---|---|
| 商品试算页(5 区块) | studio 前端(**Phase A,本次**) | [`trial-page.md`](./trial-page.md) |
| 套图规划/一键生成/单张 AI 生图 前端 | studio 前端(**Phase A,本次**) | [`image-suite.md`](./image-suite.md) |
| suite 系列服务端接口(plan/generate/suites/image-edit/export) | server(Phase B) | [`api.md`](./api.md) |
| suite 系列服务端接口(plan/generate/suites/image-edit/export) | server(Phase B,已完成);生成和水印已接入持久化后台任务 | [`api.md`](./api.md)、[`background-jobs.md`](./background-jobs.md) |
| 采集对齐 image-suite-studio + 自动打开试算页 | extension-v2(Phase C) | [`collect.md`](./collect.md) |
| 批量 CSV 服务端导出(可选优化) | server(Phase D) | [`api.md`](./api.md) §7 |
@@ -111,6 +111,8 @@ V2(原计划) V2.1(现在)
| [`collect.md`](./collect.md) | 采集方案:扩展并入 `extensions/collector/` + 上报开关 + 自动打开试算页(已实施) | 做插件改动时读 |
| [`trial-page.md`](./trial-page.md) | 商品试算页:页面结构、计价公式与校验、文案、采购地址、入库数据结构、CSV/组合码导出 | 做试算页前后端时读 |
| [`image-suite.md`](./image-suite.md) | 图片生成:套图规划→一键生成、模型路由、单张 AI 生图、水印、生成图回写与导出 | 做图片功能时读 |
| [`background-jobs.md`](./background-jobs.md) | **后台任务中心**:统一任务模型、Worker、状态、恢复、已完成阶段和后续路线图 | 改生图、水印或任何耗时任务前先读 |
| [`image-asset-lifecycle.md`](./image-asset-lifecycle.md) | 图片原图、水印当前版、对象路径与删除回收流程 | 改图片、水印或存储前先读 |
| [`api.md`](./api.md) | 前后端 REST 契约(前端服务层已按此开发) | 前后端联调时读 |
| [`backend-structure.md`](./backend-structure.md) | 后端代码结构盘点:文件/接口/服务树形图、数据流、状态标注(在用/冻结/残留) | 改后端前先读 |
@@ -122,8 +124,9 @@ V2(原计划) V2.1(现在)
|---|---|---|
| **Phase A** | docs/v2.1 方案文档 + 试算页前端(路由/入口/5 区块/服务层契约) | ✅ 完成 |
| **Phase C** | 采集对接:ISS 扩展并入 `extensions/collector/` + 「上报商品」开关 + 自动打开试算页(方案三,见 collect.md) | ✅ 完成 |
| **Phase B** | 服务端:从 ISS server 平移 planner/generator/prompts/watermark/tasks(/api/suite/*、/api/suites/*、/api/export/images、/api/proxy-image),打通试算页一键生成与单张 AI 生图 | 待开工 |
| **Phase D** | 打磨:批量 CSV 服务端化、生成图自动回写素材、水印增强、ISS-only 产品模式构建配置 | 待开工 |
| **Phase B** | 服务端:planner/generator/prompts/watermark、套图/单图接口、图片回写与导出;已切为持久化任务 + 独立 Worker | ✅ 完成 |
| **Phase D** | 打磨:批量 CSV 服务端化、图片生命周期、水印增强、ISS-only 产品模式构建配置 | 🚧 部分完成 |
| **Phase E** | 后台任务中心第三阶段:当前商品任务抽屉已完成;全局面板、任务事件、调度公平性与幂等性增强待开发 | 🚧 进行中,见 [`background-jobs.md`](./background-jobs.md) |
每个阶段结束都可独立运行:Phase A 结束时,试算页的计价/文案/素材上传/入库立即可用(对接已有接口),套图相关按钮点击会提示「服务端接口未实现」。
@@ -142,7 +145,7 @@ V2(原计划) V2.1(现在)
| 风险 | 级别 | 对策 |
|---|---|---|
| image-suite-studio 服务端是无状态内存任务表,重启丢任务 | 🟡 低 | 接受(与该项目建设一致):图片落盘 storage 不丢,任务状态丢了重新生成即可;文档明示 |
| 模型生成耗时长、可能跨服务重启 | 🟡 中 | 本仓已用持久化父/子任务 + 独立 Worker;Pcore/RightAPI/通义万相保存外部 task id 后可继续轮询,详见 `background-jobs.md` |
| 采集图防盗链(Ozon/阿里 CDN)在 studio 页面直接展示可能失败 | 🟡 中 | 素材展示优先 `stored_url`(服务端已转存);Phase B 补 `/api/proxy-image` 兜底 |
| 前端先行的契约与服务端实现不一致导致返工 | 🟡 中 | api.md 即契约真源,服务端按文档实现;字段命名对齐 image-suite-studio 已验证的结构 |
| CSV 人工上品仍是手工活 | 🟢 低 | 组合码 + CSV 已是 web/ 验证过的效率水平;后续可再评估半自动(表格粘贴)方案 |
+23 -12
View File
@@ -10,7 +10,7 @@
| 接口 | 用途(试算页) |
|---|---|
| `GET /api/products/{id}`、`PATCH /api/products/{id}` | 商品详情 / 试算页自动保存(raw/pricing/copy/price/weight/... 全字段) |
| `GET /api/products/{id}/assets` | 素材列表(分组/规格/状态/stored_url) |
| `GET /api/products/{id}/assets` | 素材列表(分组/规格/状态/current `stored_url`、`original_url`、水印状态) |
| `GET /api/products?stage&q&page&page_size` | 批量 CSV / 组合码导出的数据源 |
| `POST /api/materials/bytes` | 试算页「上传图片」(FormData:`product_id, group_key=upload, file`)→ `{asset_id, status}` |
| `GET /api/fx` | 汇率 `{rate, source, ts}` |
@@ -54,22 +54,24 @@
```
**Res** `{ "suite_id": "…" }`
实现要点:方案按 count 展开逐张 job;内存任务表 + 全局串行队列;逐张 build_prompt(模型家族分册)→ 参考图解析(variant 精确匹配→main 第 1 张;≤2 张)→ 生图 → 水印 → 落 storage;生成图追加 `product_assets(group_key='generated')`。
实现要点:方案按 count 展开为持久化逐图子任务;独立 Worker 领取子项并在完成后回写进度。Pcore、RightAPI 与通义万相的第三方任务 ID 会先写入数据库,重启后继续轮询同一个模型任务;完成后再水印、落 storage、追加 `product_assets(group_key='generated')`。
## 4. 任务查询 `GET /api/suites/{id}`(Phase B)
**Res**
```jsonc
{
"id": "…", "status": "pending|running|done|partial|failed",
"id": "…", "status": "queued|running|done|partial|failed|cancelled",
"style_set": 1, "platform": "ozon", "lang": "俄文", "ratio": "3:4", "provider": "rightapi",
"total": 8, // 计划总张数(images 逐张追加,过程中 length < total)
"total": 8, "completed": 3, "failed": 1,
"images": [ { "type_id": "white_bg", "name": "白底主图-01.png", "url": "/media/suites/…png",
"status": "ok|failed", "error": null } ],
"status": "queued|waiting_provider|running|done|failed|cancelled", "error": null } ],
"error": null
}
```
前端 3s 轮询;`done/partial/failed` 终态;后端重启后 404,前端提示重新生成。
前端采用“本次请求结束后再延迟下一次”的递归轮询(正常 3s、请求失败指数退避),并在切换任务、组件卸载时取消在途请求;`done/partial/failed/cancelled` 为终态。每条 `images` 项另含 `item_id`,用于按图重试。浏览器会暂存当前任务 ID,并从服务端查询商品的活动任务;Web 服务或 Worker 重启后仍可恢复任务状态。
辅助接口:`GET /api/products/{product_id}/suites?active_only=true` 恢复活动任务;`POST /api/suites/{id}/cancel` 取消任务;`POST /api/suites/{id}/retry` 重试全部失败子项;`POST /api/suites/{id}/items/{item_id}/retry` 只重试指定失败项。
## 5. 结果导出 `GET /api/suites/{id}/zip`(Phase B)
@@ -87,12 +89,19 @@
"append": true // true:结果追加 product_assets(generated)
}
```
**Res** `{ "url": "/media/suites/….png", "asset_id": "uuid|null" }`
**Res** `{ "suite_id": "…" }`
同步接口(单张 5s-5min,前端 axios 超时 120s+;gpt 系列偶发超时由前端重试提示兜底)。
实现要点:复用 §3 的 provider 层与 prompt 家族;参考图 = 传入单图;Ozon 规格(俄文 3:4)。
接口仅创建并提交统一图片任务,立即返回;前端通过 `GET /api/suites/{suite_id}` 查询状态和 `images[0].url`。完成后若 `append=true`,结果会回写为 `product_assets(group_key='generated')`。单张编辑优先级高于套图,提交模型后同样持久化第三方任务 ID;参考图 = 传入单图;Ozon 规格(俄文 3:4)。
## 7. 导出与代理(Phase B / D)
## 7. 素材水印 `POST /api/assets/watermark` / `POST /api/assets/watermark/remove`
`POST /api/assets/watermark` 的请求体为 `{ "asset_ids": ["uuid"], "watermark": { "enabled": true, "type": "text|image", "text": "…", "opacity": 30 } }`,立即返回 `{ "job_id": "uuid", "total": n }`。Worker 对选中的采集图或生成图逐张应用水印,前端经 `GET /api/suites/{job_id}` 查看逐图进度和错误;`GET /api/products/{product_id}/watermark-jobs?active_only=true` 用于页面刷新后恢复。
`POST /api/assets/watermark/remove` 的请求体为 `{ "asset_ids": ["uuid"] }`,将选中图片恢复为无水印原图。
`POST /api/assets/watermark/remove` 当前仍为短时同步操作,返回 `{ "updated_ids": [], "failed": [] }`。两接口均只保留原图和当前展示版本:重新加水印会在数据库提交后删除旧水印文件;前端展示、单张下载与导出均使用 `stored_url`。
## 8. 导出与代理(Phase B / D)
| 接口 | 方法 | Req | Res |
|---|---|---|---|
@@ -100,11 +109,13 @@
| `/api/proxy-image?url=` | GET | 源站图片 URL | 图片字节(带站点 Referer 代下,绕防盗链) |
| `/api/export/trial-csv` | GET | query:`stage?` | csv blob(16 列,见 trial-page.md §7;Phase D 服务端化,替代前端拼装) |
## 8. 采集相关(Phase C 使用,均已实现)
图片资源上限:默认单张上传/远程下载 20 MB;一次导出最多 50 张,ZIP 内源图片总量与成品 ZIP 均最多 200 MB。超过限制返回 HTTP `413` 及可读错误提示;ZIP 超过 2 MB 自动写入临时磁盘,并以分块响应下载。
## 9. 采集相关(Phase C 使用,均已实现)
- `POST /api/materials`:插件上传采集结果;**响应含 `product_id`**(插件用于打开 `/trial/{id}`)。
- `GET /api/collected?platform=&itemId=`:采集前查重。
## 9. 冻结接口(不投入,保留代码)
## 10. 冻结接口(不投入,保留代码)
`/api/publish/*`、`/api/shops/*`、`/api/categories/*`——Ozon API 直传链路(D1)。
+12 -9
View File
@@ -30,10 +30,11 @@ server/
│ │ ├── GET·POST /api/products 商品列表 / 新建
│ │ ├── GET·PATCH·DELETE /api/products/{id} 详情 / 自动保存 / 删除(软删 archived | 硬删)
│ │ └── POST /api/products/{id}/copy 复制为新变体(重置货号/图片防冲突)
│ ├── suite.py 【326 ✅核心·Phase B】
│ ├── suite.py 【✅核心·Phase B】
│ │ ├── POST /api/suite/plan DeepSeek 出图方案规划(同步,数秒级)
│ │ ├── POST /api/suite/generate 一键生成:方案展开→内存任务→串行生图→回写 generated 素材
│ │ ├── GET /api/suites/{id} 任务状态轮询(前端 3s 一次)
│ │ ├── POST /api/suite/generate 一键生成:方案展开→持久化子任务→Worker 生图→回写 generated 素材
│ │ ├── GET /api/suites/{id} 任务状态轮询(前端 3s 一次)/ cancel / retry
│ │ ├── GET /api/products/{id}/suites 恢复商品活动任务
│ │ ├── GET /api/suites/{id}/zip 生成结果打包下载(中文文件名)
│ │ └── POST /api/suite/image-edit 单张 AI 生图(试算页每图按钮,append=true 回写素材)
│ ├── export.py 【121 ✅】POST /api/export/images 采集图片打包 ZIP(标题/分组/SKU规格 文件夹结构)
@@ -47,8 +48,8 @@ server/
│ └── ozon.py 【5 🧊冻结】空占位(远期直传重启时用)
│
├── services/ ──────────────────────── 业务逻辑层
│ ├── generator.py 【494 ✅核心】三大生图 provider(doubao 火山 / tongyi 通义 / rightapi 中转)
│ │ + asyncio.Lock 全局串行队列 + run_suite 任务执行 + 参考图解析
│ ├── generator.py 【✅核心】生图 provider(doubao / tongyi / rightapi / pcore)
│ │ + 可恢复的 submit/poll 推进器 + 参考图解析
│ │ (variant 精确匹配 SKU 图 → 回退 main 首张;≤2 张转 data-URI)
│ ├── planner.py 【227 ✅核心】DeepSeek 出图方案规划器
│ │ (单行 JSON 解析 / kind 白名单 / count 钳 0-3 / 幻觉 SKU 丢弃绑定 / 截断修复)
@@ -59,7 +60,9 @@ server/
│ │ ├── gpt.py gpt-image-2「edits 保真」语义:以参考图为准,禁止按文案重造商品
│ │ ├── google.py nano-banana「主体保持」语义:一句话钉死主体 + 自然语言指令
│ │ └── doubao.py 豆包(复用 alibaba)
│ ├── tasks.py 【70 ✅】套图内存任务注册表(⚠️ server 重启丢任务状态;已落盘图片不丢)
│ ├── jobs.py 【✅核心】持久化任务仓库、租约领取、取消/失败重试、任务视图
│ ├── job_worker.py【✅核心】独立 Worker:逐图提交/轮询外部模型、落图与回写素材
│ ├── tasks.py 【兼容】旧内存任务执行器,活跃 API 不再使用
│ ├── watermark.py 【122 ✅】Pillow 水印合成(图片徽章/文字,右下角,失败回退原图)
│ ├── deepseek.py 【182 ✅】DeepSeek chat 封装(俄文文案 + 出图规划共用;JSON 容错解析)
│ ├── models_catalog.py【84 ✅】读 config/models.yaml(俄文文案模型白名单:快/省、质量两档)
@@ -141,7 +144,7 @@ extensions/collector 采集
→ 试算页 /trial/{id}(studio)
├─ 01/02/03 编辑 → PATCH /api/products/{id} 自动保存(raw/pricing/copy JSON)
├─ 04 生图:POST /api/suite/plan(planner+deepseek)→ POST /api/suite/generate
│ → tasks 内存任务 → generator(prompts 家族 → provider API → watermark → storage)
│ → background_jobs / items → 独立 Worker → generator(prompts 家族 → provider API → watermark → storage)
│ → 每张成功回写 product_assets(generated) → 前端轮询 GET /api/suites/{id} + 刷新素材
└─ 05 登记(localStorage)→ 导出 CSV / 组合码(纯前端)
```
@@ -150,8 +153,8 @@ extensions/collector 采集
| 项 | 说明 |
|---|---|
| 套图任务在内存 | `tasks.py` 进程内注册表:server 重启丢任务状态(前端提示重新生成);已落盘图片不丢 |
| 生图全局串行 | `generator.py` 的 `asyncio.Lock`:一次只跑一个生成队列(防中转限流,ISS 同款) |
| 生图任务依赖独立 Worker | `start.command` 已自动启动 `python server/worker.py`;手动启动 uvicorn 时也必须启动 Worker |
| 外部任务恢复 | Pcore、RightAPI、通义万相的 provider task id 会保存到数据库,重启后继续轮询原任务;同步 provider 无外部 id 时只能重试 |
| 全部接口无鉴权 | V2.1 决策:先功能后鉴权;重新引入时从 `deps.py` + 路由依赖层加 |
| 残留可删 | `schemas/auth.py`(无引用);`api/ozon.py`(空占位,随冻结链路保留) |
| 密钥 | `.env`:DEEPSEEK_API_KEY(规划+文案)、RIGHTAPI_API_KEY(gpt/nano 生图)、DASHSCOPE_API_KEY(通义)、ARK_API_KEY(豆包,可选) |
+281
View File
@@ -0,0 +1,281 @@
# 后台任务中心:设计、实施状态与路线图
> 更新:2026-08-30
> 适用范围:套图生成、单张 AI 生图 / AI 编辑、批量添加水印,以及未来的耗时导出和批处理。
> 阅读顺序:接手相关开发前,先读本文,再读 [`image-asset-lifecycle.md`](./image-asset-lifecycle.md) 与 [`api.md`](./api.md)。
## 1. 结论与边界
“单图弹窗等待”、“套图生成”和“批量水印”本质都是耗时、可失败、需要恢复的后台操作,不应分别实现异步逻辑。项目统一采用:
```text
Web API
└─ 在数据库创建父任务 + 子项,立即返回任务 ID
独立 Worker
└─ 领取一个子项 → 调模型 / 合成水印 / 保存图片 → 回写进度与结果
试算页任务入口(当前)/ 任务中心(后续)
└─ 查询状态、恢复进度、取消、重试、打开结果
```
当前是本地单机、SQLite 架构,**不引入 Redis、Celery 或分布式队列**。数据库驱动的独立 Worker 已能覆盖断开弹窗、刷新页面、Web 服务重启、模型长耗时和失败重试,复杂度与收益最平衡。未来扩展到多机器或多实例时,再迁移 PostgreSQL 并评估 Redis / Celery / RQ 等队列。
本文中的“任务中心”指统一的数据模型、调度和用户可见的任务体验;不意味着每一种任务各有一套后台实现。
---
## 2. 产品体验
### 2.1 目标体验
单张“AI 生图 / AI 编辑”弹窗只负责配置,不负责等待:
1. 用户填写要求、选择模型,点击“提交任务”。
2. 页面立即提示“已加入任务队列”,弹窗关闭;用户可继续编辑商品或离开当前页面。
3. 页面提供“处理中(N)”入口,任务完成后图片自动出现在“生图记录”。
4. 套图与批量水印同样进入任务中心,可查看进度、失败原因、取消、重试和结果。
单张 AI 生图和 AI 编辑保留不同入口、提示词与来源图,但后端都属于 `image_edit` 单图生成任务;区别只在任务快照和标题。
### 2.2 统一状态
| 用户看到的状态 | 父任务 / 子项状态 | 含义 |
|---|---|---|
| 排队中 | `queued` | 等待 Worker 领取。 |
| 处理中 | `running` | 正在生成、下载或合成水印。 |
| 等待模型结果 | 子项 `waiting_provider` | 已提交给第三方模型,Worker 后续轮询同一个模型任务。 |
| 已完成 | `done` | 所有子项成功。 |
| 部分完成 | `partial` | 至少一个成功,且有失败或取消项。 |
| 已失败 | `failed` | 没有成功项,失败原因可查看与重试。 |
| 已取消 | `cancelled` | 用户取消;后续结果不会回写商品。 |
### 2.3 当前已呈现的界面
- 单图弹窗提交成功后关闭,生成进度由“生成结果”区域继续跟踪。
- 套图在“生成结果”区域展示排队、进行、完成、取消、失败重试。
- 水印设置浮层展示本次批量水印进度、取消、全部失败项重试和单图重试。
- 试算页右下角“录入商品”上方有固定“任务 / 处理中(N)”入口;点击打开当前商品的右侧任务抽屉,统一显示套图、单图与水印任务。
- 两类任务均会在刷新试算页后恢复跟踪。
**尚未完成的是应用级全局“任务中心”**:当前抽屉只展示当前商品,且生成结果区和水印设置浮层仍保留上下文状态提示。见第 9 节第三阶段。
---
## 3. 统一数据模型
### 3.1 已实现表
#### `background_jobs`(父任务)
一条用户可感知的后台操作,关键字段包括:
| 字段 | 用途 |
|---|---|
| `id`、`product_id`、`kind` | 任务身份、归属商品和任务类型。商品删除后 `product_id` 置空,避免任务反向复活商品。 |
| `status`、`priority`、`cancel_requested` | 调度与取消控制。 |
| `provider`、`model`、`platform`、`lang`、`ratio`、`style_set` | 生图任务的可恢复执行上下文。 |
| `request_payload` | 不可变请求快照:商品上下文、参考图、提示词、风格、水印配置等。 |
| `total`、`completed`、`failed`、`error` | 整体进度和概要错误。 |
| `created_at`、`started_at`、`completed_at`、`updated_at` | 任务历史、排序和诊断。 |
#### `background_job_items`(子项)
可独立领取、失败和重试的最小单元:
| 场景 | 父任务 | 子项 |
|---|---|---|
| 套图生成 | 一次套图请求 | 一张计划生成图 |
| 单张 AI 生图 / 编辑 | 一次编辑请求 | 唯一的一张图 |
| 批量添加水印 | 一次批量操作 | 一张商品素材 |
子项保存 `payload`、状态、尝试次数、租约、结果 URL、生成回写素材 ID、错误、下次执行时间,以及第三方 `provider_task_id` / `provider_task_kind`。
### 3.2 任务类型
| 类型 | 当前状态 | 说明 |
|---|---|---|
| `image_suite` | 已实施 | 套图按“每张图一个子项”执行。 |
| `image_edit` | 已实施 | 单张 AI 生图 / AI 编辑。 |
| `watermark_batch` | 已实施 | 已选素材逐张生成当前水印版。 |
| `export_zip` / 批量下载 | 待开发 | 可复用同一父/子任务模型处理长时间 ZIP。 |
| 孤儿文件清理 | 待开发 | 后台扫描并回收存储异常残留。 |
### 3.3 可选诊断表(未实施)
未来新增 `background_job_events`,记录“提交模型”“获取 provider task ID”“轮询失败”“租约恢复”“用户取消”“人工重试”等事件。它不参与调度,作用是排障、审计和任务中心时间线;当前诊断信息主要存于子项错误文本和服务日志。
---
## 4. 执行、恢复与一致性
### 4.1 Worker 执行流程(已实施)
`start.command` 会额外启动 `python server/worker.py`。Worker 通过数据库租约领取一个符合条件的子项:
```text
queued / waiting_provider
→ Worker 写入 running + 租约
→ 执行一次推进
├─ 模型未完成:持久化 provider_task_id → waiting_provider → 延迟下次轮询
├─ 成功:保存图片 / 回写素材 → done
├─ 可诊断错误:failed
└─ 已取消或商品不存在:cancelled
→ 从所有子项重新计算父任务进度与终态
```
Worker 异常退出时,过期租约会重新回到队列;Web 服务重启不影响已提交的数据库任务。
### 4.2 第三方模型恢复(已实施范围)
- **Pcore、RightAPI、通义万相**:提交后先持久化 `provider_task_id`,Worker 重启后继续轮询同一模型任务,不重复扣费。
- **豆包、同步千问等同步接口**:供应商不提供可恢复的外部任务 ID。若进程恰好在请求期间中断,只能重试,无法保证供应商侧幂等。
- **仍待完善的极小窗口**:若“已向供应商提交”与“成功写入 `provider_task_id`”之间进程崩溃,当前无法完全避免重复提交。第三阶段应引入供应商幂等键;不支持幂等键时,把子项标为“待人工确认”,不盲目重复扣费。
### 4.3 图片生命周期配合(已实施)
批量水印任务创建时会冻结:
- `asset_id`;
- 当时的 `current_storage_key` / 当前图片 URL;
- 水印配置快照。
Worker 始终基于素材 `original` 无水印版本合成新的 `current` 水印版。执行时若素材已删除或 current 版本已经变化,子项失败而不会用旧任务覆盖新图片;单项重试会重新冻结该素材的最新 current 版本。数据库成功切换到新 current 后,才尝试删除旧水印对象。
详细的原图、当前版、删除回收规则见 [`image-asset-lifecycle.md`](./image-asset-lifecycle.md)。
### 4.4 删除与取消(已实施)
删除商品前会取消该商品所有未完成任务。Worker 在落盘和回写素材前再次检查取消标志与商品是否存在;若商品或素材已删除,只丢弃/清理本次临时结果,绝不“复活”商品或素材记录。
---
## 5. 调度与优先级
### 当前实现
当前为一个独立 Worker 进程,按子项粒度领取,排序为优先级优先、再按创建时间:
| 类型 | 当前优先级 | 含义 |
|---|---:|---|
| `image_edit` | `10` | 用户临时单图编辑优先。 |
| `image_suite` | `0` | 常规套图。 |
| `watermark_batch` | `-10` | 低优先级,不应优先于模型生成。 |
这已避免“十张套图作为整体长期占住任务”的问题:套图也是逐张领取,单图编辑可插队。
### 后续目标(未实施)
AI 生成与水印不应长期共用单线程吞吐:
1. AI 按供应商设置串行或小并发及速率限制;单图编辑优先。
2. 批量水印使用独立逻辑队列 / Worker 池,建议并发约 2,不阻塞模型任务。
3. 调度增加公平性:连续处理 2–3 个高优先级单图后,给套图一个执行机会,避免套图饿死。
在 SQLite 单机阶段,这可先通过两个启动参数不同的 Worker 和类型过滤实现;迁移 PostgreSQL 后再做跨进程可靠并发领取。
---
## 6. API 与前端契约
### 当前接口
| 接口 | 返回 / 用途 |
|---|---|
| `POST /api/suite/generate` | 返回 `{ suite_id }`,创建 `image_suite`。 |
| `POST /api/suite/image-edit` | 返回 `{ suite_id }`,创建 `image_edit`。 |
| `POST /api/assets/watermark` | 返回 `{ job_id, total }`,创建 `watermark_batch`。 |
| `GET /api/suites/{id}` | 通用任务详情、父任务进度、逐项状态和错误。 |
| `GET /api/products/{id}/suites?active_only=true` | 恢复活动的套图 / 单图生成任务。 |
| `GET /api/products/{id}/watermark-jobs?active_only=true` | 恢复活动的水印任务。 |
| `POST /api/suites/{id}/cancel` | 取消任务中尚未完成的子项。 |
| `POST /api/suites/{id}/retry` | 重试全部失败子项。 |
| `POST /api/suites/{id}/items/{item_id}/retry` | 仅重试指定失败子项。 |
`GET /api/suites/{id}` 是历史命名;它已经承载水印任务。全局任务中心落地时应新增语义更通用的 `GET /api/background-jobs/{id}`(保留旧接口兼容)。
### 当前前端轮询
前端使用递归 `setTimeout`,在上一轮请求完成后才安排下一轮;网络失败采用指数退避并使用 `AbortController` 取消页面卸载或任务切换后的请求。浏览器会保存最近任务 ID,刷新后还会从服务端查询商品活动任务,因此不依赖单次页面生命周期。
---
## 7. 已完成阶段
| 阶段 | 内容 | 状态 |
|---|---|---|
| 第一阶段:AI 任务可靠性 | 任务表与迁移;独立 Worker;套图与单图编辑迁入持久化任务;provider task ID 持久化;取消、失败重试、刷新恢复。 | ✅ 已完成 |
| 第二阶段:批量水印复用 | `watermark_batch` 逐素材子项;后台合成和版本切换;整体进度、取消、全部失败重试、单项重试、刷新恢复。 | ✅ 已完成 |
| 数据安全配合 | 商品删除取消任务;写入前二次检查;水印任务冻结素材 current 版本,拒绝旧任务覆盖新状态。 | ✅ 已完成 |
| 第三阶段:前端任务中心(第一步) | 当前商品固定任务入口、右侧任务抽屉、任务进度、取消、失败重试、跳转生成结果。 | ✅ 已完成 |
已验证:Python 编译、前端 TypeScript/生产构建;模拟存储下的“创建水印任务 → Worker 完成 → 素材 current 回写”以及“单个失败项按最新版本重试”。真实模型与七牛仍应按第 10 节进行人工回归。
---
## 8. 当前限制
- 任务中心 UI 尚未独立成页面或全局抽屉,任务入口仍分散在生成结果和水印设置中。
- `background_job_events` 尚未建立,复杂问题主要依赖日志与错误摘要。
- 当前一个 Worker 统一领取 AI 与水印任务,优先级有效,但没有独立并发池和公平调度。
- 移除水印仍是短时同步接口;大量素材场景可迁入 `watermark_batch` 同一框架。
- SQLite 适合本机单用户,不适合多机器或高并发抢占。
- 同步供应商无法做到提交中的完全幂等恢复;异步供应商的“提交后未保存 provider ID”窗口尚未用幂等键闭合。
- 历史任务和失败临时文件尚未设置保留期与定期清理策略。
---
## 9. 待开发阶段
### 第三阶段:前端任务中心后续完善
1. **已完成**:在试算页右下角“录入商品”上方提供“任务 / 处理中(N)”入口,打开当前商品任务抽屉。
2. **已完成**:抽屉合并套图、单图编辑、水印任务,展示逐图失败、取消、重试和“查看结果”。
3. 待开发:按类型、状态筛选,展示更细的 `waiting_provider` 时间线与预计耗时。
4. 待开发:新增通用列表接口,例如 `GET /api/background-jobs?product_id=&status=&kind=`;旧 `/suites/*` 继续兼容。
5. 待开发:单图弹窗提交后提供“查看任务”快捷入口,并逐步移除弹窗自身的轮询职责。
### 第四阶段:可靠性、调度与诊断
1. 新增 `background_job_events` 与结构化日志,记录供应商、模型、耗时、错误类别和重试。
2. 引入供应商幂等键或“待人工确认”状态,关闭提交模型后的重复扣费窗口。
3. 拆分 AI 与水印的逻辑 Worker 池,配置并发、供应商限流和公平调度。
4. 增加任务历史保留期、失败临时文件清理和七牛孤儿对象扫描。
5. 把移除水印、耗时 ZIP 导出、批量下载逐步迁入任务中心。
### 第五阶段:多实例基础设施
前提是产品确实需要多用户、多机器或更高吞吐:
1. SQLite 迁移 PostgreSQL。
2. 采用 PostgreSQL 行级锁 / `SKIP LOCKED` 强化子项领取。
3. 再评估 Redis + Celery、RQ 或专业队列;不要在本地单机阶段提前引入。
---
## 10. 回归与运维清单
每次修改任务系统后,至少验证:
1. `./start.command` 会同时启动 Web、前端和 Worker;手动启动 server 时必须另开 `python server/worker.py`。
2. 单图编辑提交后可关闭弹窗;刷新试算页后仍能看到任务和最终图片。
3. 套图多张任务中插入单图编辑,单图应优先领取,套图不丢进度。
4. 批量水印中删除商品或单个素材,Worker 不得回写/复活资源。
5. 改水印任务执行期间再次修改同一素材,旧任务必须失败或跳过,不能覆盖新 current。
6. Pcore / 通义等异步模型在重启 Worker 后继续轮询原 `provider_task_id`,不再次提交。
7. 失败项的全部重试与单项重试都能恢复状态;单项水印重试使用最新素材版本。
8. 真实存储环境检查旧水印对象仅在新 current 数据库提交后删除;失败时不会丢失原图。
## 11. 代码入口
| 位置 | 责任 |
|---|---|
| `server/models/job.py` | 父任务、子项 ORM 模型。 |
| `server/migrations/versions/b5d2c9a3e1f7_add_background_jobs.py` | 任务表迁移。 |
| `server/services/jobs.py` | 创建、领取、租约恢复、状态聚合、取消与重试。 |
| `server/services/job_worker.py` | 执行生成和批量水印子项。 |
| `server/worker.py`、`start.command` | 独立 Worker 命令与启动编排。 |
| `server/api/suite.py` | 套图、单图编辑和通用任务查询接口。 |
| `server/api/materials.py` | 批量水印创建和水印任务列表。 |
| `studio/src/pages/trial/TrialSuitePanel.tsx` | 试算页任务恢复、轮询、取消与重试入口。 |
| `studio/src/pages/trial/AiImageGenModal.tsx` | 单图任务配置与提交入口。 |
+127
View File
@@ -0,0 +1,127 @@
# 图片资产生命周期
> 状态:已实施(2026-08-30)
> 适用范围:采集图、手动上传图、AI 套图、单张 AI 图生图,以及图片水印。
## 1. 核心模型
每个 `product_assets` 图片素材只维护两个逻辑版本:
```text
original:无水印原图,始终保留
current:页面预览、单图下载、ZIP 导出使用的当前版本
```
| 状态 | original | current | 七牛对象数 |
|---|---|---|---|
| 未加水印 | 同一个对象 | 指向 original | 1 |
| 已加水印 | 无水印对象 | 指向水印对象 | 2 |
系统**不保留历史水印版**。修改水印时,先上传新的水印版并切换 current,再删除旧水印版。
采集图、上传图、AI 图的区别只在 original 的来源:采集源站、用户上传文件、模型输出。后续水印、下载、删除均走同一套流程。
## 2. 数据字段
`product_assets` 继续保留 `stored_url` 兼容既有前端接口;其语义为 current URL。新增字段:
| 字段 | 含义 |
|---|---|
| `original_url` | 无水印原图公开 URL |
| `original_storage_key` | 无水印原图对象 Key |
| `current_storage_key` | 当前展示对象 Key |
| `watermark_enabled` | current 是否为水印版 |
| `watermark_config` | 当前水印配置快照 |
删除对象必须使用 `storage_key`,不应由 URL 反推;`storage_key_from_url()` 仅用于兼容旧数据迁移。
迁移:`a41e8c7f4b20_add_asset_image_lifecycle.py`。`start.command` 会在启动服务前执行 `alembic upgrade head`。
## 3. 对象路径
新图片统一使用:
```text
products/{product_id}/assets/{asset_id}/original.{ext}
products/{product_id}/assets/{asset_id}/watermark-{uuid}.{ext}
```
路径前缀不仅便于单图精准删除,也允许删除商品时按 `products/{product_id}/` 兜底清理所有对象,包括异常中断留下的孤儿文件。
## 4. 创建与水印流程
### 采集、上传、无水印 AI 图
```text
写入 original
→ current 指向 original
→ watermark_enabled = false
```
### 添加或修改水印
```text
读取 original
→ 合成新水印图
→ 上传新 current
→ 提交数据库切换 current
→ 删除旧 current(水印版)
```
因此不会叠加水印。AI 生图参考图也优先使用 `original_url`,避免把水印带进下一次生成。
### 移除水印
```text
current 切回 original
→ 提交数据库
→ 删除旧水印对象
```
## 5. 删除流程
### 删除单张素材
```text
删除 products/{product_id}/assets/{asset_id}/ 前缀
→ 删除旧格式路径中的 original/current 对象(兼容迁移前数据)
→ 删除 product_assets 记录
```
若对象存储删除失败,接口返回失败并保留数据库素材记录,避免出现“数据库已删、文件未删”的静默状态。
### 硬删除商品
```text
查询商品所有素材
→ 清理每个素材目录和旧格式对象
→ 清理 products/{product_id}/ 前缀(孤儿兜底)
→ 删除商品,数据库级联删除素材记录
```
归档商品只改变商品状态,不删除图片。
## 6. 服务职责
| 模块 | 职责 |
|---|---|
| `services/image_assets.py` | original/current 生命周期、加/移水印、迁入正式路径、单图/整商品回收 |
| `services/storage.py` | 本地/七牛上传、下载、按 Key 删除、按前缀删除、URL→Key 兼容解析 |
| `services/watermark.py` | 纯图片字节合成;不操作数据库或对象存储 |
| API 路由 | 参数校验、数据库提交、调用生命周期服务 |
## 7. 前端入口
试算页“采集图片”和“生成结果”均可打开水印设置:
- 为已选采集图添加水印;
- 为已选生成图添加水印;
- 移除已选采集图或生成图水印;
- 新的一键生图和单张 AI 生图按当前设置自动加水印。
预览、下载与 ZIP 导出始终使用 `stored_url`(current);AI 生成参考图使用 `original_url`。
## 8. 旧数据兼容
旧素材首次加/移水印时会迁入新的商品/素材路径,并补齐 original/current 字段。迁移前已经产生但数据库无记录的旧七牛对象无法可靠识别;后续可按运营需要增加七牛前缀扫描与人工确认清理工具。
+6 -5
View File
@@ -47,8 +47,8 @@ POST /api/suite/generate
`title(title_zh||title) / price / brand / sales / shop / params(含尺寸、重量覆盖回参数表) / selling_point / desc`。
- **参考图解析**(服务端,ISS 逻辑):variant_name 精确匹配 SKU 图 → 回退 main 组第 1 张;material 偏好第 2 张;最多 2 张;本地 storage 直读,远程 URL 带站点 Referer 下载,转 data-URI 进请求。
- **提示词**:按模型家族分册(alibaba 主体参考语义 / gpt edits 保真语义 / google 主体保持),共用 10 种图类型 builder + 商品上下文(参数表提炼卖点)+ 5 套风格 + `requirements` 置顶强制约束 + 图内文案按语言规范(俄文 ≤4 词)。
- **任务模型**:进程内内存任务表 + `asyncio.Lock` 全局串行队列(防中转限流);逐张生成 → Pillow 水印 → 按 PNG 魔数定扩展名落 `storage`。**重启丢任务、图片不丢**(与 ISS 一致,接受)。
- **前端轮询**:`GET /api/suites/{id}` 每 3s;`done/partial/failed` 终态;连续 3 次失败停止跟踪;超预算(5min/张+10min)提示。
- **任务模型**:数据库父任务 + 逐图子任务,独立 Worker 按子项领取;逐张生成 → Pillow 水印 → 按 PNG 魔数定扩展名落 `storage`。Pcore、RightAPI、通义万相的外部任务 ID 会持久化,Worker/Web 重启后继续查询原任务,不重复提交。详见 [`background-jobs.md`](./background-jobs.md)。
- **前端轮询**:请求结束后再延迟下一次(正常 3s),因此不会发生异步 `setInterval` 重叠;在途请求由 `AbortController` 管理,切换任务/卸载时取消。请求失败最多重试 3 次,并按 6s、12s 指数退避;当前任务 ID 存入浏览器会话,刷新时也会查询商品活动任务。`done/partial/failed/cancelled` 为终态;超预算(5min/张+10min)提示。
## 5. 模型路由(与 ISS 一致,服务端 MODEL_PROVIDERS 表)
@@ -72,14 +72,15 @@ POST /api/suite/generate
```
采集图/生成图单元格 → 「AI生图」按钮
→ 弹窗:左侧原图预览;右侧要求文本域(必填)+ 模型下拉(同上 10 个)+ 生成按钮
→ 弹窗:左侧原图预览;右侧要求文本域(必填)+ 模型下拉(同上 10 个)+ 提交任务按钮
→ POST /api/suite/image-edit { product_id, image_url, prompt, model, append: true }
→ 返回 { url },弹窗内展示结果,可下载/打开原图
→ 立即返回 { suite_id },弹窗轮询 GET /api/suites/{suite_id}
→ 终态后展示 images[0].url,可下载/打开原图
→ 服务端把结果追加为 product_assets(group_key='generated', variant_name=null)
```
- 与「智能修图」页(`/ai-image`,wanx 注解编辑)并存:本入口是**轻量单张再生成**(选模型 + 一句话要求),不做标注/遮罩。
- 服务端实现 = 统一模型路由的单张调用(复用 §5 provider 层 + 参考图即传入的单图 + prompt = 用户要求 + Ozon 平台规格),生成后落 storage 并回写素材;`append=false` 时不回写。
- 服务端实现 = 复用套图的持久化任务和模型路由(参考图即传入的单图 + prompt = 用户要求 + Ozon 平台规格),生成后落 storage 并回写素材;`append=false` 时不回写。提交接口不等待模型完成,用户可关闭弹窗继续工作。
## 8. 展示与防盗链
+167
View File
@@ -0,0 +1,167 @@
# 前后端优化审查清单
> 审查日期:2026-08-29
> 状态:审查项持续跟踪;“图片原图与当前水印版管理”已于 2026-08-30 实施基础版本。
> 范围:商品试算页、图片生成/水印/导出链路,以及对应 FastAPI 服务端。
---
## 优先级说明
| 优先级 | 含义 |
|---|---|
| P0 | 若服务不再局限于本机,需优先处理的安全或资源风险 |
| P1 | 会影响数据可靠性、生成任务稳定性或存储成本 |
| P2 | 提升维护效率、性能与用户体验 |
---
## 1. 服务端:任务可靠性
### P1:生成任务应持久化并异步执行(第一阶段已实施)
- 已实施:新增 `background_jobs`、`background_job_items`,保存请求快照、状态、进度、逐图结果、错误、重试计数与第三方任务 ID;套图和单张编辑均已迁入。
- 已实施:`server/worker.py` 为独立 Worker;Pcore、RightAPI、通义万相在提交模型后先持久化外部任务 ID,再分次轮询,服务或 Worker 重启后继续原任务。
- 已实施:前端可恢复商品活动任务、取消任务、仅重试失败图。
- 边界:SQLite 适用于当前单机;跨机器/多实例前需迁移 PostgreSQL。同步供应商接口没有外部任务 ID,进程恰好中断时不能保证幂等。任务事件日志与历史清理策略留待后续。
### P1:单张 AI 编辑改为异步任务(已实施)
- 已实施:`POST /api/suite/image-edit` 创建并提交与套图共用的任务,立即返回 `suite_id`;单张编辑因此会走统一的模型、队列、水印、落库与追加素材链路。
- 已实施:弹窗提交后交由试算页统一任务区跟踪;请求不再受前端 120 秒 HTTP 超时影响,并支持取消、仅重试失败图与刷新恢复。
- 边界:任务事件日志、历史清理和供应商主动取消仍是后续项;同步供应商没有外部任务 ID 时无法保证中断后的幂等恢复。
### P2:前端轮询避免异步 `setInterval` 重叠(已实施)
- 已实施:套图和单张编辑均改为递归 `setTimeout`;只有前一次 `GET /api/suites/{id}` 完成后才安排下一次请求。
- 已实施:使用 `AbortController` 取消过期/卸载请求;网络失败采用最多 3 次、6s→12s 的指数退避;套图任务 ID 写入 `sessionStorage`,刷新页面后可恢复跟踪。
- 边界:状态已持久化,服务端重启不会导致 404;同步供应商恰在请求中断时需要重试,异步供应商则继续使用保存的外部任务 ID 轮询。
---
## 2. 服务端:图片、水印与存储生命周期
### P1:统一管理原图与当前水印版(已实施基础版本)
- 已实施:素材记录保存原图、当前展示图、对象 key 与水印配置;未加水印时原图即当前图,加水印时仅保留一张当前水印版,不保留水印历史。
- 已实施:采集、上传、AI 生成图片统一迁入 `products/{product_id}/assets/{asset_id}/`;重新加水印在提交后回收旧水印对象,移除水印回到原图。
- 已实施:删除单图清理素材目录,硬删除商品清理商品目录,并兼容清理旧路径引用。详见 [`image-asset-lifecycle.md`](./image-asset-lifecycle.md)。
- 后续:可增加定期扫描与清理孤儿对象的后台任务,用于处理存储服务瞬时失败后的残留。
### P1:批量水印改后台任务(第二阶段已实施)
- 已实施:`POST /api/assets/watermark` 只创建 `watermark_batch` 持久化任务并返回任务 ID;Worker 逐图从原图合成、上传、切换当前展示版本和回收旧水印对象。
- 已实施:页面独立轮询水印任务,刷新可恢复,支持取消、重试全部失败项和按单张重试;等待期间素材被修改时会拒绝旧任务覆盖,单项重试会重新冻结最新版本。
- 边界:移除水印目前仍是同步短操作;若未来单次处理规模继续提高,可迁入同一 Worker 框架。
### P2:统一图片格式、尺寸和元数据校验
- 现状:存储层主要依据 URL 后缀或 Content-Type 推断格式,未统一校验文件大小、像素、实际 MIME 类型和方向信息。
- 涉及:`server/services/storage.py`、`server/api/materials.py`。
- 建议:上传/下载后用图片库校验真实格式,限制像素与文件大小,按需要压缩、纠正 EXIF 方向并记录宽高。
---
## 3. 服务端:安全与资源保护
### P0:限制图片代理和导出 URL,防止 SSRF
- 现状:图片代理、单图下载、ZIP 导出会请求客户端传入的 HTTP(S) URL,并跟随重定向。
- 涉及:`server/api/proxy.py`、`server/api/export.py`、`server/services/storage.py`。
- 风险:若服务可被非本机访问,可能被用于读取内网地址、云元数据地址,或下载超大文件。
- 建议:解析并拒绝回环、内网、链路本地与保留 IP;每次重定向后再次校验;设置域名白名单/允许列表、最大响应体、连接与总耗时上限。
### P0:限制上传与 ZIP 导出的内存占用(已实施基础版本)
- 已实施:上传与远程下载按块读取,默认单张限制 20 MB;超限返回 HTTP 413 和可读错误。限制可通过 `.env` 覆盖。
- 已实施:每次采集最多 100 张;图片导出最多 50 张;ZIP 源图片总量和成品最大 200 MB。
- 已实施:ZIP 使用 `SpooledTemporaryFile`,超过 2 MB 自动转临时磁盘,响应以 64 KB 分块输出,不再使用无上限 `BytesIO`。
- 后续:若部署为多实例/容器,需要为临时目录增加独立容量配额与定期清理监控。
### P0:服务脱离本机后补鉴权与资源归属
- 现状:V2.1 按决策未启用鉴权,商品、素材、生成、导出、代理接口都可直接访问。
- 涉及:`server/main.py`、全部业务路由。
- 建议:上线局域网/公网前先接入认证、商品/素材归属校验、请求限流和审计日志。
- 备注:若服务始终只绑定本机回环地址,可作为后续事项,但不应暴露端口。
---
## 4. 前端:可维护性与交互
### P2:拆分 TrialSuitePanel
- 现状:`TrialSuitePanel.tsx` 已约 1,400 行,采集图和生成图卡片、悬浮操作、选择、下载等逻辑有重复。
- 建议:拆分为素材网格、结果网格、图片动作层、预览下载、水印设置、套图轮询等子组件/hooks。
- 收益:降低后续 UI 修改遗漏风险,便于复用和测试。
### P2:图片多时采用懒加载/虚拟化
- 现状:图片网格会一次性创建全部 Ant Design Image 与预览节点。
- 建议:大量历史生成图时采用分页、虚拟列表或按需加载;缩略图使用合适尺寸和 `loading="lazy"`。
### P2:补触屏与键盘操作
- 现状:删除、下载、AI 编辑等操作多为 hover 展示。
- 建议:增加焦点可见性、键盘标签与快捷操作;移动端使用常显按钮或“更多”菜单。
### P2:将水印设置按商品或用户保存
- 现状:水印设置写入浏览器全局 localStorage,同一浏览器的所有商品共享。
- 涉及:`trialWatermark`。
- 建议:明确产品语义:若按店铺/用户通用则服务端保存偏好;若按商品不同则保存到商品配置。
---
## 5. 数据、导出与接口契约
### P1:登记表与批量 CSV 服务端化
- 现状:登记记录保存在 `trialRegisterRecords` localStorage,不跨设备、清理浏览器数据后丢失,也无法多人/多窗口协作。
- 涉及:`studio/src/pages/trial/TrialExportPanel.tsx`。
- 建议:建立登记记录表,并实现服务端批量 CSV 导出;保留当前本地模式作为离线兜底(如有需要)。
### P2:减少前后端手工接口类型重复
- 现状:前端服务层与 FastAPI schema 分别维护,请求字段随功能增加后容易漂移。
- 涉及:`studio/src/services/*.ts`、`server/schemas/*.py`。
- 建议:从 OpenAPI 自动生成 TypeScript 客户端,或至少以契约测试校验关键请求/响应。
### P2:补充高频查询组合索引
- 现状:商品与素材已有单列索引;随着数据增长,按商品、分组、排序、状态组合查询会变多。
- 涉及:`server/models/product.py`、`server/models/asset.py`。
- 建议:在真实数据库和访问量出现后,用慢查询确认并增加如 `(product_id, group_key, sort_order)` 的组合索引;避免过早添加无效索引。
---
## 6. 测试、交付与可观测性
### P1:建立自动化测试基础
- 现状:未发现前后端单测、契约测试或端到端测试目录;`studio` 仅提供 dev/build/preview 脚本。
- 建议:
1. 后端:计价、水印、文件名、任务状态、URL 安全校验、导出。
2. 前端:价格计算、选择/导出、水印设置、图片操作组件。
3. E2E:采集素材 → 试算 → 单张/套图生成 → 水印 → 导出。
### P2:统一日志、指标和错误码
- 现状:生成失败主要保存在任务错误文本或日志中,缺少 provider、模型、耗时、重试、存储失败等聚合指标。
- 建议:结构化日志中加入 task_id、product_id、provider、model、耗时、错误类别;健康检查补充存储与关键第三方状态。
### P1:先提交当前已验证改动,再做重构
- 现状:工作区存在多个未提交的前后端改动。
- 建议:按“水印/图片交互”“提示词”“导出”等模块提交;后续优化在独立提交或分支完成,便于回滚与定位。
---
## 建议实施顺序
1. **P0 资源与 URL 防护**:SSRF、文件大小、ZIP 限制。
2. **P1 任务与文件可靠性**:异步持久化任务、图片版本和清理策略。
3. **P1 测试基础**:先覆盖水印、导出、任务、URL 安全。
4. **P1 登记表服务端化**:持久化登记记录和批量导出。
5. **P2 前端拆分与性能**:拆 TrialSuitePanel、图片虚拟化、触屏/键盘体验。
6. **P2 契约、索引、可观测性**:依据真实使用量逐步完善。