fix: harden generation task polling fallback
This commit is contained in:
@@ -8,6 +8,7 @@ import ToastContainer from "./components/toast/ToastContainer";
|
|||||||
import { toast } from "./components/toast/toastStore";
|
import { toast } from "./components/toast/toastStore";
|
||||||
import { aiGenerationClient } from "./api/aiGenerationClient";
|
import { aiGenerationClient } from "./api/aiGenerationClient";
|
||||||
import { keyServerClient } from "./api/keyServerClient";
|
import { keyServerClient } from "./api/keyServerClient";
|
||||||
|
import { setUserMaxConcurrency } from "./api/generationConcurrency";
|
||||||
import { notificationClient } from "./api/notificationClient";
|
import { notificationClient } from "./api/notificationClient";
|
||||||
import {
|
import {
|
||||||
SERVER_SESSION_REPLACED_EVENT,
|
SERVER_SESSION_REPLACED_EVENT,
|
||||||
@@ -466,6 +467,7 @@ function App() {
|
|||||||
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
const clearAuthenticatedState = useCallback((options?: { resetView?: boolean }) => {
|
||||||
clearAllUserStorage();
|
clearAllUserStorage();
|
||||||
clearSessionState();
|
clearSessionState();
|
||||||
|
setUserMaxConcurrency(null);
|
||||||
setProjects([]);
|
setProjects([]);
|
||||||
setProjectsLoaded(true);
|
setProjectsLoaded(true);
|
||||||
setUsage(emptyUsageSummary);
|
setUsage(emptyUsageSummary);
|
||||||
@@ -574,6 +576,7 @@ function App() {
|
|||||||
const nextSession = await keyServerClient.getCurrentSession();
|
const nextSession = await keyServerClient.getCurrentSession();
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setSession(nextSession);
|
setSession(nextSession);
|
||||||
|
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
|
||||||
await hydrateAccountData(nextSession);
|
await hydrateAccountData(nextSession);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -606,6 +609,7 @@ function App() {
|
|||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
if (nextSession) {
|
if (nextSession) {
|
||||||
setSession(nextSession);
|
setSession(nextSession);
|
||||||
|
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
|
||||||
} else {
|
} else {
|
||||||
clearAuthenticatedState({ resetView: true });
|
clearAuthenticatedState({ resetView: true });
|
||||||
}
|
}
|
||||||
@@ -943,6 +947,7 @@ function App() {
|
|||||||
async (nextSession: WebUserSession) => {
|
async (nextSession: WebUserSession) => {
|
||||||
hideSessionReplaced();
|
hideSessionReplaced();
|
||||||
setSession(nextSession);
|
setSession(nextSession);
|
||||||
|
setUserMaxConcurrency(nextSession?.user?.maxConcurrency);
|
||||||
await hydrateAccountData(nextSession);
|
await hydrateAccountData(nextSession);
|
||||||
|
|
||||||
if (nextSession.user.email && !nextSession.user.emailVerified) {
|
if (nextSession.user.email && !nextSession.user.emailVerified) {
|
||||||
|
|||||||
@@ -7,10 +7,20 @@ interface GenerationSlot {
|
|||||||
createdAt: number;
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_ACTIVE_GENERATION_TASKS = 3;
|
const DEFAULT_MAX_ACTIVE_GENERATION_TASKS = 3;
|
||||||
const STALE_SLOT_MS = 6 * 60 * 60 * 1000;
|
const STALE_SLOT_MS = 6 * 60 * 60 * 1000;
|
||||||
const activeSlots = new Map<string, GenerationSlot>();
|
const activeSlots = new Map<string, GenerationSlot>();
|
||||||
|
|
||||||
|
let userMaxConcurrency: number | null = null;
|
||||||
|
|
||||||
|
export function setUserMaxConcurrency(limit: number | null | undefined): void {
|
||||||
|
userMaxConcurrency = typeof limit === "number" && limit > 0 ? limit : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEffectiveLimit(): number {
|
||||||
|
return userMaxConcurrency ?? DEFAULT_MAX_ACTIVE_GENERATION_TASKS;
|
||||||
|
}
|
||||||
|
|
||||||
export function getGenerationUserKey(userId?: string | number | null): string {
|
export function getGenerationUserKey(userId?: string | number | null): string {
|
||||||
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
|
return userId === undefined || userId === null || userId === "" ? "anonymous" : String(userId);
|
||||||
}
|
}
|
||||||
@@ -39,8 +49,9 @@ export function claimGenerationSlot(input: {
|
|||||||
}): () => void {
|
}): () => void {
|
||||||
pruneStaleSlots();
|
pruneStaleSlots();
|
||||||
const activeCount = getActiveGenerationTaskCount(input.userKey);
|
const activeCount = getActiveGenerationTaskCount(input.userKey);
|
||||||
if (activeCount >= MAX_ACTIVE_GENERATION_TASKS) {
|
const effectiveLimit = getEffectiveLimit();
|
||||||
throw new Error("当前账号同时最多生成 3 个图片/视频任务,请等待已有任务完成后再提交。");
|
if (activeCount >= effectiveLimit) {
|
||||||
|
throw new Error(`当前账号同时最多生成 ${effectiveLimit} 个图片/视频任务,请等待已有任务完成后再提交。`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const id = input.id || `generation-slot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
const id = input.id || `generation-slot-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
|||||||
@@ -434,6 +434,7 @@ function normalizeUser(raw: unknown): WebUserSession["user"] | null {
|
|||||||
candidate.enterpriseBalance ??
|
candidate.enterpriseBalance ??
|
||||||
candidate.enterprise_balance,
|
candidate.enterprise_balance,
|
||||||
),
|
),
|
||||||
|
maxConcurrency: toNumber(candidate.maxConcurrency ?? candidate.max_concurrency),
|
||||||
activePackages: toActivePackages(candidate.activePackages ?? candidate.active_packages),
|
activePackages: toActivePackages(candidate.activePackages ?? candidate.active_packages),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ export function waitForTask(
|
|||||||
let settled = false;
|
let settled = false;
|
||||||
let cleanup: (() => void) | null = null;
|
let cleanup: (() => void) | null = null;
|
||||||
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||||
let sseConnected = false;
|
|
||||||
let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
|
let fallbackTimerId: ReturnType<typeof setTimeout> | null = null;
|
||||||
let lastProgress = 0;
|
let lastProgress = 0;
|
||||||
let lastProgressAt = startedAt;
|
let lastProgressAt = startedAt;
|
||||||
@@ -83,10 +82,9 @@ export function waitForTask(
|
|||||||
};
|
};
|
||||||
|
|
||||||
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
|
cleanup = aiGenerationClient.subscribeTaskStatus(taskId, handleUpdate);
|
||||||
sseConnected = true;
|
|
||||||
|
|
||||||
fallbackTimerId = setTimeout(() => {
|
fallbackTimerId = setTimeout(() => {
|
||||||
if (settled || !sseConnected) return;
|
if (settled) return;
|
||||||
if (cleanup) cleanup();
|
if (cleanup) cleanup();
|
||||||
startPolling();
|
startPolling();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ export interface WebUserSession extends WebApiResultMeta {
|
|||||||
enterpriseAdminUserId?: number | string | null;
|
enterpriseAdminUserId?: number | string | null;
|
||||||
balanceCents?: number;
|
balanceCents?: number;
|
||||||
enterpriseBalanceCents?: number;
|
enterpriseBalanceCents?: number;
|
||||||
|
maxConcurrency?: number;
|
||||||
activePackages?: Array<{
|
activePackages?: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
expiresAt: string;
|
expiresAt: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user