Add runtime restore and temp asset cleanup

This commit is contained in:
2026-06-05 20:28:26 +08:00
parent 644a6f9d17
commit d41d9482c7
22 changed files with 1837 additions and 50 deletions
+46
View File
@@ -84,6 +84,7 @@ import {
pollTaskUntilSettled,
registerLocalAccount,
replacePlatformModels,
restoreModelRuntimeStatus,
setUserWalletBalance,
type HealthResponse,
updateAccessRule,
@@ -635,6 +636,50 @@ export function App() {
}
}
async function restoreRuntimeModel(platformModelId: string) {
setCoreState('loading');
setCoreMessage('');
try {
const restored = await restoreModelRuntimeStatus(token, platformModelId);
setModelRateLimits((current) => current.map((status) => {
if (status.platformId !== restored.platformId) return status;
return {
...status,
platformStatus: 'enabled',
platformCooldownUntil: undefined,
platformDisabledReason: undefined,
...(status.platformModelId === platformModelId
? {
enabled: restored.enabled,
modelCooldownUntil: restored.modelCooldownUntil,
}
: {}),
};
}));
setPlatforms((current) => current.map((platform) => platform.id === restored.platformId
? {
...platform,
status: 'enabled',
cooldownUntil: undefined,
}
: platform));
setModels((current) => current.map((model) => model.id === platformModelId
? {
...model,
enabled: true,
cooldownUntil: undefined,
}
: model));
invalidateDataKeys('modelCatalog', 'modelRateLimits', 'models', 'platforms', 'playgroundModels');
setCoreState('ready');
setCoreMessage('模型运行状态已恢复。');
} catch (err) {
setCoreState('error');
setCoreMessage(err instanceof Error ? err.message : '恢复模型运行状态失败');
throw err;
}
}
async function removePlatform(platformId: string) {
setCoreState('loading');
setCoreMessage('');
@@ -1143,6 +1188,7 @@ export function App() {
onResetBaseModel={resetBaseModelToDefault}
onSavePlatform={savePlatformWithModels}
onSavePlatformDynamicPriority={savePlatformDynamicPriority}
onRestoreRuntimeModel={restoreRuntimeModel}
onTogglePlatformStatus={savePlatformStatus}
onSaveProvider={saveProvider}
onSavePricingRuleSet={savePricingRuleSet}
+7
View File
@@ -913,6 +913,13 @@ export async function listModelRateLimitStatuses(token: string): Promise<ListRes
return request<ListResponse<ModelRateLimitStatus>>('/api/admin/runtime/model-rate-limits', { token });
}
export async function restoreModelRuntimeStatus(token: string, platformModelId: string): Promise<ModelRateLimitStatus> {
return request<ModelRateLimitStatus>(`/api/admin/runtime/model-rate-limits/${platformModelId}/restore`, {
method: 'POST',
token,
});
}
export async function getNetworkProxyConfig(token: string): Promise<GatewayNetworkProxyConfig> {
return request<GatewayNetworkProxyConfig>('/api/admin/config/network-proxy', { token });
}
+2
View File
@@ -71,6 +71,7 @@ export function AdminPage(props: {
onBatchAccessRules: (input: GatewayAccessRuleBatchRequest) => Promise<void>;
onSavePlatform: (input: PlatformWithModelsInput) => Promise<void>;
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
onRestoreRuntimeModel: (platformModelId: string) => Promise<void>;
onTogglePlatformStatus: (platform: IntegrationPlatform, status: 'enabled' | 'disabled') => Promise<void>;
onSaveProvider: (input: CatalogProviderUpsertRequest, providerId?: string) => Promise<void>;
onSavePricingRuleSet: (input: PricingRuleSetUpsertRequest, ruleSetId?: string) => Promise<void>;
@@ -173,6 +174,7 @@ export function AdminPage(props: {
modelRateLimitsUpdatedAt={props.data.modelRateLimitsUpdatedAt}
platforms={props.data.platforms}
onSavePlatformDynamicPriority={props.onSavePlatformDynamicPriority}
onRestoreRuntimeModel={props.onRestoreRuntimeModel}
/>
)}
{props.section === 'tenants' && <TenantsPanel {...identityPanelProps(props)} />}
+100 -21
View File
@@ -9,11 +9,13 @@ export function RealtimeLoadPanel(props: {
modelRateLimitsUpdatedAt: number | null;
platforms: IntegrationPlatform[];
onSavePlatformDynamicPriority: (platformId: string, input: PlatformDynamicPriorityUpdateRequest) => Promise<void>;
onRestoreRuntimeModel: (platformModelId: string) => Promise<void>;
}) {
const [now, setNow] = useState(() => Date.now());
const [priorityDialog, setPriorityDialog] = useState<PriorityDialogState | null>(null);
const [priorityError, setPriorityError] = useState('');
const [prioritySaving, setPrioritySaving] = useState(false);
const [restoreSavingId, setRestoreSavingId] = useState<string | null>(null);
const platformMap = useMemo(() => new Map(props.platforms.map((item) => [item.id, item])), [props.platforms]);
useEffect(() => {
@@ -65,6 +67,17 @@ export function RealtimeLoadPanel(props: {
}
}
async function restoreRuntimeModel(platformModelId: string) {
setRestoreSavingId(platformModelId);
try {
await props.onRestoreRuntimeModel(platformModelId);
} catch {
// App-level state owns the error message.
} finally {
setRestoreSavingId(null);
}
}
return (
<section className="pageStack">
<Card>
@@ -81,6 +94,8 @@ export function RealtimeLoadPanel(props: {
statuses={props.modelRateLimits}
updatedAt={props.modelRateLimitsUpdatedAt}
onAdjustPriority={openPriorityDialog}
onRestoreRuntimeModel={restoreRuntimeModel}
restoreSavingId={restoreSavingId}
/>
</CardContent>
</Card>
@@ -109,6 +124,8 @@ function RateLimitStatusTable(props: {
now: number;
updatedAt: number | null;
onAdjustPriority: (status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined) => void;
onRestoreRuntimeModel: (platformModelId: string) => Promise<void>;
restoreSavingId: string | null;
}) {
if (!props.statuses.length) {
return <EmptyState title="暂无实时负载" description="模型产生请求后会在这里显示实时 RPM、TPM 和并发窗口。" />;
@@ -150,7 +167,15 @@ function RateLimitStatusTable(props: {
<small>{status.provider}</small>
</span>
</TableCell>
<TableCell className="platformLimitStatusCell">{modelRuntimeStatusCell(status, platform, props.now)}</TableCell>
<TableCell className="platformLimitStatusCell">
{modelRuntimeStatusCell(
status,
platform,
props.now,
props.onRestoreRuntimeModel,
props.restoreSavingId === status.platformModelId,
)}
</TableCell>
<TableCell className="platformLimitNumberCell">{platformPriorityCell(status, platform, props.onAdjustPriority)}</TableCell>
<TableCell className="platformLimitNumberCell">
<span className="rateLoadCell" data-overloaded={status.loadRatio > 0.8 ? 'true' : undefined}>
@@ -447,50 +472,104 @@ function shortId(value: string | undefined) {
return value.length > 8 ? value.slice(0, 8) : value;
}
function modelRuntimeStatusCell(status: ModelRateLimitStatus, platform: IntegrationPlatform | undefined, now: number) {
function modelRuntimeStatusCell(
status: ModelRateLimitStatus,
platform: IntegrationPlatform | undefined,
now: number,
onRestore: (platformModelId: string) => Promise<void>,
restoring: boolean,
) {
const modelCooldownMs = cooldownRemainingMs(status.modelCooldownUntil, now);
const platformCooldownMs = cooldownRemainingMs(status.platformCooldownUntil, now);
const platformStatus = platform?.status || status.platformStatus || 'enabled';
const restoreButton = runtimeRestoreButton(status, platformStatus, modelCooldownMs, platformCooldownMs, onRestore, restoring);
if (modelCooldownMs > 0) {
return (
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(modelCooldownMs)}</small>
<span className="platformRuntimeStatusCell">
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(modelCooldownMs)}</small>
</span>
{restoreButton}
</span>
);
}
if (platformStatus !== 'enabled') {
const badge = <Badge variant="warning"></Badge>;
return (
<AntPopover
align={{ offset: [0, 8] }}
content={<PlatformDisabledReasonPopover record={status.platformDisabledReason} />}
overlayClassName="priorityDemotionAntPopover"
placement="bottomLeft"
trigger={['hover', 'focus']}
>
<span className="platformTableName" tabIndex={0}>
<strong>{badge}</strong>
<span className="platformRuntimeStatusCell">
<AntPopover
align={{ offset: [0, 8] }}
content={<PlatformDisabledReasonPopover record={status.platformDisabledReason} />}
overlayClassName="priorityDemotionAntPopover"
placement="bottomLeft"
trigger={['hover', 'focus']}
>
<span className="platformTableName" tabIndex={0}>
<strong>{badge}</strong>
</span>
</AntPopover>
{restoreButton}
</span>
);
}
if (!status.enabled) {
return (
<span className="platformRuntimeStatusCell">
<span className="platformTableName">
<strong><Badge variant="secondary"></Badge></strong>
<small></small>
</span>
</AntPopover>
{restoreButton}
</span>
);
}
if (platformCooldownMs > 0) {
return (
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(platformCooldownMs)}</small>
<span className="platformRuntimeStatusCell">
<span className="platformTableName">
<strong><Badge variant="warning"></Badge></strong>
<small> {formatCooldownRemaining(platformCooldownMs)}</small>
</span>
{restoreButton}
</span>
);
}
return (
<span className="platformTableName">
<strong><Badge variant={status.enabled ? 'success' : 'secondary'}>{status.enabled ? '可用' : '已停用'}</Badge></strong>
<small>{status.enabled ? '参与路由' : '不参与路由'}</small>
<span className="platformRuntimeStatusCell">
<span className="platformTableName">
<strong><Badge variant="success"></Badge></strong>
<small></small>
</span>
</span>
);
}
function runtimeRestoreButton(
status: ModelRateLimitStatus,
platformStatus: string,
modelCooldownMs: number,
platformCooldownMs: number,
onRestore: (platformModelId: string) => Promise<void>,
restoring: boolean,
) {
const canRestore = modelCooldownMs > 0 || platformCooldownMs > 0 || platformStatus !== 'enabled' || !status.enabled;
if (!canRestore) return null;
return (
<Button
className="platformRestoreButton"
type="button"
variant="outline"
size="xs"
disabled={restoring}
onClick={() => void onRestore(status.platformModelId)}
>
<RotateCcw size={13} />
{restoring ? '恢复中' : '恢复'}
</Button>
);
}
function cooldownRemainingMs(cooldownUntil: string | undefined, now: number) {
if (!cooldownUntil) return 0;
const until = Date.parse(cooldownUntil);
+18 -2
View File
@@ -1086,8 +1086,8 @@
}
.platformLimitTable .shTableRow {
grid-template-columns: minmax(180px, 1.1fr) minmax(160px, 0.9fr) 160px 132px 150px 170px 140px 132px;
min-width: 1224px;
grid-template-columns: minmax(180px, 1.1fr) minmax(160px, 0.9fr) 178px 132px 150px 170px 140px 132px;
min-width: 1242px;
}
.platformLimitTable .shTableHead,
@@ -1130,6 +1130,22 @@
justify-items: start;
}
.platformRuntimeStatusCell {
display: grid;
min-width: 0;
gap: 7px;
align-content: start;
}
.platformRestoreButton {
justify-self: start;
min-height: 22px;
padding-inline: 8px;
border-color: var(--border);
color: var(--text-normal);
background: #fff;
}
.rateMetricCell,
.rateLoadCell {
display: grid;