feat: 开发采集、采集箱和商品编辑功能

This commit is contained in:
Joey
2026-08-15 22:17:26 +08:00
parent c61d1a3154
commit 36357843d0
130 changed files with 18005 additions and 12 deletions
+218
View File
@@ -0,0 +1,218 @@
# Ozon Seller Kit 现有能力清单(V2 方案设计输入)
> 依据代码逐文件核对生成(server/ 与 studio/,忽略 node_modules/.venv/__pycache__/.output)。
> 数据来源文件:`ozon-seller-kit/server/**` 与 `ozon-seller-kit/studio/src/**`,全部行号以当前工作区为准。
---
## 一、后端 API 清单
FastAPI 应用入口 `server/main.py`
- 应用名 `Ozon Seller Kit` v0.1.0main.py:13)。
- CORS 中间件:仅当 `cors_origin_list` 非空时启用,`allow_credentials=True`,方法/请求头全放行(main.py:16-23)。
- 挂载三个路由:`api/ai``api/image``api/ozon`main.py:25-27)。
- `GET /api/health``{"status":"ok"}`main.py:30-32)。
- 若仓库根 `web/`v1 工具台,冻结)存在,则 `app.mount("/", StaticFiles(html=True))` 静态托管(main.py:11, 35-36)——即生产形态下 FastAPI 同源托管前端。
| 方法 | 路径 | 功能 | 关键入参 | 关键出参 |
|---|---|---|---|---|
| GET | `/api/health` | 健康检查(main.py:30-32 | 无 | `{status: "ok"}` |
| GET | `/api/ai/models` | 列出可用模型(ai.py:10-12 → models_catalog.list_model_options | 无 | `{default: str, models: [{id, label}]}` |
| POST | `/api/ai/copy` | 生成 Ozon 俄文商品文案(ai.py:15-17 → deepseek.generate_copy | `CopyRequest``source_text`(≥10字符)、`product_name``model_code``model`(可选) | `CopyResponse``titles_ru/zh[2]``description_ru/zh``tags_ru/zh``model``usage{prompt_tokens,completion_tokens}` |
| POST | `/api/image/edit` | AI 图生图/图像编辑(image.py:9-11 → image_edit.edit_image | `ImageEditRequest``base_image`(dataURL/公网URL)、`prompt``model`(白名单)、`mask_image?``function?``n`(1-4)、`size?``seed?``style?``prompt_extend``strength?` | `ImageEditResponse``task_id``results[{url(24h), image_base64(dataURL)}]``image_count``request_id` |
| — | `/api/ozon/*` | **空占位**ozon.py:3-5):仅定义前缀与注释 `# Phase 3: Ozon Seller API product upload`,无任何端点 | — | — |
前端调用面:`studio/src/services/image.ts` 只调 `/api/image/edit``/api/ai/*` 目前只有 v1 工具台 `web/js/ai-copy.js`233 行 models、299 行 copy,用原生 fetch)在消费。
---
## 二、配置与模型目录机制
### 2.1 环境变量(server/config/settings.py + 仓库根 `.env`
- `load_dotenv` 与 pydantic-settings 均指向仓库根 `.env`settings.py:8-9, 16-17`parents[2]` 上跳两级)。
- 字段(settings.py:21-29):
- `deepseek_api_key` / `openai_api_key` / `dashscope_api_key`
- `dashscope_base_http_api_url`(华北2北京业务空间专用,普通 API Key 留空)
- `host=127.0.0.1``port=8800`uvicorn 启动参数)
- `cors_origins`(逗号分隔)→ 属性 `cors_origin_list`settings.py:31-35
- `get_settings()``@lru_cache`settings.py:38-40)。
- `.env.example` 给出全部变量名:`DEEPSEEK_API_KEY``DASHSCOPE_API_KEY``DASHSCOPE_BASE_HTTP_API_URL``HOST``PORT``CORS_ORIGINS`
### 2.2 模型目录(server/config/models.yaml + services/models_catalog.py
- YAML 结构:`default`(默认模型 id+ `models[]`,每项含 `id/label/provider/api_model/base_url/api_key_env/max_tokens/params`
- 密钥**不写入 yaml**,只引用环境变量名(`api_key_env: DEEPSEEK_API_KEY`)。
- 当前仅两个 deepseek 模型(deepseek-v4-flash 默认、deepseek-v4-pro),均 `base_url=https://api.deepseek.com``max_tokens=4000``params.thinking.type=disabled`(关闭思维链,防止推理耗尽 token 正文为空)。
- `models_catalog.py`
- `ModelSpec` Pydantic 模型(16-25 行),`params` 为任意 dict、直接并入请求体。
- `load_models_file()``@lru_cache`43-54 行),校验 default 在列表中、列表非空;文件缺失抛 RuntimeError。
- `list_model_options()`57-62 行)→ 前端下拉用 `{id,label}`
- `get_model_spec(model_id)`65-74 行):空值回落 default,未知 id 抛 400。
- `resolve_api_key(spec)`77-83 行):按 `api_key_env` 读环境变量,缺失抛 500。
- 设计要点:**模型即配置**——新增模型只需改 yaml + 加环境变量,代码零改动(deepseek 类);这是 V2 可直接继承的机制。
### 2.3 依赖(server/requirements.txt
`fastapi>=0.115``uvicorn[standard]>=0.32``httpx>=0.27``pydantic-settings>=2.6``python-dotenv``PyYAML``dashscope>=1.23.8`
---
## 三、AI 文案服务细节(services/deepseek.py + prompts/copy_ru.py
### 3.1 调用链
`POST /api/ai/copy``generate_copy(req)`deepseek.py:144-181):
1. `get_model_spec(req.model)` 取模型规格,未传用默认。
2. messages = system(`SYSTEM_PROMPT`) + user(`build_user_prompt(...)`)system 完整文本见 copy_ru.py:1-65。
3. `_chat_once(spec, messages)`deepseek.py:93-141):
- URL = `base_url + /chat/completions`Bearer 认证。
- payload`model=api_model``temperature=0.45`(事实稳定、营销留少量变化)、`max_tokens``response_format={type:"json_object"}``**spec.params`
- httpx 超时 90s;网络错误/HTTP≥400 一律 502detail 截断 500 字符)。
- 解析 `body["choices"][0]["message"]["content"]`;若 `finish_reason=="length"` 且正文为空 → 502 并提示调 max_tokens 或关思维链(131-138 行,对应 yaml 中 thinking disabled 的注释)。
4. **重试机制**(159-177 行):最多 2 次;解析失败时把上一次输出以 `role=assistant` 追加,再追加一条"请仅重新输出合法 JSON"的 user 消息重试一次;仍失败抛 502。
### 3.2 JSON 解析与字段清洗
- `_extract_json_object`17-37 行):剥 ```json 代码块 → `json.loads` → 失败则截取首 `{` 到末 `}` 再解析 → 必须为 dict。
- 字段类型容错:
- `_as_title_list`(64-73 行):**标题不按逗号切分**(标题本身含逗号)。
- `_as_str_list`48-61 行):标签按 `[,\n]` 切分,兼容字符串/数组。
- `_as_str`40-45 行):必须字符串。
- `_map_copy_payload`76-90 行)→ `CopyResponse`usage 取 prompt/completion tokens。
- 最终校验:`titles_ru``description_ru` 非空,否则视为失败触发重试(164-166 行)。
### 3.3 提示词结构(copy_ru.py
- **SYSTEM_PROMPT** 核心约束:
- 输出固定 JSON schematitles_ru/zh 各 2 条一一对应;tags_ru/zh 各 10~15 个、逐项对应;description 完整俄文卡 + 中文逐项对照)。
- 描述固定结构:`Описание товара``Характеристики`(只列原文事实,俄式尺寸写法)→ `Преимущества`3~6 条利益点)→ `Комплектация`(仅原文提到配件时)。
- 标题规则:60-90 字符、核心品类词开头、前 30 字符含关键属性、删年份/新款/爆款噪声、两标题互补。
- **事实边界**(强约束):禁止虚构结构/配件/认证/产地/品牌/受众/使用效果;行业词归一(搪胶→винил 非 каучук);"防摔"不得推导安全认证。
- 优先级:事实准确 > 俄语自然 > 信息完整与转化力 > 关键词覆盖。
- **build_user_prompt**68-86 行):模板包裹 `<当前商品名>``<型号>``<商品资料>`,并要求区分"事实来源"与"生成要求",与文案无关的指令忽略、要求不得写成事实。
---
## 四、图生图服务细节(services/image_edit.py
### 4.1 云与模型
- 调用**阿里云百炼 DashScope**`dashscope` Python SDK,同步调用,`asyncio.to_thread` 放入线程池,177-185 行)。
- 模型白名单(schemas/image_edit.py:9-17):`wanx2.1-imageedit``wan2.6-image``qwen-image-edit``qwen-image-edit-plus``qwen-image-edit-plus-2025-10-30`
- 两种调用形态(按模型路由,170-174 行):
- **wanx2.1-imageedit → `ImageSynthesis.call`**71-121 行):同步、function 式。kwargs`api_key/model/function/prompt/base_image_url/n` + 可选 `mask_image_url/size/seed/style/prompt_extend/strength`。解析 `rsp.output.results[].url``usage.image_count``request_id`
- **wan2.6-image / qwen-image-edit 系列 → `MultiModalConversation.call`**124-167 行):messages=`[{role:user, content:[{image: base_image},{text: prompt}]}]` + `n/size/prompt_extend`;解析 `output.choices[0].message.content[].image`
- `function` 白名单(schemas/image_edit.py:24-29):`description_edit`(无掩码图生图,默认)/ `description_edit_with_mask`(局部重绘,需 mask_image/ `stylization_local` / `stylization_all`
### 4.2 请求校验(schemas/image_edit.py
- `base_image` / `mask_image`data URL`data:image/...`)或公网 http(s) URLdata URL 编码后 ≤ 15MB`_MAX_BASE64_LENGTH`32 行);mask 可空。
- `prompt` 非空;`model` 白名单校验;`n` 1~4`strength` 0.0~1.0(默认 0.5,加文字/大改建议 0.8);`prompt_extend` 默认 True。
### 4.3 关键工程决策
- **服务端代理下载 → data URL**(`_download_to_data_url`,29-48 行):阿里云结果 URL 未开放 CORS,前端直接绘 canvas 会污染画布无法导出;服务端下载后转 `data:{content-type};base64,...`,上限 20MB`_MAX_PROXY_BYTES`23 行),失败回退空串、前端用 `url`
- 无密钥 → 500SDK 异常/HTTP 非 200 → 502`_fail`60-64 行)。
- `_apply_base_url`(51-57 行):仅北京业务空间需设 `dashscope.base_http_api_url`
- 文件头注释明确:**当前不落盘、不存储图片**,将来接七牛云可在返回 URL 后加"下载并转存"步骤,api 层与前端契约不动(image_edit.py:1-9)——这是刻意的扩展点。
---
## 五、studio 前端页面/组件/功能清单与交互流程
### 5.1 技术栈(studio/package.json + vite.config.ts
- React 19.2、react-router 7createBrowserRouter)、antd 6.1zhCN)、axios、Vite 7SWC 插件)、TS 5.9`@` 别名 → `src`
- dev server:端口 8900strictPort、open),`/api` 代理到 `http://127.0.0.1:8800`vite.config.ts:17-22)。
### 5.2 页面与路由
| 文件 | 内容 |
|---|---|
| src/main.tsx | createRoot 挂载 |
| src/App.tsx | ConfigProviderzhCN、主色 #8b5cf6、圆角 8+ AntdApp + RouterProvider |
| src/router/index.tsx | `/``/ai-image` → AiImagePageMainLayout 内);`*` → Navigate `/` |
| src/layouts/MainLayout.tsx | 固定 Sider(240, 深色, 可折叠) + Header(页面标题/副标题) + Content(Outlet)<768px 切 Drawer 移动菜单 |
| src/layouts/SidebarMenu.tsx | antd Menu"主要功能"分组,点击 navigate |
| src/layouts/menuConfig.tsx | **仅一个菜单项**`/ai-image`「AI 图生图」(subtitle:上传图片、加水印、用万相模型进行图生图编辑);`getPageInfo` 路径→标题映射 |
### 5.3 AiImagePagepages/ai-image/AiImagePage.tsx)——主页面
状态:图片列表 `ImageItem[]`id/fileName/image/dataUrl/state)、全局水印设置(type=image|text、text 默认 'Panda Store'、opacity 默认 30、水印图 `/imgs/watermark.jpg` 预加载为 HTMLImageElement)、编辑弹窗(modalOpen/editing)。
区块与流程:
1. **水印设置面板**:类型 Radio(图片/文字)→ 文字输入 或 水印图预览(圆形贴图);透明度 Slider(0-100)。
2. **上传**`Upload.Dragger` 多图、`accept=image/*``beforeUpload` 返回 false(阻止自动上传,纯前端读文件)→ `fileToImage`FileReader → dataURL → Image)→ 追加进列表,按图片尺寸算默认水印位置(右下角)。
3. **预览网格**:每张图一个 `WatermarkCanvas`(可拖拽水印,坐标相对原图、`clampPos` 限制不越界);卡片操作:加水印/清除水印(`toggleWatermark`)、**AI 生图**`openEdit``renderFullRes` 全分辨率合成水印图 → dataURL → 打开弹窗)、导出 PNG(`downloadCanvas`,文件名加 `_watermark` 后缀)、删除;顶部"全部加水印"(`applyToAll` 重建所有 state)与"清空所有"。
4. **AI 生图弹窗** = ImageEditModal(见下)。
### 5.4 ImageEditModalpages/ai-image/components/ImageEditModal.tsx)——编辑工作台 Drawer
右抽屉(`min(1240px, 96vw)`),每次打开重置状态;三段式布局 + 底部 AI 指令条:
- **左:底图切换 Thumb 列表**——「原图」+ 每次 AI 生成的结果图(`AiResult{id,image,dataUrl,url}`),点击切换当前底图(标注可叠加在 AI 结果上继续编辑)。
- **中:AnnotationCanvas**(预览最大宽 900)——标注画布,Pointer 交互:拖拽移动、旋转手柄旋转、四角手柄缩放(文字改 fontSize、标尺等比改 length/tSize/lineWidth/labelFontSize);命中检测 `hitTest`(旋转手柄 → 四角 → body);`onBeginInteraction` 每次交互开始时压撤销快照。
- **右:属性编辑**——Tabs(文字/标尺);添加文字/标尺按钮;选中元素属性面板 ElementPropsPanel**样式预设**:选中元素可"保存预设"localStorage key `ozon_annotation_presets_v1`,只存样式不含内容/位置),新建元素/选中元素可套用预设;撤销/清空/导出 PNG。
- **底部 AI 条**TextArea 指令(Enter 快捷生成)+ 模型 Select(5 个模型带中文说明)+「AI 生成」按钮。
- **runAi**195-228 行):`editImage({base_image: currentDataUrl, prompt, model, n:1, strength:0.8, prompt_extend:true})` → 取 `results[0]`,优先 `image_base64` 否则 `url` → 解码为 Image 追加为结果底图并自动切换。出错用 `apiErrorMessage` 提示。
- **exportImage**230-240 行):新 canvas 合成底图 + 全部标注元素 → PNG 下载。
### 5.5 组件与工具细节
| 文件 | 能力 |
|---|---|
| components/WatermarkCanvas.tsx | 单图水印预览(PREVIEW_MAX_WIDTH=360),命中水印区域拖拽,坐标映射回原图,`clampPos` 防越界 |
| components/AnnotationCanvas.tsx | 标注画布(预览最大宽 900):绘制底图+元素+选中框;Pointer 交互(move/rotate/resize);交互开始回调存撤销 |
| components/ElementPropsPanel.tsx | 文字:内容/字体(9 种)/字号/加粗/文字色/描边色/描边宽;标尺:长度/线色/线宽/T字大小/标签文字/标签色/标签字号;公共:透明度/旋转 |
| utils/watermark.ts | `WATERMARK_SCALE=0.15`(图片水印直径比)、`WATERMARK_MARGIN=10``WATERMARK_TEXT_SCALE=0.051``getWatermarkSize/clampPos/defaultPos``drawWatermarked`:图片水印圆形裁剪+全局透明度,文字水印先绘离屏层再合成(避免描边透出);`renderFullRes` 全分辨率合成 |
| utils/annotation.ts | `FONT_FAMILIES``HANDLE_RADIUS=7``ROTATE_GAP=26``measureText/elementBox/toLocal/hitTest``drawElement/drawText/drawRuler/drawSelectionHandles``createTextElement/createRulerElement` 工厂(默认值随图宽缩放) |
| utils/image.ts | `fileToImage``canvasToDataUrl``downloadCanvas` |
| types/image.ts | `WatermarkType/WatermarkState/ImageItem/ImageEditRequest/ImageEditResponse`(与后端 schema 字段一致) |
| types/annotation.ts | `TextElement/RulerElement`(中心点/旋转/透明度 + 各自样式字段) |
| services/api.ts | axios 实例:baseURL=`envConfig.apiBaseUrl`(默认 `/api`)、**timeout 120s**`api.get/post` 解包 `res.data``apiErrorMessage` 提取 FastAPI `detail` |
| services/image.ts | `editImage(payload)``POST /image/edit` |
| config/env.ts | `VITE_API_BASE_URL`(默认 `/api`)、`VITE_APP_NAME``debug=import.meta.env.DEV` |
### 5.6 前后端衔接方式
- 前端**只调 `/api/image/edit`**services/image.ts);`/api/ai/*` 暂无 studio 页面(v1 `web/js/ai-copy.js` 在消费,v1 已冻结)。
- 图片以 data URL 在请求体内传输(15MB 上限),响应以服务端代理的 `image_base64` 为主、`url` 兜底——专为规避阿里云结果 URL 无 CORS 的画布污染问题。
---
## 六、可直接复用到 V2 vs 需要重写的部分
### 6.1 可直接复用(成熟、结构清晰、低耦合)
| 资产 | 理由 |
|---|---|
| 模型目录机制(models.yaml + models_catalog.py | "模型即配置":换/加 LLM 只改 yaml+env,代码零改动;`resolve_api_key` 按 env 名取密钥,安全;V2 加多厂商直接扩展 |
| DeepSeek 调用骨架(deepseek.py 的 `_chat_once` + `_extract_json_object` + 重试纠错循环) | 通用性强:JSON 输出强制、代码块剥离、容错截取、失败追加修正消息重试一次;可抽象为通用 "JSON 任务 LLM 调用器" 供 V2 任意生成任务复用 |
| 文案 schema 与提示词(schemas/copy.py + prompts/copy_ru.py | 领域逻辑已打磨(事实边界、俄语标题/描述结构、中俄对照);V2 若保留文案生成,整套平移 |
| 图生图服务(services/image_edit.py + schemas/image_edit.py | 多模型路由(ImageSynthesis vs MultiModalConversation)、服务端代理下载规避 CORS、线程池隔离同步 SDK、响应契约(task_id/request_id 已预留异步字段);注释已规划"返回后接七牛转存"扩展点,与 V2 存储需求天然衔接 |
| 前端 API 层(services/api.ts + services/image.ts + config/env.ts | axios 封装(120s 超时、detail 提取)与端点封装可原样复用;新增端点照 image.ts 模式加即可 |
| 水印/图片/标注工具集(utils/watermark.ts、utils/image.ts、utils/annotation.ts | 纯 canvas 数学、框架无关、按原图坐标系建模(预览缩放/全分辨率导出分离),可直接搬入 V2 |
| 标注数据模型(types/annotation.tsTextElement/RulerElement | 字段设计合理(中心点+旋转+透明度+样式),可直接作为 V2 标注/叠加元素的数据契约种子,甚至与后端共享 schema |
| 布局壳(MainLayout + SidebarMenu + menuConfig | 菜单配置驱动、移动端适配完整;V2 加页面只改 menuConfig + router |
| 水印批处理主流程(AiImagePage 上传→全局水印→批量套用→导出) | 完整闭环,可整体作为 V2 的一个功能模块迁入 |
### 6.2 需要重写/新建(当前缺失或形态不适配 V2)
| 资产 | 现状与重写理由 |
|---|---|
| Ozon 对接(api/ozon.py | 纯占位,V2 若做"发布到 Ozon"需从零实现 Seller API(token 管理、类目树、商品上传、图片上传等) |
| 前端 AI 文案页 | studio 完全没有文案 UI;后端 `/api/ai/copy` 就绪,V2 需新建页面(可参考 v1 web/js/ai-copy.js 的表单交互) |
| 服务端文件/任务持久化 | 现状:图不落盘、无上传端点、无 DB、无任务队列;图生图同步等待(前端 120s 超时)。V2 若引入"商品文件夹"契约(README 所述四部分衔接点)与异步任务,需新增上传/资产/任务/持久化体系 |
| ImageEditModal 状态管理 | 编辑+AI 生成+撤销+预设全在组件本地 useState;V2 若需跨步骤工作流/历史/多页共享,需抽出 store(如 zustand/redux)与后端任务状态同步 |
| AiImagePage 批量状态 | 纯内存 useState,刷新即失;V2 按商品文件夹组织时需要持久化/恢复会话 |
| 鉴权与错误治理 | 无任何鉴权、无统一错误码规范(全部 HTTPException detail 字符串);V2 多用户/上线需补齐 |
| 水印可配置性 | 水印图硬编码 `/imgs/watermark.jpg`、比例/字号为常量;V2 应支持自定义水印资源与配置 |
| 路由/菜单 | 仅 1 页;V2 多页面需按功能域重组(当前结构太薄,扩展时建议直接重构而非继续堆叠) |
| 测试与文档链 | 无自动化测试;契约靠 README/docs 维护。V2 建议引入 schema 共享(README 中 `packages/schema` 待建项)避免前后端字段漂移 |
### 6.3 一句话结论
**后端资产(模型目录、LLM 调用骨架、图生图多模型服务、schema)质量高且刻意留了扩展点(异步字段、七牛转存注释),可大幅平移;前端可平移的是纯 canvas 工具层与布局壳,而所有"产品化"能力——Ozon 对接、文案 UI、持久化/资产、异步任务、鉴权、状态管理——目前为空或雏形,是 V2 的主要建设量。**