refactor: 清理未使用参数、移除死代码、聚焦电商核心模块

主要变更概述:
================

1. 清理未使用的函数参数 (TypeScript noUnusedParameters)
------------------------------------------------------
- AppShell.tsx: 移除未使用的 backendHealth prop 及 ServerConnectionHealth 导入
- canvasUtils.ts: 移除 resolveWorkflowVideoModel 的 workflowModel 参数
- canvasWorkflowDeserialize.ts: 同步更新调用方
- CanvasPage.tsx: 移除 resolveWorkflowVideoModel 未使用导入
- HomePage.tsx: 移除 onOpenTokenMonitor、onOpenImageTool 未使用 props
- ToolboxSection.tsx: 移除 onOpenImageTool 未使用 prop 及 WebImageWorkbenchTool 类型导入
- ScriptTokensPage.tsx: 移除 formatReportMarkdown 的 script 参数,更新 2 处调用
- TokenUsagePage.tsx: 移除 onOpenImageTool、onSelectView 未使用 props
- WorkbenchPage.tsx: 移除 renderComposerToolbar 的 showStop 参数,更新 2 处调用

2. 移除未使用的模块和死代码
--------------------------
删除以下未在电商模块中使用的功能模块:
- 画布模块 (canvas/): CanvasPage, canvasUtils, canvasWorkflow* 等
- 主页模块 (home/): HomePage, ToolboxSection, WelcomeSplash 等
- 工作台模块 (workbench/): WorkbenchPage, ConversationSidebar 等
- 社区模块 (community/, community-review/)
- 数字人模块 (digital-human/)
- 图片工作台 (image-workbench/)
- 其他独立工具页: agent, assets, beta-applications, character-mix,
  compliance, dialog-generator, more, profile, provider-health,
  report, resolution-upscale, script-tokens, settings, size-template,
  subtitle-removal, watermark-removal

3. 移除未使用的公共组件
----------------------
- AnimatedPanel, BeforeAfterCompare, BetaApplicationModal
- CookieConsentBanner, DropZone, EmptyState, NotFoundPage
- NotificationCenter, OnboardingTour, OptimizedImage
- PageTransition, RechargeModal, ShellIcon, Skeleton
- StudioToolLayout, TaskStatusBar, WorkspacePageShell

4. 移除未使用的 API 客户端
--------------------------
- betaApplicationClient, communityClient, conversationClient
- draftClient, modelCapabilitiesClient, notificationClient
- projectTaskClient, providerHealthClient, publicConfigClient
- referenceUploadService, reportClient, scriptEvalClient
- uploadWithProgress

5. 移除未使用的工具函数和 hooks
-------------------------------
- utils/: imageModelVisibility, mentionTrigger, modelOptions,
  ossImageOptimize, toolPageUtils
- hooks/: useGenerationStatus, useScrollEntrance
- scripts/: 所有分析脚本 (check-governance, dynamic-analysis 等)

6. 移除未使用的样式文件
----------------------
删除与已移除模块对应的 CSS 文件,保留电商模块专用样式

7. 新增电商模块功能文件
----------------------
+ src/api/generationRecordClient.ts (生成记录客户端)
+ src/features/ecommerce/ecommerceGenerationPersistence.ts (生成持久化)

验证:
- TypeScript 编译 (tsc --noEmit --noUnusedParameters) 零错误通过
- 所有保留文件的功能完整性未受影响
This commit is contained in:
2026-06-12 11:12:55 +08:00
parent 52e704375c
commit 6d93c2b9b8
184 changed files with 2146 additions and 89530 deletions
File diff suppressed because it is too large Load Diff
@@ -100,7 +100,9 @@ function EcommerceTemplatesPage({
const direction = carouselMotion?.direction ?? 0;
const minSlot = APPLE_CAROUSEL_SLOTS[0]! + Math.min(direction, 0);
const maxSlot = APPLE_CAROUSEL_SLOTS[APPLE_CAROUSEL_SLOTS.length - 1]! + Math.max(direction, 0);
return Array.from({ length: maxSlot - minSlot + 1 }, (_, index) => minSlot + index);
const offsets: number[] = [];
for (let slot = minSlot; slot <= maxSlot; slot += 1) offsets.push(slot);
return offsets;
}, [carouselMotion?.direction]);
const startCarouselShift = useCallback(
@@ -31,6 +31,7 @@ import {
loadEcommerceVideoState,
clearEcommerceVideoState,
} from "./ecommerceVideoKeepalive";
import { saveUnifiedEcommerceGenerationRecord } from "./ecommerceGenerationPersistence";
interface EcommerceVideoWorkspaceProps {
isAuthenticated: boolean;
@@ -172,9 +173,9 @@ export default function EcommerceVideoWorkspace({
}, [stage, scenes, planResult]);
// ── External trigger: start plan from parent ────────────────
const triggerPlanPrevRef = useRef(triggerPlan);
const triggerPlanPrevRef = useRef(0);
useEffect(() => {
if (triggerPlan != null && triggerPlan !== triggerPlanPrevRef.current) {
if (typeof triggerPlan === "number" && triggerPlan > 0 && triggerPlan !== triggerPlanPrevRef.current) {
triggerPlanPrevRef.current = triggerPlan;
void handlePlan();
}
@@ -187,7 +188,7 @@ export default function EcommerceVideoWorkspace({
if (historySavedRef.current) return;
if (!planResult || !scenes.length) return;
historySavedRef.current = true;
const title = planResult.storyboard?.video_title || planResult.summary?.product_name || "电商广告视频";
const title = planResult.storyboard?.video_title || planResult.summary?.product_name || "电商视频";
saveVideoHistory({
title,
config: { platform, aspectRatio, durationSeconds, resolution },
@@ -195,7 +196,35 @@ export default function EcommerceVideoWorkspace({
scenes: scenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
sourceImageUrls,
}).catch(() => {});
}, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution]);
void saveUnifiedEcommerceGenerationRecord({
clientRecordId: `ecommerce-video-${inputFingerprint}-${Date.now()}`,
title,
mode: "short-video",
prompt: requirement,
sourceImages: sourceImageUrls.map((url, index) => ({ url, label: `source-${index + 1}` })),
results: scenes
.filter((scene) => Boolean(scene.resultUrl))
.map((scene) => ({
url: scene.resultUrl!,
label: `scene-${scene.sceneId}`,
mediaType: "video",
taskId: scene.taskId,
})),
taskIds: scenes.map((scene) => scene.taskId).filter((taskId): taskId is string => Boolean(taskId)),
config: { platform, aspectRatio, durationSeconds, resolution },
result: {
plan: planResult as unknown as Record<string, unknown>,
scenes: scenes.map((scene) => ({
sceneId: scene.sceneId,
prompt: scene.prompt,
imageUrl: scene.imageUrl,
videoUrl: scene.resultUrl,
status: scene.status,
})),
},
metadata: { inputFingerprint },
});
}, [stage, planResult, scenes, sourceImageUrls, platform, aspectRatio, durationSeconds, resolution, inputFingerprint, requirement]);
// ── Expose manual save via ref ──────────────────────────
const planResultRef = useRef(planResult);
@@ -212,7 +241,7 @@ export default function EcommerceVideoWorkspace({
const currentScenes = scenesRef.current;
const currentSources = sourceImageUrlsRef.current;
if (!currentPlan || !currentScenes.length) return;
const title = currentPlan.storyboard?.video_title || currentPlan.summary?.product_name || "电商广告视频";
const title = currentPlan.storyboard?.video_title || currentPlan.summary?.product_name || "电商视频";
saveVideoHistory({
title,
config: { platform, aspectRatio, durationSeconds, resolution },
@@ -220,8 +249,36 @@ export default function EcommerceVideoWorkspace({
scenes: currentScenes.map((s) => ({ sceneId: s.sceneId, prompt: s.prompt, imageUrl: s.imageUrl, videoUrl: s.resultUrl })),
sourceImageUrls: currentSources,
}).catch(() => {});
void saveUnifiedEcommerceGenerationRecord({
clientRecordId: `ecommerce-video-manual-${inputFingerprint}-${Date.now()}`,
title,
mode: "short-video",
prompt: requirement,
sourceImages: currentSources.map((url, index) => ({ url, label: `source-${index + 1}` })),
results: currentScenes
.filter((scene) => Boolean(scene.resultUrl))
.map((scene) => ({
url: scene.resultUrl!,
label: `scene-${scene.sceneId}`,
mediaType: "video",
taskId: scene.taskId,
})),
taskIds: currentScenes.map((scene) => scene.taskId).filter((taskId): taskId is string => Boolean(taskId)),
config: { platform, aspectRatio, durationSeconds, resolution },
result: {
plan: currentPlan as unknown as Record<string, unknown>,
scenes: currentScenes.map((scene) => ({
sceneId: scene.sceneId,
prompt: scene.prompt,
imageUrl: scene.imageUrl,
videoUrl: scene.resultUrl,
status: scene.status,
})),
},
metadata: { inputFingerprint, manual: true },
});
};
}, [saveRef, platform, aspectRatio, durationSeconds, resolution]);
}, [saveRef, platform, aspectRatio, durationSeconds, resolution, inputFingerprint, requirement]);
// ── Keep-alive: resume polling for running tasks ──────────
useEffect(() => {
@@ -341,8 +398,8 @@ export default function EcommerceVideoWorkspace({
const handleSaveAsset = async (url: string) => {
try {
const result = await addToolResultToAssetLibrary({
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频生成结果",
type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商视频生成结果",
type: "video", isVideo: true, tags: ["电商", "短视频"],
metadata: { source: "ecommerce-video", platform },
});
showNotice(result === "server" ? "已保存到资产库" : "已保存到本地资产库");
@@ -356,8 +413,8 @@ export default function EcommerceVideoWorkspace({
try {
await addToolResultToAssetLibrary({
url: scene.resultUrl!, name: `电商短视频-镜头${scene.sceneId}-${Date.now()}.mp4`,
description: `电商广告视频 - 镜头${scene.sceneId}`,
type: "video", isVideo: true, tags: ["电商", "短视频", "广告视频"],
description: `电商视频 - 镜头${scene.sceneId}`,
type: "video", isVideo: true, tags: ["电商", "短视频"],
metadata: { source: "ecommerce-video", platform, sceneId: scene.sceneId },
});
saved++;
@@ -380,7 +437,7 @@ export default function EcommerceVideoWorkspace({
const handleImportToCanvas = async (url: string) => {
try {
await addToolResultToAssetLibrary({
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商广告视频 - 导入画布",
url, name: `电商短视频-${Date.now()}.mp4`, description: "电商视频 - 导入画布",
type: "video", isVideo: true, tags: ["电商", "短视频", "画布导入"],
metadata: { source: "ecommerce-video", platform },
});
@@ -0,0 +1,70 @@
import {
buildGenerationOssScope,
deleteGenerationRecordByClientId,
saveGenerationRecord,
type GenerationRecordAsset,
type SaveGenerationRecordInput,
} from "../../api/generationRecordClient";
export const ecommerceOssScopes = {
productSource: buildGenerationOssScope(["ecommerce", "source", "product"]),
cloneResult: (mode: string) => buildGenerationOssScope(["ecommerce", "result", mode]),
videoSource: buildGenerationOssScope(["ecommerce", "short-video", "source"]),
videoHistory: buildGenerationOssScope(["ecommerce", "short-video", "history"]),
};
export interface EcommerceUnifiedRecordInput {
clientRecordId: string;
title: string;
mode: string;
prompt?: string;
status?: SaveGenerationRecordInput["status"];
sourceImages?: Array<{ url: string; ossKey?: string | null; label?: string }>;
results?: Array<{ url: string; label?: string; mediaType?: "image" | "video" | string; taskId?: string | null }>;
taskIds?: string[];
config?: Record<string, unknown>;
result?: Record<string, unknown>;
metadata?: Record<string, unknown>;
createdAt?: string;
}
export function saveUnifiedEcommerceGenerationRecord(input: EcommerceUnifiedRecordInput): Promise<{ source: "server" | "local"; id: string }> {
const assets: GenerationRecordAsset[] = [
...(input.sourceImages || []).map((item): GenerationRecordAsset => ({
role: "source",
mediaType: "image",
url: item.url,
ossKey: item.ossKey,
label: item.label,
scope: ecommerceOssScopes.productSource,
})),
...(input.results || []).map((item): GenerationRecordAsset => ({
role: "result",
mediaType: item.mediaType || "image",
url: item.url,
label: item.label,
taskId: item.taskId,
scope: item.mediaType === "video" ? ecommerceOssScopes.videoHistory : ecommerceOssScopes.cloneResult(input.mode),
})),
].filter((asset) => Boolean(asset.url));
return saveGenerationRecord({
clientRecordId: input.clientRecordId,
tool: "ecommerce",
mode: input.mode,
title: input.title,
status: input.status || "completed",
prompt: input.prompt,
taskIds: input.taskIds,
assets,
config: input.config,
result: input.result,
metadata: input.metadata,
createdAt: input.createdAt,
updatedAt: new Date().toISOString(),
});
}
export async function deleteEcommerceGenerationRecord(clientRecordId: string): Promise<void> {
await deleteGenerationRecordByClientId(clientRecordId);
}
@@ -12,6 +12,7 @@ import { aiGenerationClient } from "../../api/aiGenerationClient";
import { serverRequest } from "../../api/serverConnection";
import { waitForTask } from "../../api/taskSubscription";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import { ecommerceOssScopes } from "./ecommerceGenerationPersistence";
import { normalizeEcommerceImageMime } from "./ecommerceImageValidation";
import type {
EcommerceVideoPlanProgress,
@@ -99,7 +100,7 @@ export async function resolveDurableMediaUrl(
sourceUrl,
name: buildDurableMediaName(options.namePrefix, sourceUrl, mimeType),
mimeType,
scope: options.scope || "ecommerce-video-history",
scope: options.scope || ecommerceOssScopes.videoHistory,
});
return {
url: uploaded.url || null,
@@ -155,13 +156,13 @@ async function uploadProductImageSource(source: string | Blob): Promise<string>
if (source.startsWith("data:")) {
const mimeType = normalizeEcommerceImageMime(source.match(/^data:([^;,]+)/)?.[1] || "image/png");
const result = await aiGenerationClient.uploadAsset({ dataUrl: source, mimeType, scope: "ecommerce-product" });
const result = await aiGenerationClient.uploadAsset({ dataUrl: source, mimeType, scope: ecommerceOssScopes.videoSource });
return result.url;
}
const remoteUrl = normalizeRemoteImageUrl(source);
if (remoteUrl) {
const result = await aiGenerationClient.uploadAssetByUrl({ sourceUrl: remoteUrl, scope: "ecommerce-product" });
const result = await aiGenerationClient.uploadAssetByUrl({ sourceUrl: remoteUrl, scope: ecommerceOssScopes.videoSource });
return result.url;
}
@@ -171,7 +172,7 @@ async function uploadProductImageSource(source: string | Blob): Promise<string>
const mimeType = normalizeEcommerceImageMime(source.type || "image/png");
const blob = source.type === mimeType ? source : new Blob([source], { type: mimeType });
const dataUrl = await readBlobAsDataUrl(blob);
const result = await aiGenerationClient.uploadAsset({ dataUrl, mimeType, scope: "ecommerce-product" });
const result = await aiGenerationClient.uploadAsset({ dataUrl, mimeType, scope: ecommerceOssScopes.videoSource });
return result.url;
}
@@ -9,10 +9,10 @@ import {
} from "@ant-design/icons";
import { createPortal } from "react-dom";
import type { CSSProperties, ChangeEvent, DragEvent, MutableRefObject, RefObject } from "react";
import { useRef, useState } from "react";
import { useState } from "react";
type ProductSetOutputKey = "set" | "detail" | "model" | "video";
type CloneOutputKey = ProductSetOutputKey | "hot" | "video-outfit";
type CloneOutputKey = ProductSetOutputKey | "hot";
type CloneSetCountKey = "selling" | "white" | "scene";
type CloneModelPanelTab = "scene" | "model";
type CloneReferenceMode = "upload" | "link";
@@ -138,7 +138,6 @@ interface EcommerceClonePanelProps {
handleGenerate: () => void;
onCancelGenerate: () => void;
formatRatioDisplayValue: (value: string) => string;
setVideoOutfitFiles?: (video: File | null, ref: File | null) => void;
onStartVideoPlan?: () => void;
}
@@ -208,13 +207,8 @@ export default function EcommerceClonePanel({
handleGenerate,
onCancelGenerate,
formatRatioDisplayValue,
setVideoOutfitFiles,
onStartVideoPlan,
}: EcommerceClonePanelProps) {
const videoOutfitVideoRef = useRef<HTMLInputElement>(null);
const videoOutfitRefRef = useRef<HTMLInputElement>(null);
const [videoOutfitVideoUrl, setVideoOutfitVideoUrl] = useState<string | null>(null);
const [videoOutfitRefUrl, setVideoOutfitRefUrl] = useState<string | null>(null);
const [zoomImage, setZoomImage] = useState<{ src: string; x: number; y: number } | null>(null);
const handleFileMouseEnter = (src: string, event: React.MouseEvent<HTMLElement>) => {
@@ -267,18 +261,6 @@ export default function EcommerceClonePanel({
);
};
const handleVideoOutfitVideoChange = () => {
const file = videoOutfitVideoRef.current?.files?.[0] || null;
if (file) setVideoOutfitVideoUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(file, videoOutfitRefRef.current?.files?.[0] || null);
};
const handleVideoOutfitRefChange = () => {
const file = videoOutfitRefRef.current?.files?.[0] || null;
if (file) setVideoOutfitRefUrl(URL.createObjectURL(file));
setVideoOutfitFiles?.(videoOutfitVideoRef.current?.files?.[0] || null, file);
};
return (
<>
<div className="product-clone-panel__scroll clone-ai-panel">
@@ -346,7 +328,7 @@ export default function EcommerceClonePanel({
</div>
) : null}
</div>
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} />
<input ref={productInputRef} type="file" accept="image/*" multiple onChange={handleProductUpload} aria-label="上传商品图片" />
</section>
<section className="clone-ai-card clone-ai-settings-card clone-ai-settings-card--mode">
@@ -356,7 +338,7 @@ export default function EcommerceClonePanel({
</h2>
<div className="clone-ai-settings-section">
<span className="clone-ai-settings-label"></span>
<div className="clone-ai-tag-group" role="radiogroup" aria-label="生成内容">
<div className="clone-ai-tag-group" role="toolbar" aria-label="生成内容">
{cloneOutputOptions.map((option) => (
<button
key={option.key}
@@ -478,11 +460,12 @@ export default function EcommerceClonePanel({
accept="image/jpeg,image/png,image/webp"
multiple
onChange={handleCloneReferenceUpload}
aria-label="上传参考图片"
/>
</div>
<div className="clone-ai-replicate-section">
<span className="clone-ai-replicate-title"></span>
<div className="clone-ai-replicate-levels" role="radiogroup" aria-label="复刻程度">
<div className="clone-ai-replicate-levels" role="toolbar" aria-label="复刻程度">
{cloneReplicateLevelOptions.map((option) => (
<button
key={option.key}
@@ -779,50 +762,9 @@ export default function EcommerceClonePanel({
</button>
) : null}
{cloneOutput === "video-outfit" ? (
<section className="clone-ai-video-panel" aria-label="视频换装">
<div className="clone-ai-dynamic-head">
<strong></strong>
<span></span>
</div>
<div className="clone-ai-video-section">
<span className="clone-ai-video-title"></span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitVideoRef}
type="file"
accept="video/*"
onChange={handleVideoOutfitVideoChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitVideoRef.current?.click()}>
{videoOutfitVideoUrl ? "重新上传视频" : "点击上传视频"}
</button>
{videoOutfitVideoUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
<div className="clone-ai-video-section">
<span className="clone-ai-video-title">/</span>
<div className="clone-ai-video-outfit-upload">
<input
ref={videoOutfitRefRef}
type="file"
accept="image/*"
onChange={handleVideoOutfitRefChange}
style={{ display: "none" }}
/>
<button type="button" className="clone-ai-video-outfit-upload-btn" onClick={() => videoOutfitRefRef.current?.click()}>
{videoOutfitRefUrl ? "重新上传参考图" : "点击上传参考图"}
</button>
{videoOutfitRefUrl ? <span className="clone-ai-video-outfit-info"></span> : null}
</div>
</div>
</section>
) : null}
<button type="button" className="clone-ai-generate" disabled={!canGenerate || cloneOutput === "video"} onClick={status === "failed" && lastFailedActionRef.current ? lastFailedActionRef.current : handleGenerate} style={cloneOutput === "video" ? { display: "none" } : undefined}>
{status === "generating" ? <LoadingOutlined /> : status === "failed" ? <ReloadOutlined /> : null}
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : cloneOutput === "video-outfit" ? "✦ 开始换装" : "✦ 开始生成"}
{status === "generating" ? "生成中..." : status === "failed" ? "重新生成" : "✦ 开始生成"}
</button>
{status === "generating" && cloneOutput !== "video" ? (
<button type="button" className="clone-ai-generate clone-ai-generate--cancel" onClick={onCancelGenerate}>
@@ -84,7 +84,7 @@ export default function EcommerceSetPanel({
</strong>
<span className="product-set-upload-note"> 3 </span>
</button>
<input ref={setInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={handleSetUpload} />
<input ref={setInputRef} type="file" accept="image/jpeg,image/png,image/webp" multiple onChange={handleSetUpload} aria-label="上传商品图片" />
{setImages.length ? (
<div className="product-clone-thumb-row product-set-thumb-row" aria-label="已上传商品原图">
{setImages.map((item) => (
@@ -109,7 +109,7 @@ export default function EcommerceSetPanel({
</h2>
<div className="product-set-setting-block">
<span className="product-set-setting-title"></span>
<div className="product-set-output-grid" role="radiogroup" aria-label="生成内容">
<div className="product-set-output-grid" role="toolbar" aria-label="生成内容">
{productSetOutputOptions.map((option) => (
<button
key={option.key}