feat: add river-backed async task queue

This commit is contained in:
2026-05-12 10:11:54 +08:00
parent d69aaed444
commit 7e220b7477
30 changed files with 1342 additions and 200 deletions
+18 -31
View File
@@ -21,7 +21,7 @@ import { mermaid } from '@streamdown/mermaid';
import type { GatewayApiKey, GatewayTask, PlatformModel } from '@easyai-ai-gateway/contracts';
import { Bot, ChevronDown, Image as ImageIcon, MessageSquarePlus, Paperclip, Send, Sparkles, Video } from 'lucide-react';
import { Badge, Button, Select, Textarea } from '../components/ui';
import { GatewayApiError, createImageGenerationTask, createVideoGenerationTask, getTask, streamChatCompletionText } from '../api';
import { GatewayApiError, createImageGenerationTask, createVideoGenerationTask, pollTaskUntilSettled, streamChatCompletionText, taskIsPending } from '../api';
import type { PlaygroundMode } from '../types';
import {
defaultMediaGenerationSettings,
@@ -170,14 +170,12 @@ export function PlaygroundPage(props: {
resumableRuns.forEach((run) => {
if (!run.task?.id) return;
resumedTaskIdsRef.current.add(run.task.id);
void pollTaskUntilSettled(credential, run.task)
void pollTaskUntilSettled(credential, run.task, {
onUpdate: (detail) => updateMediaRunFromTask(run.localId, detail),
})
.then((detail) => {
if (!isMountedRef.current) return;
setMediaRuns((current) => updateMediaRun(current, run.localId, {
error: gatewayTaskErrorText(detail, '任务执行失败'),
status: detail.status,
task: detail,
}));
updateMediaRunFromTask(run.localId, detail);
})
.catch((err) => {
if (!isMountedRef.current) return;
@@ -249,13 +247,11 @@ export function PlaygroundPage(props: {
async function pollMediaRunUntilSettled(credential: string, localId: string, task: GatewayTask) {
try {
const detail = await pollTaskUntilSettled(credential, task);
const detail = await pollTaskUntilSettled(credential, task, {
onUpdate: (nextTask) => updateMediaRunFromTask(localId, nextTask),
});
if (!isMountedRef.current) return;
setMediaRuns((current) => updateMediaRun(current, localId, {
error: gatewayTaskErrorText(detail, '任务执行失败'),
status: detail.status,
task: detail,
}));
updateMediaRunFromTask(localId, detail);
} catch (err) {
if (!isMountedRef.current) return;
const errorMessage = err instanceof Error ? err.message : '任务状态同步失败';
@@ -263,6 +259,15 @@ export function PlaygroundPage(props: {
}
}
function updateMediaRunFromTask(localId: string, task: GatewayTask) {
if (!isMountedRef.current) return;
setMediaRuns((current) => updateMediaRun(current, localId, {
error: taskIsPending(task.status) ? '' : gatewayTaskErrorText(task, '任务执行失败'),
status: task.status,
task,
}));
}
function editMediaRun(run: MediaGenerationRun) {
setPrompt(run.prompt);
setMediaSettings(run.settings);
@@ -1042,20 +1047,6 @@ function updateMediaRun(runs: MediaGenerationRun[], localId: string, patch: Part
return runs.map((run) => run.localId === localId ? { ...run, ...patch } : run);
}
async function pollTaskUntilSettled(token: string, task: GatewayTask) {
let detail = task;
for (let attempt = 0; attempt < 20; attempt += 1) {
detail = await getTask(token, detail.id);
if (!taskIsPending(detail.status)) return detail;
await delay(1200);
}
return detail;
}
function taskIsPending(status: string) {
return status === 'queued' || status === 'running' || status === 'submitting';
}
function readStoredMediaRuns(): MediaGenerationRun[] {
if (typeof window === 'undefined') return [];
try {
@@ -1182,10 +1173,6 @@ function booleanFromUnknown(value: unknown, fallback: boolean) {
return fallback;
}
function delay(ms: number) {
return new Promise((resolve) => window.setTimeout(resolve, ms));
}
function newLocalId() {
return typeof crypto !== 'undefined' && 'randomUUID' in crypto
? crypto.randomUUID()
@@ -593,11 +593,14 @@ function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platfor
<TableRow className="shTableHeader">
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead>TPM</TableHead>
<TableHead>RPM</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead className="platformLimitMetricHead platformLimitNumberHead" title="正在执行 / 并发上限 / 排队任务">
<span></span>
<small> / / </small>
</TableHead>
<TableHead className="platformLimitNumberHead">TPM</TableHead>
<TableHead className="platformLimitNumberHead">RPM</TableHead>
<TableHead className="platformLimitStatusHead"></TableHead>
<TableHead className="platformLimitNumberHead"></TableHead>
</TableRow>
{props.statuses.map((status) => {
const platform = props.platformMap.get(status.platformId);
@@ -615,12 +618,12 @@ function RateLimitStatusTable(props: { statuses: ModelRateLimitStatus[]; platfor
<small>{status.provider}</small>
</span>
</TableCell>
<TableCell>{metricCell(status.concurrent)}</TableCell>
<TableCell>{metricCell(status.tpm, true)}</TableCell>
<TableCell>{metricCell(status.rpm)}</TableCell>
<TableCell>{modelRuntimeStatusCell(status, props.now)}</TableCell>
<TableCell>
<span className="rateLoadCell">
<TableCell className="platformLimitNumberCell">{concurrencyMetricCell(status)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.tpm, true)}</TableCell>
<TableCell className="platformLimitNumberCell">{metricCell(status.rpm)}</TableCell>
<TableCell className="platformLimitStatusCell">{modelRuntimeStatusCell(status, props.now)}</TableCell>
<TableCell className="platformLimitNumberCell">
<span className="rateLoadCell" data-overloaded={status.loadRatio > 0.8 ? 'true' : undefined}>
<strong>{formatPercent(status.loadRatio)}</strong>
<span className="rateLoadTrack"><i style={{ width: `${Math.min(status.loadRatio * 100, 100)}%` }} /></span>
</span>
@@ -1210,6 +1213,16 @@ function metricCell(metric: ModelRateLimitStatus['rpm'], includeReserved = false
);
}
function concurrencyMetricCell(status: ModelRateLimitStatus) {
const queuedTasks = status.queuedTasks ?? 0;
const limitText = status.concurrent.limited ? formatLimit(status.concurrent.limitValue) : '不限';
return (
<span className="rateMetricCell" title="正在执行 / 并发上限 / 排队任务">
<strong>{formatLimit(status.concurrent.currentValue)} / {limitText} / {formatLimit(queuedTasks)}</strong>
</span>
);
}
function reservedMetricText(metric: ModelRateLimitStatus['rpm']) {
return `已结算 ${formatLimit(metric.usedValue)} + 预占 ${formatLimit(metric.reservedValue)}`;
}