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:
@@ -3,6 +3,26 @@ 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;
|
||||
};
|
||||
|
||||
function combineAbortSignals(signal: AbortSignal | undefined, timeoutSignal: AbortSignal): AbortSignal {
|
||||
if (!signal) return timeoutSignal;
|
||||
const abortSignal = AbortSignal as AbortSignalConstructorWithAny;
|
||||
if (typeof abortSignal.any === "function") return abortSignal.any([signal, timeoutSignal]);
|
||||
|
||||
const controller = new AbortController();
|
||||
const abortFrom = (source: AbortSignal) => {
|
||||
if (!controller.signal.aborted) controller.abort(source.reason);
|
||||
};
|
||||
if (signal.aborted) abortFrom(signal);
|
||||
else signal.addEventListener("abort", () => abortFrom(signal), { once: true });
|
||||
if (timeoutSignal.aborted) abortFrom(timeoutSignal);
|
||||
else timeoutSignal.addEventListener("abort", () => abortFrom(timeoutSignal), { once: true });
|
||||
return controller.signal;
|
||||
}
|
||||
|
||||
export interface AdVideoUserConfig {
|
||||
platform: string;
|
||||
aspectRatio: string;
|
||||
@@ -162,9 +182,7 @@ async function chat(
|
||||
{ role: "user", content: userContent },
|
||||
];
|
||||
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
||||
const combinedSignal = options?.signal
|
||||
? AbortSignal.any([options.signal, timeoutSignal])
|
||||
: timeoutSignal;
|
||||
const combinedSignal = combineAbortSignals(options?.signal, timeoutSignal);
|
||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||
method: "POST",
|
||||
headers: buildAuthHeaders(),
|
||||
@@ -210,9 +228,7 @@ async function visionChat(
|
||||
let lastError: Error | null = null;
|
||||
for (const model of VISION_MODELS) {
|
||||
const timeoutSignal = AbortSignal.timeout(CHAT_TIMEOUT_MS);
|
||||
const combinedSignal = signal
|
||||
? AbortSignal.any([signal, timeoutSignal])
|
||||
: timeoutSignal;
|
||||
const combinedSignal = combineAbortSignals(signal, timeoutSignal);
|
||||
try {
|
||||
const out = await retryOnTransient(async () => {
|
||||
const res = await fetch(buildApiUrl("ai/chat"), {
|
||||
|
||||
@@ -64,17 +64,6 @@ export interface VideoGenInput {
|
||||
style?: "speech" | "sing" | "performance" | string;
|
||||
}
|
||||
|
||||
export interface VideoEditInput {
|
||||
projectId?: string;
|
||||
conversationId?: number;
|
||||
videoUrl: string;
|
||||
referenceUrls: string[];
|
||||
prompt?: string;
|
||||
model?: string;
|
||||
ratio?: string;
|
||||
resolution?: string;
|
||||
}
|
||||
|
||||
export interface VideoSuperResolveInput {
|
||||
projectId?: string;
|
||||
conversationId?: number;
|
||||
@@ -304,16 +293,6 @@ export const aiGenerationClient = {
|
||||
});
|
||||
},
|
||||
|
||||
async createVideoEditTask(input: VideoEditInput): Promise<{ taskId: string }> {
|
||||
return serverRequest<{ taskId: string }>("ai/video/edit", {
|
||||
method: "POST",
|
||||
body: { ...input, model: input.model || "happyhorse-1.0-video-edit" },
|
||||
timeoutMs: TASK_SUBMIT_TIMEOUT_MS,
|
||||
maxRetries: NON_RETRYING_REQUEST.maxRetries,
|
||||
fallbackMessage: "Video edit request failed",
|
||||
});
|
||||
},
|
||||
|
||||
async createImageSuperResolveTask(input: ImageSuperResolveInput): Promise<{ taskId: string }> {
|
||||
return serverRequest<{ taskId: string }>("ai/image/super-resolve", {
|
||||
method: "POST",
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
export interface BetaApplicationInput {
|
||||
name: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
wechat: string;
|
||||
industry: string;
|
||||
company: string;
|
||||
city: string;
|
||||
aiTools: string;
|
||||
aiDuration: string;
|
||||
aiTrack: string;
|
||||
aiDirection: string[];
|
||||
weeklyUsage: string;
|
||||
feedbackWilling: string;
|
||||
wantFeature: string[];
|
||||
selfStatement: string;
|
||||
signature: string;
|
||||
applicationDate: string;
|
||||
agreeRules: boolean;
|
||||
}
|
||||
|
||||
export type BetaApplicationStatus = "pending" | "approved" | "rejected";
|
||||
|
||||
export interface BetaApplicationItem extends BetaApplicationInput {
|
||||
id: number;
|
||||
userId: number | null;
|
||||
username: string | null;
|
||||
status: BetaApplicationStatus;
|
||||
inviteCode: string | null;
|
||||
reviewNote: string | null;
|
||||
reviewedBy: number | null;
|
||||
reviewerUsername: string | null;
|
||||
reviewedAt: string | null;
|
||||
ipAddress: string | null;
|
||||
userAgent: string | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BetaApplicationSubmitResult {
|
||||
id: number;
|
||||
status: BetaApplicationStatus;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
function readString(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function readNullableString(value: unknown): string | null {
|
||||
return typeof value === "string" && value ? value : null;
|
||||
}
|
||||
|
||||
function readNumberOrNull(value: unknown): number | null {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
const next = Number(value);
|
||||
return Number.isFinite(next) ? next : null;
|
||||
}
|
||||
|
||||
function readStringArray(value: unknown): string[] {
|
||||
return Array.isArray(value) ? value.filter((item): item is string => typeof item === "string") : [];
|
||||
}
|
||||
|
||||
function normalizeStatus(value: unknown): BetaApplicationStatus {
|
||||
return value === "approved" || value === "rejected" ? value : "pending";
|
||||
}
|
||||
|
||||
function normalizeApplication(raw: unknown): BetaApplicationItem {
|
||||
const item = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
|
||||
return {
|
||||
id: Number(item.id) || 0,
|
||||
userId: readNumberOrNull(item.userId),
|
||||
username: readNullableString(item.username),
|
||||
name: readString(item.name),
|
||||
email: readString(item.email),
|
||||
phone: readString(item.phone),
|
||||
wechat: readString(item.wechat),
|
||||
industry: readString(item.industry),
|
||||
company: readString(item.company),
|
||||
city: readString(item.city),
|
||||
aiTools: readString(item.aiTools),
|
||||
aiDuration: readString(item.aiDuration),
|
||||
aiTrack: readString(item.aiTrack),
|
||||
aiDirection: readStringArray(item.aiDirection),
|
||||
weeklyUsage: readString(item.weeklyUsage),
|
||||
feedbackWilling: readString(item.feedbackWilling),
|
||||
wantFeature: readStringArray(item.wantFeature),
|
||||
selfStatement: readString(item.selfStatement),
|
||||
signature: readString(item.signature),
|
||||
applicationDate: readString(item.applicationDate),
|
||||
agreeRules: item.agreeRules === true,
|
||||
status: normalizeStatus(item.status),
|
||||
inviteCode: readNullableString(item.inviteCode),
|
||||
reviewNote: readNullableString(item.reviewNote),
|
||||
reviewedBy: readNumberOrNull(item.reviewedBy),
|
||||
reviewerUsername: readNullableString(item.reviewerUsername),
|
||||
reviewedAt: readNullableString(item.reviewedAt),
|
||||
ipAddress: readNullableString(item.ipAddress),
|
||||
userAgent: readNullableString(item.userAgent),
|
||||
createdAt: readString(item.createdAt),
|
||||
updatedAt: readString(item.updatedAt),
|
||||
};
|
||||
}
|
||||
|
||||
export const betaApplicationClient = {
|
||||
async submit(input: BetaApplicationInput): Promise<BetaApplicationSubmitResult> {
|
||||
const payload = await serverRequest<{ application: BetaApplicationSubmitResult }>("beta-applications", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "提交内测申请失败",
|
||||
});
|
||||
return payload.application;
|
||||
},
|
||||
|
||||
async listAdminApplications(status?: BetaApplicationStatus | ""): Promise<BetaApplicationItem[]> {
|
||||
const query = status ? `?status=${encodeURIComponent(status)}` : "";
|
||||
const payload = await serverRequest<{ applications?: unknown[] }>(`admin/beta-applications${query}`, {
|
||||
fallbackMessage: "读取内测申请失败",
|
||||
});
|
||||
return Array.isArray(payload.applications) ? payload.applications.map(normalizeApplication) : [];
|
||||
},
|
||||
|
||||
async reviewApplication(
|
||||
id: number,
|
||||
action: "approve" | "reject",
|
||||
reviewNote?: string,
|
||||
): Promise<BetaApplicationItem> {
|
||||
const payload = await serverRequest<{ application: unknown }>(`admin/beta-applications/${id}`, {
|
||||
method: "PATCH",
|
||||
body: { action, reviewNote },
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "审核内测申请失败",
|
||||
});
|
||||
return normalizeApplication(payload.application);
|
||||
},
|
||||
};
|
||||
@@ -1,207 +0,0 @@
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ServerCommunityAsset {
|
||||
id?: number;
|
||||
assetType: "image" | "video" | "project" | "workflow" | "asset" | "cover" | "other";
|
||||
title?: string | null;
|
||||
url?: string | null;
|
||||
ossKey?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface ServerCommunityCase {
|
||||
id: number;
|
||||
userId?: number;
|
||||
username?: string | null;
|
||||
projectId?: string | null;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
coverUrl?: string | null;
|
||||
tags: string[];
|
||||
metadata: Record<string, unknown>;
|
||||
status: "pending" | "approved" | "rejected";
|
||||
reviewNote?: string | null;
|
||||
publishedAt?: string | null;
|
||||
copyCount: number;
|
||||
favoriteCount: number;
|
||||
likeCount: number;
|
||||
isFavorited: boolean;
|
||||
isLiked: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
assets: ServerCommunityAsset[];
|
||||
}
|
||||
|
||||
export interface PublishCommunityCaseInput {
|
||||
projectId?: string | null;
|
||||
title: string;
|
||||
description?: string | null;
|
||||
coverUrl?: string | null;
|
||||
tags?: string[];
|
||||
metadata?: Record<string, unknown>;
|
||||
assets?: Array<{
|
||||
assetType?: ServerCommunityAsset["assetType"];
|
||||
title?: string;
|
||||
url?: string;
|
||||
ossKey?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
sortOrder?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown, fallback = 0): number {
|
||||
const numeric = typeof value === "number" ? value : Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function toStringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value.trim() || fallback;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const result: string[] = [];
|
||||
for (const item of value) {
|
||||
const text = toStringValue(item);
|
||||
if (text) result.push(text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function toMetadata(value: unknown): Record<string, unknown> {
|
||||
return isRecord(value) ? value : {};
|
||||
}
|
||||
|
||||
function normalizeAsset(raw: unknown): ServerCommunityAsset {
|
||||
const asset = isRecord(raw) ? raw : {};
|
||||
const assetType = toStringValue(asset.assetType ?? asset.asset_type ?? asset.type, "other");
|
||||
return {
|
||||
id: Number.isFinite(Number(asset.id)) ? Number(asset.id) : undefined,
|
||||
assetType:
|
||||
assetType === "image" ||
|
||||
assetType === "video" ||
|
||||
assetType === "image" ||
|
||||
assetType === "project" ||
|
||||
assetType === "workflow" ||
|
||||
assetType === "asset" ||
|
||||
assetType === "cover"
|
||||
? assetType
|
||||
: "other",
|
||||
title: toStringValue(asset.title) || null,
|
||||
url: toStringValue(asset.url) || null,
|
||||
ossKey: toStringValue(asset.ossKey ?? asset.oss_key) || null,
|
||||
metadata: toMetadata(asset.metadata),
|
||||
sortOrder: toNumber(asset.sortOrder ?? asset.sort_order),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCase(raw: unknown): ServerCommunityCase {
|
||||
const item = isRecord(raw) ? raw : {};
|
||||
const status = toStringValue(item.status, "pending");
|
||||
return {
|
||||
id: toNumber(item.id),
|
||||
userId: Number.isFinite(Number(item.userId ?? item.user_id)) ? Number(item.userId ?? item.user_id) : undefined,
|
||||
username: toStringValue(item.username) || null,
|
||||
projectId: toStringValue(item.projectId ?? item.project_id) || null,
|
||||
title: toStringValue(item.title, "未命名案例"),
|
||||
description: toStringValue(item.description) || null,
|
||||
coverUrl: toStringValue(item.coverUrl ?? item.cover_url) || null,
|
||||
tags: toStringArray(item.tags),
|
||||
metadata: toMetadata(item.metadata),
|
||||
status: status === "approved" || status === "rejected" ? status : "pending",
|
||||
reviewNote: toStringValue(item.reviewNote ?? item.review_note) || null,
|
||||
publishedAt: toStringValue(item.publishedAt ?? item.published_at) || null,
|
||||
copyCount: toNumber(item.copyCount ?? item.copy_count),
|
||||
favoriteCount: toNumber(item.favoriteCount ?? item.favorite_count),
|
||||
likeCount: toNumber(item.likeCount ?? item.like_count),
|
||||
isFavorited: Boolean(item.isFavorited ?? item.is_favorited),
|
||||
isLiked: Boolean(item.isLiked ?? item.is_liked),
|
||||
createdAt: toStringValue(item.createdAt ?? item.created_at, new Date().toISOString()),
|
||||
updatedAt: toStringValue(item.updatedAt ?? item.updated_at, new Date().toISOString()),
|
||||
assets: Array.isArray(item.assets) ? item.assets.map(normalizeAsset) : [],
|
||||
};
|
||||
}
|
||||
|
||||
function extractCases(payload: unknown): ServerCommunityCase[] {
|
||||
if (Array.isArray(payload)) return payload.map(normalizeCase);
|
||||
if (!isRecord(payload)) return [];
|
||||
const rows = payload.cases ?? payload.items;
|
||||
return Array.isArray(rows) ? rows.map(normalizeCase) : [];
|
||||
}
|
||||
|
||||
export const communityClient = {
|
||||
async listApprovedCases(
|
||||
params: number | { limit?: number; q?: string; category?: string; tag?: string; sort?: string } = 30,
|
||||
): Promise<ServerCommunityCase[]> {
|
||||
const search = new URLSearchParams();
|
||||
if (typeof params === "number") {
|
||||
search.set("limit", String(params));
|
||||
} else {
|
||||
search.set("limit", String(params.limit ?? 30));
|
||||
if (params.q) search.set("q", params.q);
|
||||
if (params.category && params.category !== "全部") search.set("category", params.category);
|
||||
if (params.tag) search.set("tag", params.tag);
|
||||
if (params.sort) search.set("sort", params.sort);
|
||||
}
|
||||
return extractCases(await serverRequest<unknown>(`community/cases?${search.toString()}`));
|
||||
},
|
||||
|
||||
async listMyCases(): Promise<ServerCommunityCase[]> {
|
||||
return extractCases(await serverRequest<unknown>("community/me/cases"));
|
||||
},
|
||||
|
||||
async listCasesForReview(status: "" | ServerCommunityCase["status"] = "pending"): Promise<ServerCommunityCase[]> {
|
||||
const search = new URLSearchParams();
|
||||
if (status) search.set("status", status);
|
||||
const query = search.toString();
|
||||
return extractCases(await serverRequest<unknown>(`admin/community/cases${query ? `?${query}` : ""}`));
|
||||
},
|
||||
|
||||
async publishCase(input: PublishCommunityCaseInput): Promise<ServerCommunityCase> {
|
||||
const payload = await serverRequest<{ case: unknown }>("community/cases", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
return normalizeCase(payload.case);
|
||||
},
|
||||
|
||||
async updateReviewStatus(
|
||||
caseId: number | string,
|
||||
status: Exclude<ServerCommunityCase["status"], "pending">,
|
||||
reviewNote: string,
|
||||
): Promise<ServerCommunityCase> {
|
||||
const payload = await serverRequest<{ case: unknown }>(`admin/community/cases/${encodeURIComponent(String(caseId))}/status`, {
|
||||
method: "PATCH",
|
||||
body: { status, reviewNote, review_note: reviewNote },
|
||||
});
|
||||
return normalizeCase(payload.case);
|
||||
},
|
||||
|
||||
async copyCase(caseId: number, input?: { projectId?: string; name?: string; ossKey?: string }): Promise<void> {
|
||||
await serverRequest(`community/cases/${caseId}/copy`, {
|
||||
method: "POST",
|
||||
body: input || {},
|
||||
});
|
||||
},
|
||||
|
||||
async setReaction(
|
||||
caseId: number,
|
||||
reactionType: "favorite" | "like",
|
||||
active: boolean,
|
||||
): Promise<{ favoriteCount: number; likeCount: number; isFavorited: boolean; isLiked: boolean }> {
|
||||
const payload = await serverRequest<{ stats: unknown }>(`community/cases/${caseId}/reactions`, {
|
||||
method: "POST",
|
||||
body: { reactionType, active },
|
||||
});
|
||||
const stats = isRecord(payload.stats) ? payload.stats : {};
|
||||
return {
|
||||
favoriteCount: toNumber(stats.favoriteCount),
|
||||
likeCount: toNumber(stats.likeCount),
|
||||
isFavorited: Boolean(stats.isFavorited),
|
||||
isLiked: Boolean(stats.isLiked),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,67 +0,0 @@
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ConversationSummary {
|
||||
id: number;
|
||||
title: string;
|
||||
mode: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface ConversationDetail extends ConversationSummary {
|
||||
messages: ConversationMessage[];
|
||||
}
|
||||
|
||||
export interface ConversationMessage {
|
||||
id: string;
|
||||
role: "user" | "assistant";
|
||||
author: string;
|
||||
mode: string;
|
||||
body: string;
|
||||
createdAt: string;
|
||||
status?: string;
|
||||
taskId?: string;
|
||||
conversationId?: number;
|
||||
taskProgress?: number;
|
||||
taskStatusLabel?: string;
|
||||
attachments?: Array<{ kind: string; name: string; token: string; previewUrl?: string; remoteUrl?: string }>;
|
||||
resultUrl?: string;
|
||||
resultType?: string;
|
||||
resultOriginalUrl?: string;
|
||||
resultOssKey?: string;
|
||||
resultMimeType?: string;
|
||||
result?: { title: string; summary: string; specs: string[] };
|
||||
}
|
||||
|
||||
export interface DeleteConversationOptions {
|
||||
cleanupUserData?: boolean;
|
||||
}
|
||||
|
||||
export const conversationClient = {
|
||||
async list(): Promise<ConversationSummary[]> {
|
||||
const data = await serverRequest<{ conversations: ConversationSummary[] }>("conversations");
|
||||
return data.conversations || [];
|
||||
},
|
||||
|
||||
async create(title: string, mode: string, messages?: ConversationMessage[]): Promise<ConversationSummary> {
|
||||
const data = await serverRequest<{ conversation: ConversationSummary }>("conversations", {
|
||||
method: "POST",
|
||||
body: { title, mode, messages },
|
||||
});
|
||||
return data.conversation;
|
||||
},
|
||||
|
||||
async get(id: number): Promise<ConversationDetail> {
|
||||
const data = await serverRequest<{ conversation: ConversationDetail }>(`conversations/${id}`);
|
||||
return data.conversation;
|
||||
},
|
||||
|
||||
async update(id: number, data: { title?: string; messages?: ConversationMessage[] }): Promise<void> {
|
||||
await serverRequest(`conversations/${id}`, { method: "PUT", body: data });
|
||||
},
|
||||
|
||||
async delete(id: number, options?: DeleteConversationOptions): Promise<void> {
|
||||
const path = options?.cleanupUserData ? `conversations/${id}?cleanupUserData=1` : `conversations/${id}`;
|
||||
await serverRequest(path, { method: "DELETE" });
|
||||
},
|
||||
};
|
||||
@@ -1,53 +0,0 @@
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
export interface WebDraft<TPayload = unknown> {
|
||||
id: string;
|
||||
scope: string;
|
||||
targetId: string;
|
||||
payload: TPayload;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
function toStringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value.trim() || fallback;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeDraft(raw: unknown): WebDraft {
|
||||
const item = isRecord(raw) ? raw : {};
|
||||
return {
|
||||
id: toStringValue(item.id, `draft-${Date.now()}`),
|
||||
scope: toStringValue(item.scope),
|
||||
targetId: toStringValue(item.targetId ?? item.target_id, "default"),
|
||||
payload: item.payload,
|
||||
createdAt: toStringValue(item.createdAt ?? item.created_at),
|
||||
updatedAt: toStringValue(item.updatedAt ?? item.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
function extractDrafts(payload: unknown): WebDraft[] {
|
||||
if (Array.isArray(payload)) return payload.map(normalizeDraft);
|
||||
if (!isRecord(payload)) return [];
|
||||
const rows = payload.drafts ?? payload.items;
|
||||
return Array.isArray(rows) ? rows.map(normalizeDraft) : [];
|
||||
}
|
||||
|
||||
export const draftClient = {
|
||||
async list(params?: { scope?: string; targetId?: string }): Promise<WebDraft[]> {
|
||||
const search = new URLSearchParams();
|
||||
if (params?.scope) search.set("scope", params.scope);
|
||||
if (params?.targetId) search.set("targetId", params.targetId);
|
||||
const query = search.toString();
|
||||
return extractDrafts(await serverRequest<unknown>(`drafts${query ? `?${query}` : ""}`));
|
||||
},
|
||||
|
||||
async save<TPayload>(input: { scope: string; targetId?: string; payload: TPayload }): Promise<WebDraft<TPayload>> {
|
||||
const payload = await serverRequest<{ draft: unknown }>("drafts", {
|
||||
method: "PUT",
|
||||
body: input,
|
||||
});
|
||||
return normalizeDraft(payload.draft ?? payload) as WebDraft<TPayload>;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,131 @@
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
const PENDING_RECORDS_KEY = "omniai:generation-records.pending";
|
||||
const MAX_PENDING_RECORDS = 80;
|
||||
|
||||
export type GenerationRecordStatus = "queued" | "running" | "completed" | "failed" | "cancelled";
|
||||
|
||||
export interface GenerationRecordAsset {
|
||||
role: "source" | "reference" | "intermediate" | "result" | "thumbnail";
|
||||
mediaType: "image" | "video" | "text" | "asset" | string;
|
||||
url: string;
|
||||
ossKey?: string | null;
|
||||
scope?: string;
|
||||
label?: string;
|
||||
taskId?: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SaveGenerationRecordInput {
|
||||
clientRecordId: string;
|
||||
tool: string;
|
||||
mode?: string;
|
||||
title: string;
|
||||
status: GenerationRecordStatus;
|
||||
prompt?: string;
|
||||
taskIds?: string[];
|
||||
assets?: GenerationRecordAsset[];
|
||||
config?: Record<string, unknown>;
|
||||
result?: Record<string, unknown>;
|
||||
metadata?: Record<string, unknown>;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface SaveGenerationRecordResult {
|
||||
source: "server" | "local";
|
||||
id: string;
|
||||
}
|
||||
|
||||
function readPendingRecords(): SaveGenerationRecordInput[] {
|
||||
try {
|
||||
const raw = window.localStorage.getItem(PENDING_RECORDS_KEY);
|
||||
if (!raw) return [];
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((item): item is SaveGenerationRecordInput => Boolean(item?.clientRecordId)) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function writePendingRecord(input: SaveGenerationRecordInput): void {
|
||||
try {
|
||||
const records = readPendingRecords();
|
||||
const next = [input, ...records.filter((item) => item.clientRecordId !== input.clientRecordId)].slice(0, MAX_PENDING_RECORDS);
|
||||
window.localStorage.setItem(PENDING_RECORDS_KEY, JSON.stringify(next));
|
||||
} catch {
|
||||
// Ignore storage quota failures; generation itself must not be blocked by history persistence.
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveGenerationRecord(input: SaveGenerationRecordInput): Promise<SaveGenerationRecordResult> {
|
||||
try {
|
||||
const response = await serverRequest<{ id?: string | number }>("ai/generation-records", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "Failed to save generation record",
|
||||
});
|
||||
return { source: "server", id: String(response.id ?? input.clientRecordId) };
|
||||
} catch (error) {
|
||||
if (!isOptionalApiRouteMissing(error)) {
|
||||
// Keep a local recovery copy even when the route exists but the save fails.
|
||||
writePendingRecord(input);
|
||||
return { source: "local", id: input.clientRecordId };
|
||||
}
|
||||
writePendingRecord(input);
|
||||
return { source: "local", id: input.clientRecordId };
|
||||
}
|
||||
}
|
||||
|
||||
export async function flushPendingGenerationRecords(): Promise<{ synced: number; remaining: number }> {
|
||||
const pending = readPendingRecords();
|
||||
if (!pending.length) return { synced: 0, remaining: 0 };
|
||||
|
||||
const remaining: SaveGenerationRecordInput[] = [];
|
||||
let synced = 0;
|
||||
|
||||
for (const record of pending.slice().reverse()) {
|
||||
try {
|
||||
await serverRequest<{ id?: string | number }>("ai/generation-records", {
|
||||
method: "POST",
|
||||
body: record,
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "Failed to sync generation record",
|
||||
});
|
||||
synced += 1;
|
||||
} catch {
|
||||
remaining.unshift(record);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (remaining.length) {
|
||||
window.localStorage.setItem(PENDING_RECORDS_KEY, JSON.stringify(remaining.slice(0, MAX_PENDING_RECORDS)));
|
||||
} else {
|
||||
window.localStorage.removeItem(PENDING_RECORDS_KEY);
|
||||
}
|
||||
} catch {
|
||||
// Keep runtime generation unaffected if browser storage is unavailable.
|
||||
}
|
||||
|
||||
return { synced, remaining: remaining.length };
|
||||
}
|
||||
|
||||
export async function deleteGenerationRecordByClientId(clientRecordId: string): Promise<void> {
|
||||
await serverRequest<{ success: boolean }>(`ai/generation-records/by-client-id/${encodeURIComponent(clientRecordId)}`, {
|
||||
method: "DELETE",
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "Failed to delete generation record",
|
||||
});
|
||||
}
|
||||
|
||||
export function buildGenerationOssScope(parts: Array<string | number | null | undefined>): string {
|
||||
return parts
|
||||
.map((part) => String(part ?? "").trim().toLowerCase())
|
||||
.filter(Boolean)
|
||||
.map((part) => part.replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, ""))
|
||||
.filter(Boolean)
|
||||
.join("/");
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ModelCapabilityOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
badge?: string;
|
||||
enabled?: boolean;
|
||||
status?: "available" | "maintenance" | "disabled" | string;
|
||||
}
|
||||
|
||||
export interface WebModelCapabilities {
|
||||
source: "server" | "fallback";
|
||||
imageModels: ModelCapabilityOption[];
|
||||
videoModels: ModelCapabilityOption[];
|
||||
chatModels: ModelCapabilityOption[];
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
function toStringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value.trim() || fallback;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeModelOption(raw: unknown): ModelCapabilityOption | null {
|
||||
if (typeof raw === "string") {
|
||||
const value = raw.trim();
|
||||
return value ? { value, label: value } : null;
|
||||
}
|
||||
if (!isRecord(raw)) return null;
|
||||
|
||||
const value = toStringValue(raw.value ?? raw.id ?? raw.model ?? raw.modelKey ?? raw.model_key);
|
||||
if (!value) return null;
|
||||
|
||||
const status = toStringValue(raw.status);
|
||||
const enabled = raw.enabled === undefined ? status !== "maintenance" && status !== "disabled" : Boolean(raw.enabled);
|
||||
if (!enabled) return null;
|
||||
|
||||
const label = toStringValue(raw.label ?? raw.displayName ?? raw.display_name ?? raw.name, value);
|
||||
|
||||
return {
|
||||
value,
|
||||
label:
|
||||
value === "wan2.7-image-pro"
|
||||
? label.replace(/\s*4k\b/i, "").trim() || "wan 2.7 Pro"
|
||||
: label,
|
||||
description: toStringValue(raw.description) || undefined,
|
||||
badge: toStringValue(raw.badge) || undefined,
|
||||
enabled,
|
||||
status: status || "available",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeModelList(value: unknown): ModelCapabilityOption[] {
|
||||
if (!Array.isArray(value)) return [];
|
||||
const options: ModelCapabilityOption[] = [];
|
||||
for (const item of value) {
|
||||
const option = normalizeModelOption(item);
|
||||
if (option) options.push(option);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function createFallbackCapabilities(): WebModelCapabilities {
|
||||
return {
|
||||
source: "fallback",
|
||||
imageModels: [],
|
||||
videoModels: [],
|
||||
chatModels: [],
|
||||
};
|
||||
}
|
||||
|
||||
let modelCapabilitiesRouteMissing = false;
|
||||
|
||||
export const modelCapabilitiesClient = {
|
||||
async get(name = "web-model-capabilities"): Promise<WebModelCapabilities> {
|
||||
if (modelCapabilitiesRouteMissing) return createFallbackCapabilities();
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await serverRequest<unknown>(`public/config/profile?name=${encodeURIComponent(name)}`);
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
modelCapabilitiesRouteMissing = true;
|
||||
return createFallbackCapabilities();
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const raw = isRecord(payload) && isRecord(payload.config) ? payload.config : payload;
|
||||
const config = isRecord(raw) ? raw : {};
|
||||
const models = isRecord(config.models) ? config.models : {};
|
||||
|
||||
return {
|
||||
source: "server",
|
||||
imageModels: normalizeModelList(config.imageModels ?? config.image_models ?? models.image),
|
||||
videoModels: normalizeModelList(config.videoModels ?? config.video_models ?? models.video),
|
||||
chatModels: normalizeModelList(config.chatModels ?? config.chat_models ?? models.chat ?? models.agent ?? models.text),
|
||||
updatedAt: toStringValue((payload as { updatedAt?: unknown; updated_at?: unknown })?.updatedAt ?? (payload as { updated_at?: unknown })?.updated_at),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,173 +0,0 @@
|
||||
import type { WebNotification, WebNotificationType, WebViewKey } from "../types";
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
import { isRecord, isServerRequestError, serverRequest, writeStoredSession } from "./serverConnection";
|
||||
|
||||
interface CreateNotificationInput {
|
||||
type: WebNotificationType;
|
||||
title: string;
|
||||
description?: string;
|
||||
targetType?: string;
|
||||
targetId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const NOTIFICATION_VIEW_BY_TARGET: Record<string, WebViewKey> = {
|
||||
task: "workbench",
|
||||
generation_task: "workbench",
|
||||
community_case: "login",
|
||||
asset: "assets",
|
||||
project: "canvas",
|
||||
draft: "workbench",
|
||||
};
|
||||
|
||||
let notificationsRouteMissing = false;
|
||||
let notificationsUnauthorized = false;
|
||||
|
||||
function toStringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value.trim() || fallback;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeType(value: unknown): WebNotificationType {
|
||||
const type = toStringValue(value);
|
||||
if (
|
||||
type === "task_completed" ||
|
||||
type === "task_failed" ||
|
||||
type === "review_pending" ||
|
||||
type === "review_passed" ||
|
||||
type === "review_rejected" ||
|
||||
type === "credits_low" ||
|
||||
type === "session_expired"
|
||||
) {
|
||||
return type;
|
||||
}
|
||||
return "info";
|
||||
}
|
||||
|
||||
function normalizeNotification(raw: unknown): WebNotification {
|
||||
const item = isRecord(raw) ? raw : {};
|
||||
const targetType = toStringValue(item.targetType ?? item.target_type) || null;
|
||||
const targetId = toStringValue(item.targetId ?? item.target_id) || undefined;
|
||||
const readAt = toStringValue(item.readAt ?? item.read_at) || null;
|
||||
return {
|
||||
id: toStringValue(item.id, `notice-${Date.now()}`),
|
||||
type: normalizeType(item.type),
|
||||
title: toStringValue(item.title, "通知"),
|
||||
description: toStringValue(item.description),
|
||||
createdAt: toStringValue(item.createdAt ?? item.created_at, new Date().toISOString()),
|
||||
isRead: Boolean(item.isRead ?? item.is_read ?? readAt),
|
||||
targetType,
|
||||
targetId,
|
||||
targetView: targetType ? NOTIFICATION_VIEW_BY_TARGET[targetType] : undefined,
|
||||
readAt,
|
||||
metadata: isRecord(item.metadata) ? item.metadata : {},
|
||||
};
|
||||
}
|
||||
|
||||
function extractNotifications(payload: unknown): WebNotification[] {
|
||||
if (Array.isArray(payload)) return payload.map(normalizeNotification);
|
||||
if (!isRecord(payload)) return [];
|
||||
const rows = payload.notifications ?? payload.items;
|
||||
return Array.isArray(rows) ? rows.map(normalizeNotification) : [];
|
||||
}
|
||||
|
||||
function isUnauthorized(error: unknown): boolean {
|
||||
return isServerRequestError(error) && (error.status === 401 || error.status === 403);
|
||||
}
|
||||
|
||||
function handleUnauthorizedNotifications(): void {
|
||||
notificationsUnauthorized = true;
|
||||
writeStoredSession(null);
|
||||
}
|
||||
|
||||
export const notificationClient = {
|
||||
async list(): Promise<WebNotification[]> {
|
||||
if (notificationsRouteMissing || notificationsUnauthorized) return [];
|
||||
try {
|
||||
return extractNotifications(await serverRequest<unknown>("notifications"));
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
notificationsRouteMissing = true;
|
||||
return [];
|
||||
}
|
||||
if (isUnauthorized(error)) {
|
||||
handleUnauthorizedNotifications();
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async create(input: CreateNotificationInput): Promise<WebNotification> {
|
||||
if (notificationsRouteMissing || notificationsUnauthorized) {
|
||||
return normalizeNotification({
|
||||
...input,
|
||||
id: `local-notice-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
try {
|
||||
const payload = await serverRequest<{ notification: unknown }>("notifications", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
return normalizeNotification(payload.notification ?? payload);
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
notificationsRouteMissing = true;
|
||||
return normalizeNotification({
|
||||
...input,
|
||||
id: `local-notice-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
if (isUnauthorized(error)) {
|
||||
handleUnauthorizedNotifications();
|
||||
return normalizeNotification({
|
||||
...input,
|
||||
id: `local-notice-${Date.now()}`,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async markRead(id: string, isRead = true): Promise<void> {
|
||||
if (notificationsRouteMissing || notificationsUnauthorized) return;
|
||||
try {
|
||||
await serverRequest(`notifications/${id}/read`, {
|
||||
method: "PATCH",
|
||||
body: { isRead },
|
||||
});
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
notificationsRouteMissing = true;
|
||||
return;
|
||||
}
|
||||
if (isUnauthorized(error)) {
|
||||
handleUnauthorizedNotifications();
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
async markAllRead(): Promise<void> {
|
||||
if (notificationsRouteMissing || notificationsUnauthorized) return;
|
||||
try {
|
||||
await serverRequest("notifications/read-all", { method: "POST" });
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
notificationsRouteMissing = true;
|
||||
return;
|
||||
}
|
||||
if (isUnauthorized(error)) {
|
||||
handleUnauthorizedNotifications();
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,154 +0,0 @@
|
||||
import type { WebGenerationPreviewTask } from "../types";
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
type ServerTaskStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
||||
|
||||
interface ServerProjectTask {
|
||||
id: string;
|
||||
projectId?: string | null;
|
||||
clientQueueId?: string | null;
|
||||
type: "image" | "video";
|
||||
status: ServerTaskStatus;
|
||||
params?: Record<string, unknown>;
|
||||
resultUrl?: string | null;
|
||||
progress?: number;
|
||||
error?: string | null;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ProjectTaskUpsertInput {
|
||||
clientQueueId: string;
|
||||
type: "image" | "video";
|
||||
status: ServerTaskStatus;
|
||||
params?: Record<string, unknown>;
|
||||
providerTaskId?: string | null;
|
||||
resultUrl?: string | null;
|
||||
progress?: number;
|
||||
error?: string | null;
|
||||
dedupeKey?: string | null;
|
||||
sourceDeviceId?: string | null;
|
||||
createdAt?: string | null;
|
||||
completedAt?: string | null;
|
||||
}
|
||||
|
||||
function toStringValue(value: unknown, fallback = ""): string {
|
||||
if (typeof value === "string") return value.trim() || fallback;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toNumber(value: unknown, fallback = 0): number {
|
||||
const numeric = Number(value);
|
||||
return Number.isFinite(numeric) ? numeric : fallback;
|
||||
}
|
||||
|
||||
function normalizeStatus(value: unknown): WebGenerationPreviewTask["status"] {
|
||||
const status = toStringValue(value);
|
||||
if (status === "running" || status === "completed" || status === "failed") return status;
|
||||
if (status === "cancelled") return "failed";
|
||||
return "queued";
|
||||
}
|
||||
|
||||
function normalizeTask(raw: unknown): ServerProjectTask | null {
|
||||
if (!isRecord(raw)) return null;
|
||||
const type = toStringValue(raw.type);
|
||||
if (type !== "image" && type !== "video") return null;
|
||||
|
||||
return {
|
||||
id: toStringValue(raw.id),
|
||||
projectId: toStringValue(raw.projectId ?? raw.project_id) || null,
|
||||
clientQueueId: toStringValue(raw.clientQueueId ?? raw.client_queue_id) || null,
|
||||
type,
|
||||
status: toStringValue(raw.status, "pending") as ServerTaskStatus,
|
||||
params: isRecord(raw.params) ? raw.params : {},
|
||||
resultUrl: toStringValue(raw.resultUrl ?? raw.result_url) || null,
|
||||
progress: toNumber(raw.progress),
|
||||
error: toStringValue(raw.error) || null,
|
||||
createdAt: toStringValue(raw.createdAt ?? raw.created_at),
|
||||
updatedAt: toStringValue(raw.updatedAt ?? raw.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
function extractTasks(payload: unknown): ServerProjectTask[] {
|
||||
const normalizeTasks = (rows: unknown[]): ServerProjectTask[] => {
|
||||
const tasks: ServerProjectTask[] = [];
|
||||
for (const row of rows) {
|
||||
const task = normalizeTask(row);
|
||||
if (task) tasks.push(task);
|
||||
}
|
||||
return tasks;
|
||||
};
|
||||
|
||||
if (Array.isArray(payload)) return normalizeTasks(payload);
|
||||
if (!isRecord(payload)) return [];
|
||||
const rows = payload.tasks ?? payload.items;
|
||||
return Array.isArray(rows) ? normalizeTasks(rows) : [];
|
||||
}
|
||||
|
||||
function taskTitle(task: ServerProjectTask): string {
|
||||
const prompt = toStringValue(task.params?.prompt);
|
||||
if (prompt) return prompt.length > 20 ? `${prompt.slice(0, 20)}...` : prompt;
|
||||
return task.type === "video" ? "视频生成任务" : "图像生成任务";
|
||||
}
|
||||
|
||||
function toPreviewTask(task: ServerProjectTask): WebGenerationPreviewTask {
|
||||
return {
|
||||
id: task.clientQueueId || task.id,
|
||||
title: taskTitle(task),
|
||||
type: task.type,
|
||||
status: normalizeStatus(task.status),
|
||||
progress: Math.max(0, Math.min(100, Math.trunc(task.progress || 0))),
|
||||
prompt: toStringValue(task.params?.prompt, taskTitle(task)),
|
||||
createdAt: task.createdAt || task.updatedAt || "",
|
||||
projectId: task.projectId || undefined,
|
||||
outputUrl: task.resultUrl || undefined,
|
||||
source: "server",
|
||||
errorMessage: task.error || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function listProjectTasks(projectId: string): Promise<WebGenerationPreviewTask[]> {
|
||||
const payload = await serverRequest<unknown>(`projects/${encodeURIComponent(projectId)}/tasks`);
|
||||
return extractTasks(payload).map(toPreviewTask);
|
||||
}
|
||||
|
||||
export const projectTaskClient = {
|
||||
async list(projectId: string): Promise<WebGenerationPreviewTask[]> {
|
||||
return listProjectTasks(projectId);
|
||||
},
|
||||
|
||||
async listForProjects(projectIds: string[]): Promise<WebGenerationPreviewTask[]> {
|
||||
const uniqueIds = new Set<string>();
|
||||
for (const projectId of projectIds) {
|
||||
const id = projectId.trim();
|
||||
if (id) uniqueIds.add(id);
|
||||
}
|
||||
const results = await Promise.all(Array.from(uniqueIds, (id) => listProjectTasks(id)));
|
||||
return results.flat();
|
||||
},
|
||||
|
||||
async upsert(projectId: string, input: ProjectTaskUpsertInput): Promise<WebGenerationPreviewTask> {
|
||||
const payload = await serverRequest<{ task: unknown }>(
|
||||
`projects/${encodeURIComponent(projectId)}/tasks/upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
body: input,
|
||||
},
|
||||
);
|
||||
const task = normalizeTask(payload.task ?? payload);
|
||||
if (!task) throw new Error("Project task response did not include a task");
|
||||
return toPreviewTask(task);
|
||||
},
|
||||
|
||||
async batchUpsert(projectId: string, tasks: ProjectTaskUpsertInput[]): Promise<WebGenerationPreviewTask[]> {
|
||||
const payload = await serverRequest<{ tasks: unknown }>(
|
||||
`projects/${encodeURIComponent(projectId)}/tasks/batch-upsert`,
|
||||
{
|
||||
method: "POST",
|
||||
body: { tasks },
|
||||
},
|
||||
);
|
||||
return extractTasks(payload).map(toPreviewTask);
|
||||
},
|
||||
};
|
||||
@@ -1,39 +0,0 @@
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ProviderHealthEntry {
|
||||
status: string;
|
||||
lastCheck: string | null;
|
||||
lastError: string | null;
|
||||
details: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface CallStatRow {
|
||||
provider: string;
|
||||
model: string;
|
||||
status: string;
|
||||
count: string;
|
||||
avg_ms: string | null;
|
||||
total_cost: string | null;
|
||||
}
|
||||
|
||||
export interface KeyStatRow {
|
||||
provider: string;
|
||||
total_keys: string;
|
||||
active_keys: string;
|
||||
current_load: string;
|
||||
}
|
||||
|
||||
export interface ProviderHealthResponse {
|
||||
health: Record<string, ProviderHealthEntry>;
|
||||
callStats: CallStatRow[];
|
||||
keyStats: KeyStatRow[];
|
||||
checkedAt: string;
|
||||
}
|
||||
|
||||
export const providerHealthClient = {
|
||||
async getStatus(): Promise<ProviderHealthResponse> {
|
||||
return serverRequest<ProviderHealthResponse>("admin/providers/status", {
|
||||
fallbackMessage: "Provider health request failed",
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,51 +0,0 @@
|
||||
import { isOptionalApiRouteMissing } from "./apiErrorUtils";
|
||||
import { isRecord, serverRequest } from "./serverConnection";
|
||||
|
||||
export interface WebPublicConfig {
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
companyAddress?: string;
|
||||
icpRecord?: string;
|
||||
}
|
||||
|
||||
function readString(config: Record<string, unknown>, keys: string[]): string | undefined {
|
||||
for (const key of keys) {
|
||||
const value = config[key];
|
||||
if (typeof value === "string" && value.trim()) return value.trim();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizePublicConfig(raw: unknown): WebPublicConfig {
|
||||
const config = isRecord(raw) && isRecord(raw.config) ? raw.config : raw;
|
||||
if (!isRecord(config)) return {};
|
||||
|
||||
return {
|
||||
contactEmail: readString(config, ["contactEmail", "contact_email", "supportEmail", "support_email"]),
|
||||
contactPhone: readString(config, ["contactPhone", "contact_phone", "supportPhone", "support_phone"]),
|
||||
companyAddress: readString(config, ["companyAddress", "company_address", "address"]),
|
||||
icpRecord: readString(config, ["icpRecord", "icp_record", "filingInfo", "filing_info"]),
|
||||
};
|
||||
}
|
||||
|
||||
let cachedPublicConfig: WebPublicConfig | null = null;
|
||||
let publicConfigRouteMissing = false;
|
||||
|
||||
export const publicConfigClient = {
|
||||
async get(): Promise<WebPublicConfig> {
|
||||
if (cachedPublicConfig) return cachedPublicConfig;
|
||||
if (publicConfigRouteMissing) return {};
|
||||
|
||||
try {
|
||||
const payload = await serverRequest<unknown>("public/config/profile?name=web-public-config");
|
||||
cachedPublicConfig = normalizePublicConfig(payload);
|
||||
return cachedPublicConfig;
|
||||
} catch (error) {
|
||||
if (isOptionalApiRouteMissing(error)) {
|
||||
publicConfigRouteMissing = true;
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,84 +0,0 @@
|
||||
import { aiGenerationClient } from "./aiGenerationClient";
|
||||
|
||||
interface UploadEntry {
|
||||
promise: Promise<string | null>;
|
||||
url: string | null;
|
||||
status: "pending" | "done" | "failed";
|
||||
}
|
||||
|
||||
const uploadCache = new Map<string, UploadEntry>();
|
||||
|
||||
function fileToDataUrl(file: File): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ""));
|
||||
reader.onerror = () => reject(reader.error || new Error("文件读取失败"));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
function buildCacheKey(file: File, fingerprint?: string): string {
|
||||
if (fingerprint) return fingerprint;
|
||||
return `${file.name}__${file.size}__${file.lastModified}`;
|
||||
}
|
||||
|
||||
export function preUploadReference(
|
||||
file: File,
|
||||
name: string,
|
||||
fingerprint?: string,
|
||||
): Promise<string | null> {
|
||||
const key = buildCacheKey(file, fingerprint);
|
||||
const cached = uploadCache.get(key);
|
||||
if (cached) return cached.promise;
|
||||
|
||||
const scope = file.type.startsWith("video/") ? "reference-video" : "reference-image";
|
||||
|
||||
const promise = (async () => {
|
||||
try {
|
||||
const dataUrl = await fileToDataUrl(file);
|
||||
const uploaded = await aiGenerationClient.uploadAsset({
|
||||
dataUrl,
|
||||
name,
|
||||
mimeType: file.type,
|
||||
scope,
|
||||
});
|
||||
const entry = uploadCache.get(key);
|
||||
if (entry) {
|
||||
entry.url = uploaded.url;
|
||||
entry.status = "done";
|
||||
}
|
||||
return uploaded.url;
|
||||
} catch (error) {
|
||||
const entry = uploadCache.get(key);
|
||||
if (entry) entry.status = "failed";
|
||||
console.warn("[referenceUpload] pre-upload failed:", error);
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
uploadCache.set(key, { promise, url: null, status: "pending" });
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function getPreUploadedUrl(
|
||||
file: File,
|
||||
fingerprint?: string,
|
||||
): string | null {
|
||||
const key = buildCacheKey(file, fingerprint);
|
||||
return uploadCache.get(key)?.url ?? null;
|
||||
}
|
||||
|
||||
export async function resolvePreUploadedUrl(
|
||||
file: File,
|
||||
name: string,
|
||||
fingerprint?: string,
|
||||
): Promise<string | null> {
|
||||
const key = buildCacheKey(file, fingerprint);
|
||||
const cached = uploadCache.get(key);
|
||||
if (cached) return cached.promise;
|
||||
return preUploadReference(file, name, fingerprint);
|
||||
}
|
||||
|
||||
export function clearUploadCache(): void {
|
||||
uploadCache.clear();
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ReportInput {
|
||||
reportType: string;
|
||||
targetType?: string;
|
||||
targetId?: string;
|
||||
contactName?: string;
|
||||
contactEmail?: string;
|
||||
contactPhone?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
pageUrl?: string;
|
||||
}
|
||||
|
||||
export interface AdminReportItem {
|
||||
id: number;
|
||||
userId?: number | null;
|
||||
username?: string | null;
|
||||
reportType?: string | null;
|
||||
targetType?: string | null;
|
||||
targetId?: string | null;
|
||||
contactName?: string | null;
|
||||
contactEmail?: string | null;
|
||||
contactPhone?: string | null;
|
||||
title: string;
|
||||
description: string;
|
||||
pageUrl?: string | null;
|
||||
status: string;
|
||||
ipAddress?: string | null;
|
||||
userAgent?: string | null;
|
||||
createdAt: string;
|
||||
updatedAt?: string | null;
|
||||
}
|
||||
|
||||
function normalizeReport(raw: unknown): AdminReportItem {
|
||||
const item = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as Record<string, unknown>) : {};
|
||||
return {
|
||||
id: Number(item.id) || 0,
|
||||
userId: item.userId == null ? null : Number(item.userId),
|
||||
username: typeof item.username === "string" ? item.username : null,
|
||||
reportType: typeof item.reportType === "string" ? item.reportType : null,
|
||||
targetType: typeof item.targetType === "string" ? item.targetType : null,
|
||||
targetId: typeof item.targetId === "string" ? item.targetId : null,
|
||||
contactName: typeof item.contactName === "string" ? item.contactName : null,
|
||||
contactEmail: typeof item.contactEmail === "string" ? item.contactEmail : null,
|
||||
contactPhone: typeof item.contactPhone === "string" ? item.contactPhone : null,
|
||||
title: typeof item.title === "string" && item.title.trim() ? item.title : "未命名举报",
|
||||
description: typeof item.description === "string" ? item.description : "",
|
||||
pageUrl: typeof item.pageUrl === "string" ? item.pageUrl : null,
|
||||
status: typeof item.status === "string" && item.status.trim() ? item.status : "pending",
|
||||
ipAddress: typeof item.ipAddress === "string" ? item.ipAddress : null,
|
||||
userAgent: typeof item.userAgent === "string" ? item.userAgent : null,
|
||||
createdAt: typeof item.createdAt === "string" ? item.createdAt : "",
|
||||
updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : null,
|
||||
};
|
||||
}
|
||||
|
||||
export const reportClient = {
|
||||
async submit(input: ReportInput): Promise<{ id: number; status: string; createdAt: string }> {
|
||||
const payload = await serverRequest<{ report: { id: number; status: string; createdAt: string } }>("reports", {
|
||||
method: "POST",
|
||||
body: input,
|
||||
});
|
||||
return payload.report;
|
||||
},
|
||||
|
||||
async listAdminReports(): Promise<AdminReportItem[]> {
|
||||
const payload = await serverRequest<{ reports?: unknown[] }>("admin/reports");
|
||||
return Array.isArray(payload.reports) ? payload.reports.map(normalizeReport) : [];
|
||||
},
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
import { serverRequest } from "./serverConnection";
|
||||
|
||||
export interface ScriptEvalResult {
|
||||
totalScore: number;
|
||||
grade: string;
|
||||
dimensionScores: Record<string, number>;
|
||||
subScores?: Record<string, Record<string, number>>;
|
||||
evidence?: Record<string, string[]>;
|
||||
summary: string;
|
||||
issues: string[];
|
||||
highlights: string[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
||||
const MODEL = "qwen3.7-max";
|
||||
|
||||
const EVAL_OUTPUT_CONTRACT = `
|
||||
强制输出 JSON,主维度键名必须严格为:
|
||||
hook(20), plot(20), character(15), logic(15), visual(15), content(15)。
|
||||
不要把 dialogue 作为主维度返回;台词对白作为 character/plot/content 的证据和子项分析。
|
||||
|
||||
同时返回 subScores 和 evidence:
|
||||
- subScores:每个主维度 3-5 个细分参数,分值按该维度满分拆分。
|
||||
- evidence:每个主维度 1-3 条具体证据,必须指向场景、台词、设定、冲突或段落。
|
||||
|
||||
返回结构:
|
||||
{
|
||||
"dimensionScores": { "hook": 数字, "plot": 数字, "character": 数字, "logic": 数字, "visual": 数字, "content": 数字 },
|
||||
"subScores": {
|
||||
"hook": { "openingImpact": 数字, "suspenseChain": 数字, "sceneHook": 数字 },
|
||||
"plot": { "structure": 数字, "rhythm": 数字, "conflict": 数字, "reversal": 数字 },
|
||||
"character": { "motivation": 数字, "arc": 数字, "voice": 数字, "relationship": 数字 },
|
||||
"logic": { "causality": 数字, "worldRules": 数字, "foreshadowing": 数字, "continuity": 数字 },
|
||||
"visual": { "sceneDetail": 数字, "shotPotential": 数字, "aigcFeasibility": 数字 },
|
||||
"content": { "theme": 数字, "emotion": 数字, "marketFit": 数字, "originality": 数字 }
|
||||
},
|
||||
"evidence": { "hook": ["..."], "plot": ["..."], "character": ["..."], "logic": ["..."], "visual": ["..."], "content": ["..."] },
|
||||
"summary": "200-300字综合评价",
|
||||
"issues": ["具体扣分点,带维度和证据", ...],
|
||||
"highlights": ["具体亮点,带维度和证据", ...],
|
||||
"suggestions": ["按优先级排列的改稿建议", ...]
|
||||
}`;
|
||||
|
||||
const EVAL_SYSTEM_PROMPT = `你是一位资深影视剧本评审专家,拥有二十年以上的编剧、制片和剧本医生经验。你精通各类影视叙事理论(三幕式、英雄之旅、起承转合、序列法),同时深度跟踪AIGC短剧/漫剧行业最新趋势。你的任务是对用户提供的剧本进行严谨、系统、多维度的量化评分。
|
||||
|
||||
【剧本类型识别】
|
||||
收到剧本后,首先判断类型:AIGC短剧/漫剧(单集5-30分钟,竖屏平台,高密度反转、强节奏)或传统影视剧本(单集40分钟以上,长视频平台,完整起承转合)。类型判定将影响各维度的评价侧重点。
|
||||
|
||||
【评分体系(100分制,六个维度)】
|
||||
1. hook 钩子设计(20分):开篇钩子、集末钩子、场景内钩子、悬念链完整性。短剧前3秒须有即时爆点;长剧第一幕结束前须建立核心悬念。
|
||||
2. plot 剧情结构(20分):结构框架、节奏控制、冲突设计、逻辑自洽。短剧"每分钟有事件",反转密度加分;长剧需处理好B线C线与主线交织。
|
||||
3. character 角色塑造(18分):主角弧光、角色辨识度、角色动机、配角质量。短剧角色须在前2分钟建立;长剧需要内在矛盾和多阶段成长。
|
||||
4. dialogue 台词对白(15分):角色语言区分度、信息传递效率、潜台词与留白、金句与记忆点。
|
||||
5. visual 画面表现(15分):场景描写质量、视觉叙事技巧、镜头感与节奏、制作可行性。AIGC需考虑AI生成技术边界与一致性。
|
||||
6. content 内容深度(12分):主题表达、情感共鸣、社会/人性洞察。
|
||||
|
||||
【评分铁律】
|
||||
- 扣分必须明确指出剧本中的具体段落/场景/台词。
|
||||
- 严禁给出任何维度的满分,必须有扣分理由。
|
||||
- 优缺点都要充分展开,不可只批不夸或只夸不批。
|
||||
- 不因题材类型偏见降低评分,不因某一方面出色而抬高其他维度(避免光环效应)。
|
||||
- 敢于拉开各维度分数差距,避免全部给中等分数。
|
||||
|
||||
【等级标准】按总分百分比:S≥90 | A 80-89 | B 70-79 | C 60-69 | D<60。
|
||||
|
||||
请严格按以下 JSON 格式返回(不要包含任何其他文字,不要用代码块包裹以外的说明):
|
||||
{
|
||||
"dimensionScores": { "hook": 数字, "plot": 数字, "character": 数字, "dialogue": 数字, "visual": 数字, "content": 数字 },
|
||||
"summary": "200-300字综合评价,概括整体质量、市场潜力与目标受众匹配度",
|
||||
"issues": ["每条指出具体维度的扣分点并引用剧本原文位置", ...],
|
||||
"highlights": ["核心亮点,引用剧本具体场景", ...],
|
||||
"suggestions": ["按优先级排列的改进建议(最优先/次优先/可优化)", ...]
|
||||
}`;
|
||||
|
||||
const DIMENSION_WEIGHTS: Record<string, { maxScore: number }> = {
|
||||
hook: { maxScore: 20 },
|
||||
plot: { maxScore: 20 },
|
||||
character: { maxScore: 15 },
|
||||
logic: { maxScore: 15 },
|
||||
visual: { maxScore: 15 },
|
||||
content: { maxScore: 15 },
|
||||
};
|
||||
|
||||
function computeTotalAndGrade(scores: Record<string, number>): { totalScore: number; grade: string } {
|
||||
const totalScore = Math.round(
|
||||
Object.entries(DIMENSION_WEIGHTS).reduce((sum, [key, dim]) => {
|
||||
return sum + Math.max(0, Math.min(dim.maxScore, scores[key] ?? 0));
|
||||
}, 0),
|
||||
);
|
||||
const grade = totalScore >= 90 ? "S" : totalScore >= 80 ? "A" : totalScore >= 70 ? "B" : totalScore >= 60 ? "C" : "D";
|
||||
return { totalScore, grade };
|
||||
}
|
||||
|
||||
function extractJson(text: string): unknown {
|
||||
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
||||
const raw = fenced ? fenced[1].trim() : text.trim();
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
function normalizeScoreValue(value: unknown, maxScore: number): number {
|
||||
const score = Number(value);
|
||||
if (!Number.isFinite(score)) return 0;
|
||||
return Math.max(0, Math.min(maxScore, Math.round(score * 10) / 10));
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function normalizeEvidenceItems(source: unknown[], limit: number): string[] {
|
||||
const items: string[] = [];
|
||||
for (const item of source) {
|
||||
const value = String(item).trim();
|
||||
if (!value) continue;
|
||||
items.push(value);
|
||||
if (items.length >= limit) break;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function normalizeNestedScores(value: unknown): Record<string, Record<string, number>> {
|
||||
if (!isRecord(value)) return {};
|
||||
|
||||
const normalized: Record<string, Record<string, number>> = {};
|
||||
for (const [dimensionKey, dimension] of Object.entries(DIMENSION_WEIGHTS)) {
|
||||
const source = value[dimensionKey] ?? (dimensionKey === "logic" ? value.dialogue : undefined);
|
||||
if (!isRecord(source)) continue;
|
||||
|
||||
const entries = Object.entries(source)
|
||||
.map(([key, score]) => [key, normalizeScoreValue(score, dimension.maxScore)] as const)
|
||||
.filter(([, score]) => score > 0);
|
||||
if (entries.length > 0) normalized[dimensionKey] = Object.fromEntries(entries);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeEvidence(value: unknown): Record<string, string[]> {
|
||||
if (!isRecord(value)) return {};
|
||||
|
||||
const normalized: Record<string, string[]> = {};
|
||||
for (const dimensionKey of Object.keys(DIMENSION_WEIGHTS)) {
|
||||
const source = value[dimensionKey] ?? (dimensionKey === "logic" ? value.dialogue : undefined);
|
||||
if (!Array.isArray(source)) continue;
|
||||
|
||||
const items = normalizeEvidenceItems(source, 3);
|
||||
if (items.length > 0) normalized[dimensionKey] = items;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export async function evaluateScript(script: string, signal?: AbortSignal): Promise<ScriptEvalResult> {
|
||||
const payload = await serverRequest<{
|
||||
content?: string;
|
||||
choices?: Array<{ message?: { content?: string } }>;
|
||||
text?: string;
|
||||
}>("ai/chat", {
|
||||
method: "POST",
|
||||
body: {
|
||||
model: MODEL,
|
||||
messages: [
|
||||
{ role: "system", content: EVAL_SYSTEM_PROMPT },
|
||||
{ role: "system", content: EVAL_OUTPUT_CONTRACT },
|
||||
{ role: "user", content: `请评测以下剧本:\n\n${script.slice(0, 8000)}` },
|
||||
],
|
||||
stream: false,
|
||||
temperature: 0.3,
|
||||
max_tokens: 4096,
|
||||
},
|
||||
signal,
|
||||
timeoutMs: 180_000,
|
||||
maxRetries: 0,
|
||||
fallbackMessage: "评测请求失败",
|
||||
});
|
||||
|
||||
const content: string = payload?.content ?? payload?.choices?.[0]?.message?.content ?? payload?.text ?? "";
|
||||
|
||||
if (!content) throw new Error("模型未返回有效内容");
|
||||
|
||||
const parsed = extractJson(content) as Record<string, unknown>;
|
||||
const dimensionScores: Record<string, number> = {};
|
||||
const rawScores = parsed.dimensionScores as Record<string, number> | undefined;
|
||||
if (!rawScores || typeof rawScores !== "object") throw new Error("评分格式异常");
|
||||
|
||||
for (const key of Object.keys(DIMENSION_WEIGHTS)) {
|
||||
const rawValue = key === "logic" ? rawScores.logic ?? rawScores.dialogue : rawScores[key];
|
||||
dimensionScores[key] = normalizeScoreValue(rawValue, DIMENSION_WEIGHTS[key].maxScore);
|
||||
}
|
||||
|
||||
const { totalScore, grade } = computeTotalAndGrade(dimensionScores);
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
grade,
|
||||
dimensionScores,
|
||||
subScores: normalizeNestedScores(parsed.subScores),
|
||||
evidence: normalizeEvidence(parsed.evidence),
|
||||
summary: String(parsed.summary || ""),
|
||||
issues: Array.isArray(parsed.issues) ? parsed.issues.map(String) : [],
|
||||
highlights: Array.isArray(parsed.highlights) ? parsed.highlights.map(String) : [],
|
||||
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions.map(String) : [],
|
||||
};
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
import { buildApiUrl, buildAuthHeaders, readJsonResponse, throwResponseError } from "./serverConnection";
|
||||
|
||||
export interface UploadProgressOptions {
|
||||
onProgress?: (percent: number) => void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export async function uploadAssetWithProgress(
|
||||
input: { dataUrl: string; name?: string; mimeType?: string; scope?: string },
|
||||
options?: UploadProgressOptions,
|
||||
): Promise<{ url: string; signedUrl?: string; ossKey?: string }> {
|
||||
const { onProgress, signal } = options || {};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const url = buildApiUrl("oss/upload");
|
||||
const headers = buildAuthHeaders();
|
||||
|
||||
xhr.open("POST", url);
|
||||
Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v));
|
||||
|
||||
xhr.upload.addEventListener("progress", (e) => {
|
||||
if (e.lengthComputable && onProgress) {
|
||||
onProgress(Math.round((e.loaded / e.total) * 100));
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("load", async () => {
|
||||
const fakeResponse = new Response(xhr.responseText, {
|
||||
status: xhr.status,
|
||||
statusText: xhr.statusText,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
try {
|
||||
if (!fakeResponse.ok) {
|
||||
await throwResponseError(fakeResponse, "Asset upload failed");
|
||||
}
|
||||
const result = await readJsonResponse<{ url: string; ossKey?: string }>(
|
||||
fakeResponse.clone(),
|
||||
"Asset upload response failed",
|
||||
);
|
||||
resolve(result);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
xhr.addEventListener("error", () => reject(new Error("上传失败,请检查网络连接")));
|
||||
xhr.addEventListener("abort", () => reject(new Error("上传已取消")));
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => xhr.abort());
|
||||
}
|
||||
|
||||
xhr.send(JSON.stringify(input));
|
||||
});
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export const webGenerationGateway = {
|
||||
const result = await aiGenerationClient.createImageTask({
|
||||
projectId: params?.projectId,
|
||||
conversationId: params?.conversationId,
|
||||
model: params?.model || "nano-banana-pro",
|
||||
model: "gpt-image-2",
|
||||
prompt,
|
||||
ratio: params?.ratio || "16:9",
|
||||
quality: params?.quality || "1K",
|
||||
|
||||
Reference in New Issue
Block a user