From ad4bca31b134454877b1285834188dee2ec4a44a Mon Sep 17 00:00:00 2001 From: Stringadmin Date: Fri, 12 Jun 2026 17:25:30 +0800 Subject: [PATCH] fix: address project review bugs --- .gitignore | 1 + src/api/adVideoPlanClient.ts | 165 ++++++++++-------- src/api/aiGenerationClient.ts | 27 ++- src/api/serverConnection.ts | 4 + src/api/webGenerationGateway.ts | 1 - src/features/ecommerce/EcommercePage.tsx | 12 +- .../ecommerce/EcommerceVideoWorkspace.tsx | 56 +++++- .../ecommerce/ecommerceVideoService.ts | 21 ++- src/styles/ecommerce-standalone.css | 18 +- src/utils/errorReporting.ts | 6 +- vite.config.ts | 22 ++- 11 files changed, 209 insertions(+), 124 deletions(-) diff --git a/.gitignore b/.gitignore index ddc64a0..65b2764 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ tmp/ *.swp *.swo coverage/ +屏幕截图 *.png diff --git a/src/api/adVideoPlanClient.ts b/src/api/adVideoPlanClient.ts index 1b245fc..3c5a608 100644 --- a/src/api/adVideoPlanClient.ts +++ b/src/api/adVideoPlanClient.ts @@ -1,8 +1,5 @@ import { buildApiUrl, buildAuthHeaders } from "./serverConnection"; -const TEXT_MODELS = ["qwen-max", "qwen-plus", "qwen-turbo"]; -const VISION_MODELS = ["qwen3.7-plus", "qwen-vl-plus", "qwen-vl-max"]; - type AbortSignalConstructorWithAny = typeof AbortSignal & { any?: (signals: AbortSignal[]) => AbortSignal; }; @@ -110,11 +107,45 @@ export interface ComplianceCheck { allow_video_generation: boolean; } +function findJsonSlice(raw: string): string { + const start = raw.search(/[\[{]/); + if (start < 0) return raw; + + const stack: string[] = []; + let inString = false; + let escaped = false; + + for (let index = start; index < raw.length; index += 1) { + const char = raw[index]; + + if (inString) { + if (escaped) { + escaped = false; + } else if (char === "\\") { + escaped = true; + } else if (char === "\"") { + inString = false; + } + continue; + } + + if (char === "\"") { + inString = true; + } else if (char === "{" || char === "[") { + stack.push(char === "{" ? "}" : "]"); + } else if (char === "}" || char === "]") { + if (stack.pop() !== char) break; + if (stack.length === 0) return raw.slice(start, index + 1); + } + } + + return raw.slice(start); +} + function extractJson(text: string): unknown { const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/); const raw = fenced ? fenced[1].trim() : text.trim(); - const start = raw.search(/[[{]/); - const slice = start >= 0 ? raw.slice(start) : raw; + const slice = findJsonSlice(raw); try { return JSON.parse(slice); } catch { @@ -122,9 +153,16 @@ function extractJson(text: string): unknown { } } +type ChatContent = + | string + | Array< + | { type: "image_url"; image_url: { url: string } } + | { type: "text"; text: string } + >; + interface ChatMessage { role: "system" | "user"; - content: string; + content: ChatContent; } const MAX_RETRIES = 3; @@ -171,43 +209,32 @@ async function chat( userContent: string, options?: { model?: string; signal?: AbortSignal }, ): Promise { - const candidateModels = options?.model ? [options.model] : TEXT_MODELS; - let lastError: Error | null = null; + return retryOnTransient(async () => { + const messages: ChatMessage[] = [ + { role: "system", content: systemPrompt }, + { role: "user", content: userContent }, + ]; + const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); + const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal); + const body: Record = { messages, stream: false, temperature: 0.4 }; + if (options?.model) body.model = options.model; - for (const model of candidateModels) { - try { - return await retryOnTransient(async () => { - const messages: ChatMessage[] = [ - { role: "system", content: systemPrompt }, - { role: "user", content: userContent }, - ]; - const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); - const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal); - const res = await fetch(buildApiUrl("ai/chat"), { - method: "POST", - headers: buildAuthHeaders(), - body: JSON.stringify({ model, messages, stream: false, temperature: 0.4 }), - signal: combinedSignal, - }); - if (!res.ok) { - const errBody = await res.text().catch(() => ""); - throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); - } - const payload = await res.json(); - const content: string = - payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; - if (!content) throw new Error("模型未返回有效内容"); - return content; - }, options?.signal); - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - if (options?.signal?.aborted) throw lastError; - // If user pinned a specific model, don't fall back to others - if (options?.model) throw lastError; - // Try next model in fallback chain + const res = await fetch(buildApiUrl("ai/chat"), { + method: "POST", + headers: buildAuthHeaders(), + body: JSON.stringify(body), + signal: combinedSignal, + }); + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + throw new Error(`AI 调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); } - } - throw lastError ?? new Error("所有候选模型均不可用"); + const payload = await res.json(); + const content: string = + payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; + if (!content) throw new Error("模型未返回有效内容"); + return content; + }, options?.signal); } async function visionChat( @@ -216,50 +243,36 @@ async function visionChat( imageUrls: string[], signal?: AbortSignal, ): Promise { - const content = [ - ...imageUrls.map((url) => ({ type: "image_url", image_url: { url } })), + const content: ChatContent = [ + ...imageUrls.map((url) => ({ type: "image_url" as const, image_url: { url } })), { type: "text", text }, ]; const messages = [ { role: "system", content: systemPrompt }, { role: "user", content }, - ]; + ] satisfies ChatMessage[]; - let lastError: Error | null = null; - for (const model of VISION_MODELS) { + return retryOnTransient(async () => { const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS); const combinedSignal = combineAbortSignals(signal, timeoutSignal); - try { - const out = await retryOnTransient(async () => { - const res = await fetch(buildApiUrl("ai/chat"), { - method: "POST", - headers: buildAuthHeaders(), - body: JSON.stringify({ model, messages, stream: false, temperature: 0.3 }), - signal: combinedSignal, - }); - if (!res.ok) { - const errBody = await res.text().catch(() => ""); - if (errBody.includes("image format")) throw new Error("IMAGE_FORMAT_FALLBACK"); - throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); - } - const payload = await res.json(); - const result: string = - payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; - if (!result) throw new Error("图片理解未返回有效内容"); - return result; - }, signal); - return out; - } catch (err) { - lastError = err instanceof Error ? err : new Error(String(err)); - if (signal?.aborted) throw lastError; - // Continue trying next vision model on transient failures, image format errors, or upstream errors - if (lastError.message === "IMAGE_FORMAT_FALLBACK") continue; - if (lastError.message.includes("图片理解调用失败")) continue; - if (isTransientError(lastError)) continue; - throw lastError; + + const res = await fetch(buildApiUrl("ai/chat"), { + method: "POST", + headers: buildAuthHeaders(), + body: JSON.stringify({ messages, stream: false, temperature: 0.3 }), + signal: combinedSignal, + }); + if (!res.ok) { + const errBody = await res.text().catch(() => ""); + if (errBody.includes("image format")) throw new Error("图片格式不受支持,请更换图片后重试"); + throw new Error(`图片理解调用失败 (${res.status})${errBody ? `: ${errBody.slice(0, 120)}` : ""}`); } - } - throw lastError ?? new Error("图片理解调用失败,所有模型均不可用"); + const payload = await res.json(); + const result: string = + payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? ""; + if (!result) throw new Error("图片理解未返回有效内容"); + return result; + }, signal); } const IMAGE_UNDERSTANDING_PROMPT = `你是电商产品图片分析专家。请分析用户提供的产品图片,识别产品主体、外观、颜色、材质、形状、尺寸感、品牌标识、关键部件、可视化卖点和适合展示的镜头角度。请用简洁的中文段落描述,不要编造图片中看不到的信息。`; diff --git a/src/api/aiGenerationClient.ts b/src/api/aiGenerationClient.ts index ec74c1a..db00e1f 100644 --- a/src/api/aiGenerationClient.ts +++ b/src/api/aiGenerationClient.ts @@ -12,7 +12,7 @@ import type { WebGenerationPreviewTask } from "../types"; export interface ImageGenInput { projectId?: string; conversationId?: number; - model: string; + model?: string; prompt: string; ratio?: string; quality?: string; @@ -210,18 +210,18 @@ function getStoredSessionRole(): string { } function emitImageRouteDebug(label: string, payload: Record): void { - // Only emit console logs for admin users — hides enterprise routing details - if (getStoredSessionRole() === "admin") { - const entry: ImageRouteDebugEntry = { - at: new Date().toISOString(), - label, - ...payload, - }; - try { - console.log(`${label} ${JSON.stringify(entry)}`); - } catch { - console.log(label, entry); - } + // Only emit route debug for admin users; provider routing is operational data. + if (getStoredSessionRole() !== "admin") return; + + const entry: ImageRouteDebugEntry = { + at: new Date().toISOString(), + label, + ...payload, + }; + try { + console.log(`${label} ${JSON.stringify(entry)}`); + } catch { + console.log(label, entry); } if (typeof window === "undefined") return; @@ -229,7 +229,6 @@ function emitImageRouteDebug(label: string, payload: Record): v const previousEntries = Array.isArray(debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__) ? debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ : []; - const entry: ImageRouteDebugEntry = { at: new Date().toISOString(), label, ...payload }; debugWindow.__OMNIAI_IMAGE_ROUTE_DEBUG__ = [...previousEntries.slice(-19), entry]; } diff --git a/src/api/serverConnection.ts b/src/api/serverConnection.ts index 4327da1..2316f72 100644 --- a/src/api/serverConnection.ts +++ b/src/api/serverConnection.ts @@ -161,7 +161,11 @@ export function clearAllUserStorage(): void { "omniai-web-profile-ui", "omniai:more-recent-tools", "omniai:generation-queue", + "omniai:generation-records.pending", + "omniai:ecommerce-video-workspace", "omniai-canvas-saved-assets", + "omniai.clone-ai.", + "omniai.ecommerce.", ]; for (let i = window.localStorage.length - 1; i >= 0; i--) { const key = window.localStorage.key(i); diff --git a/src/api/webGenerationGateway.ts b/src/api/webGenerationGateway.ts index 9a59609..53aed5b 100644 --- a/src/api/webGenerationGateway.ts +++ b/src/api/webGenerationGateway.ts @@ -50,7 +50,6 @@ export const webGenerationGateway = { const result = await aiGenerationClient.createImageTask({ projectId: params?.projectId, conversationId: params?.conversationId, - model: "gpt-image-2", prompt, ratio: params?.ratio || "16:9", quality: params?.quality || "1K", diff --git a/src/features/ecommerce/EcommercePage.tsx b/src/features/ecommerce/EcommercePage.tsx index e5e99b3..d1a549a 100644 --- a/src/features/ecommerce/EcommercePage.tsx +++ b/src/features/ecommerce/EcommercePage.tsx @@ -3453,8 +3453,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { return urls; }; - const IMAGE_MODEL = "gpt-image-2"; - const setCountLabels: Record = { selling: { label: "卖点图", promptDesc: "selling-point infographic image highlighting core product advantages and detail close-ups" }, white: { label: "白底图", promptDesc: "clean white-background product photo showing the item from its best angle, studio lighting, no props" }, @@ -3569,7 +3567,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const fullPrompt = userText.trim() ? `${subPrompt} Additional user requirements: ${userText.trim()}` : subPrompt; const { taskId } = await aiGenerationClient.createImageTask({ - model: IMAGE_MODEL, prompt: fullPrompt, ratio: normalizeRatioForApi(pRatio), quality: pRatio.includes("720") ? "720P" : "1080P", @@ -3653,7 +3650,6 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { const stamp = Date.now(); const { taskId } = await aiGenerationClient.createImageTask({ - model: IMAGE_MODEL, prompt, ratio: normalizeRatioForApi(pRatio), quality: pRatio.includes("720") ? "720P" : "1080P", @@ -6564,6 +6560,14 @@ function ProductClonePage(_props: ProductClonePageProps = {}) { + {isCloneTool && !isCommandHistoryCollapsed ? ( +
setIsCommandHistoryCollapsed(true)} + /> + ) : null} +