feat: deepseek 的一些修改

This commit is contained in:
Joey
2026-08-16 22:20:15 +08:00
parent b57933e983
commit 9d15d8f784
14 changed files with 229 additions and 35 deletions
+94 -17
View File
@@ -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<ImageMaterial[]>([]);
const [uploading, setUploading] = useState(false);
const fileRef = useRef<HTMLInputElement | null>(null);
const uploadSeqRef = useRef(0);
// 服务端
const [settings, setSettings] = useState<BackendSettings>({ baseUrl: 'http://127.0.0.1:3300', token: '' });
@@ -131,6 +137,8 @@ const App: React.FC = () => {
const [model, setModel] = useState<string>('wan2.7-image-pro');
/** 用户改写的风格提示词(按风格 id 存,切风格不丢) */
const [stylePrompts, setStylePrompts] = useState<Record<number, string>>({});
/** 生图要求(最高优先级,强制约束,覆盖其他设定) */
const [requirements, setRequirements] = useState('');
const [plan, setPlan] = useState<PlanItem[]>(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<HTMLInputElement>) => {
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={
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
{(result || uploadedImages.length > 0) && (
<span> {selectedKeys.size} / {allImages.filter(i => i.type !== 'video').length}</span>
)}
<button className="btn btn-sm" onClick={handleUpload} disabled={uploading}>
<UploadOutlined /> {uploading ? '上传中…' : '上传图片'}
</button>
</span>
}
>
{!result ? (
<div className="empty"></div>
<input
ref={fileRef}
type="file"
accept="image/*"
multiple
style={{ display: 'none' }}
onChange={onFilesChange}
/>
{allImages.length === 0 ? (
<div className="empty"></div>
) : (
<div className="img-groups">
{['main', 'sku', 'detail'].map(g => groupImages(g).length > 0 && (
{(['main', 'sku', 'detail', 'upload'] as const).map(g => groupImages(g).length > 0 && (
<div key={g} style={{ marginBottom: 10 }}>
<div className="group-head">
<span className="name">
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : '详情图'}
{g === 'main' ? '主图' : g === 'sku' ? 'SKU图片' : g === 'detail' ? '详情图' : '手动上传'}
</span>
<span className="count">{groupImages(g).length}</span>
<span
@@ -647,6 +712,7 @@ const App: React.FC = () => {
<span className="plan-title">{p.title}</span>
{p.variant_name && <span className="variant-chip">{p.variant_name}</span>}
{p.detail && <span className="plan-detail" title={p.detail}>{p.detail}</span>}
{p.prompt_hint && <span className="plan-detail plan-hint" title={p.prompt_hint}>🎯 {p.prompt_hint}</span>}
</div>
<Stepper value={p.count} onChange={(v) => setPlanCount(idx, v)} />
</div>
@@ -698,6 +764,15 @@ const App: React.FC = () => {
onChange={(e) => setStylePrompts(p => ({ ...p, [styleSet]: e.target.value }))}
/>
</div>
<div className="field">
<label></label>
<textarea
rows={3}
value={requirements}
onChange={(e) => setRequirements(e.target.value)}
placeholder="选填,例如:必须保留商品正面品牌标识;背景必须为纯黑色;不得添加任何文字水印"
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 }}>
{generating && (
<div style={{ flex: 1 }}>
@@ -773,7 +848,9 @@ const App: React.FC = () => {
<div style={{ aspectRatio: suite.ratio === '3:4' ? '3/4' : '1', background: 'var(--card-soft)' }} />
)}
{img.status !== 'ok' && <span className="fail-tag">{img.status === 'failed' ? '✗' : '…'}</span>}
<div className="cap">{img.name}</div>
<div className="cap" title={img.error || img.name}>
{img.status === 'failed' && img.error ? img.error : img.name}
</div>
</div>
))}
</div>
@@ -275,6 +275,8 @@
font-size: 12px; color: var(--text-2);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* 构图提示(prompt_hint):生图要求的落地处,用主题蓝区分 */
.plan-detail.plan-hint { color: #4f6bed; }
.stepper { display: inline-flex; align-items: center; gap: 0; flex-shrink: 0; }
.step-btn {
width: 24px; height: 24px; border: 1px solid var(--border-strong); background: #fff;
+26 -6
View File
@@ -2,7 +2,7 @@
* 后端 HTTP 客户端 —— 仅 background 使用(有 host_permissions,不受 CORS 约束)。
* 契约对齐 server 端 /api/materials 与 /api/suites。
*/
import type { ScanResult } from '../collector/scan';
import type { ScanResult, ImageMaterial } from '../collector/scan';
export interface MaterialsPayload {
product_id: string | null;
@@ -218,22 +218,23 @@ export interface GeneratePayload {
images: Array<{ url: string; group_key: string; variant_name?: string | null }>;
style_set: number;
style_prompt?: string;
requirements?: string | null;
plan: PlanItem[];
platform: string;
model?: string | null;
}
/** 组装无状态生成请求:编辑后的文本 + 已勾选图片 + 出图方案 */
/** 组装无状态生成请求:编辑后的文本 + 已勾选图片(含手动上传)+ 出图方案 */
export function buildGeneratePayload(
result: ScanResult,
images: ImageMaterial[],
selectedKeys: Set<string>,
texts: GeneratePayload['texts'],
config: { style_set: number; style_prompt?: string; plan: PlanItem[]; platform: string; model?: string | null },
config: { style_set: number; style_prompt?: string; requirements?: string | null; plan: PlanItem[]; platform: string; model?: string | null },
): GeneratePayload {
const images = result.images
const selected = images
.filter((img) => selectedKeys.has(img.key))
.map((img) => ({ url: img.url, group_key: img.groupKey, variant_name: img.variantName ?? null }));
return { texts, images, ...config };
return { texts, images: selected, ...config };
}
/** 出图方案规划请求体 */
@@ -242,6 +243,7 @@ export interface PlanPayload {
sku_variants: string[];
image_stats: Record<string, number>;
platform: string;
requirements?: string | null;
}
/** AI 智能规划:DeepSeek 根据商品信息生成出图方案 */
@@ -289,3 +291,21 @@ export async function getSuite(baseUrl: string, token: string, suiteId: string):
export function suiteZipUrl(baseUrl: string, suiteId: string): string {
return `${baseUrl.replace(/\/$/, '')}/api/suites/${suiteId}/zip`;
}
/** 手动上传本地图片到服务端,返回可访问 URL(补充参考图用) */
export async function uploadImage(
baseUrl: string,
token: string,
file: File,
): Promise<{ url: string; key: string }> {
const form = new FormData();
form.append('file', file);
const res = await fetch(`${baseUrl.replace(/\/$/, '')}/api/upload-image`, {
method: 'POST',
headers: authHeaders(token), // 不显式设 Content-Type,交给浏览器生成 boundary
body: form,
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.detail || `上传失败 HTTP ${res.status}`);
return data;
}
+1 -1
View File
@@ -17,7 +17,7 @@ export type TextKind =
| 'sales'
| 'shop';
export type ImageGroupKey = 'main' | 'sku' | 'detail' | 'video';
export type ImageGroupKey = 'main' | 'sku' | 'detail' | 'video' | 'upload';
export type SrcProp =
| 'data-lazyload-src'