From 9d15d8f784aba4809e292bd02da276f9125b3161 Mon Sep 17 00:00:00 2001 From: Joey Date: Sun, 16 Aug 2026 22:20:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20deepseek=20=E7=9A=84=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- extension/entrypoints/sidepanel/App.tsx | 111 +++++++++++++++++---- extension/entrypoints/sidepanel/index.html | 2 + extension/src/api/client.ts | 32 ++++-- extension/src/profiles/types.ts | 2 +- server/api/generate.py | 3 +- server/api/suites.py | 1 + server/api/upload.py | 32 ++++++ server/db.py | 2 + server/main.py | 3 +- server/models.py | 1 + server/schemas.py | 3 + server/services/generator.py | 18 +++- server/services/planner.py | 41 +++++++- server/services/prompt.py | 13 ++- 14 files changed, 229 insertions(+), 35 deletions(-) create mode 100644 server/api/upload.py diff --git a/extension/entrypoints/sidepanel/App.tsx b/extension/entrypoints/sidepanel/App.tsx index 0d8b555..cb7cddf 100644 --- a/extension/entrypoints/sidepanel/App.tsx +++ b/extension/entrypoints/sidepanel/App.tsx @@ -11,10 +11,10 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; import { App as AntApp, ConfigProvider, Popover, Progress, Select } from 'antd'; -import { SettingOutlined, DownloadOutlined, ThunderboltOutlined } from '@ant-design/icons'; +import { SettingOutlined, DownloadOutlined, ThunderboltOutlined, UploadOutlined } from '@ant-design/icons'; import type { ScanResult, ImageMaterial } from '../../src/collector/scan'; import { - buildGeneratePayload, suiteZipUrl, + buildGeneratePayload, suiteZipUrl, uploadImage, DEFAULT_PLAN, IMAGE_MODEL_OPTIONS, PLATFORM_OPTIONS, PLATFORM_SPECS, STYLE_SET_OPTIONS, type PlanItem, type SuiteInfo, } from '../../src/api/client'; @@ -121,6 +121,12 @@ const App: React.FC = () => { const [descEdit, setDescEdit] = useState(''); const [paramsOpen, setParamsOpen] = useState(false); + // 手动上传图片(补充参考图,独立「upload」分组) + const [uploadedImages, setUploadedImages] = useState([]); + const [uploading, setUploading] = useState(false); + const fileRef = useRef(null); + const uploadSeqRef = useRef(0); + // 服务端 const [settings, setSettings] = useState({ baseUrl: 'http://127.0.0.1:3300', token: '' }); @@ -131,6 +137,8 @@ const App: React.FC = () => { const [model, setModel] = useState('wan2.7-image-pro'); /** 用户改写的风格提示词(按风格 id 存,切风格不丢) */ const [stylePrompts, setStylePrompts] = useState>({}); + /** 生图要求(最高优先级,强制约束,覆盖其他设定) */ + const [requirements, setRequirements] = useState(''); const [plan, setPlan] = useState(DEFAULT_PLAN.map(p => ({ ...p }))); const [planSource, setPlanSource] = useState<'default' | 'ai'>('default'); const [planSummary, setPlanSummary] = useState(''); @@ -261,7 +269,7 @@ const App: React.FC = () => { if (['done', 'partial', 'failed'].includes(s.status)) { stopPolling(); setGenerating(false); - if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: '可重试或更换风格重新生成', okText: '知道了' }); + if (s.status === 'partial') modal.warning({ title: '部分生成失败', content: s.error || '可重试或更换风格重新生成', okText: '知道了' }); if (s.status === 'failed') modal.error({ title: '生成失败', content: s.error || '未知错误', okText: '知道了' }); } } catch (e) { @@ -314,8 +322,8 @@ const App: React.FC = () => { setSuite(null); try { const payload = buildGeneratePayload( - result, selectedKeys, editedTexts(), - { style_set: styleSet, style_prompt: currentStylePrompt, plan: activePlan, platform, model }, + allImages, selectedKeys, editedTexts(), + { style_set: styleSet, style_prompt: currentStylePrompt, requirements: requirements.trim() || null, plan: activePlan, platform, model }, ); const { suite_id } = await send<{ suite_id: string }>('generateSuite', { baseUrl: settings.baseUrl, token: settings.token, payload, @@ -343,6 +351,7 @@ const App: React.FC = () => { sku_variants: skuVariants, image_stats: result.stats, platform, + requirements: requirements.trim() || null, }, }); if (seq !== planSeqRef.current) return; // 规划已被重新采集重置,丢弃过期响应 @@ -398,13 +407,15 @@ const App: React.FC = () => { setSelectedKeys(next); }; - const groupImages = (groupKey: string): ImageMaterial[] => - result?.images.filter(i => i.groupKey === groupKey) ?? []; + /** 全部可选图片:采集结果 + 手动上传(上传图单独一组「upload」) */ + const allImages: ImageMaterial[] = result ? [...result.images, ...uploadedImages] : uploadedImages; - /** 预览用的全量图序列(主图→SKU→详情,与展示顺序一致) */ - const collectedPreviewList: string[] = result - ? (['main', 'sku', 'detail'] as const).flatMap(g => groupImages(g).map(i => i.url)) - : []; + const groupImages = (groupKey: string): ImageMaterial[] => + allImages.filter(i => i.groupKey === groupKey); + + /** 预览用的全量图序列(主图→SKU→详情→上传,与展示顺序一致) */ + const collectedPreviewList: string[] = (['main', 'sku', 'detail', 'upload'] as const) + .flatMap(g => groupImages(g).map(i => i.url)); /** 生成结果的预览序列(仅成功的图) */ const resultPreviewList: string[] = suite ? suite.images.filter(i => i.status === 'ok').map(i => i.url) : []; @@ -418,6 +429,43 @@ const App: React.FC = () => { setPlan(prev => prev.map((p, i) => i === idx ? { ...p, count } : p)); }; + /** 手动上传:点击触发隐藏的 file input */ + const handleUpload = () => fileRef.current?.click(); + + const onFilesChange = async (e: React.ChangeEvent) => { + const files = Array.from(e.target.files ?? []); + e.target.value = ''; // 清空以便再次选择同名文件 + if (files.length === 0) return; + setUploading(true); + const added: ImageMaterial[] = []; + for (const f of files) { + try { + const { url } = await uploadImage(settings.baseUrl, settings.token, f); + added.push({ + key: `upload-${String(++uploadSeqRef.current).padStart(3, '0')}`, + groupKey: 'upload', + groupName: '手动上传', + url, + thumbUrl: url, + index: added.length, + type: 'img', + }); + } catch (err) { + modal.error({ title: '上传失败', content: `${f.name}:${err instanceof Error ? err.message : String(err)}`, okText: '知道了' }); + } + } + if (added.length > 0) { + setUploadedImages(prev => [...prev, ...added]); + setSelectedKeys(prev => { + const next = new Set(prev); + added.forEach(a => next.add(a.key)); + return next; + }); + modal.success({ title: `已上传 ${added.length} 张图片` }); + } + setUploading(false); + }; + /** 源站图防盗链时的兜底:走服务端图片代理 */ const proxied = (u: string) => `${settings.baseUrl.replace(/\/$/, '')}/api/proxy-image?url=${encodeURIComponent(u)}`; @@ -567,17 +615,34 @@ const App: React.FC = () => { no="02" title="采集图片" className="section-images" - extra={result ? `已选 ${selectedKeys.size} / ${result.images.filter(i => i.type !== 'video').length}` : undefined} + extra={ + + {(result || uploadedImages.length > 0) && ( + 已选 {selectedKeys.size} / {allImages.filter(i => i.type !== 'video').length} + )} + + + } > - {!result ? ( -
采集后在此勾选图片
+ + {allImages.length === 0 ? ( +
采集后在此勾选图片,或点右上角「上传图片」手动补充
) : (
- {['main', 'sku', 'detail'].map(g => groupImages(g).length > 0 && ( + {(['main', 'sku', 'detail', 'upload'] as const).map(g => groupImages(g).length > 0 && (
- {g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : '详情图'} + {g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : g === 'detail' ? '详情图' : '手动上传'} {groupImages(g).length} { {p.title} {p.variant_name && {p.variant_name}} {p.detail && {p.detail}} + {p.prompt_hint && 🎯 {p.prompt_hint}}
setPlanCount(idx, v)} />
@@ -698,6 +764,15 @@ const App: React.FC = () => { onChange={(e) => setStylePrompts(p => ({ ...p, [styleSet]: e.target.value }))} />
+
+ +