import { useEffect, useState, type FormEvent } from 'react'; import { Select as AntSelect } from 'antd'; import { Gauge, Pencil, Plus, RotateCcw, Route, Save, ShieldCheck, Trash2 } from 'lucide-react'; import type { GatewayRunnerPolicy, GatewayRunnerPolicyUpsertRequest, RunnerFailoverAction, RunnerFailoverTarget, RuntimePolicySet, RuntimePolicySetUpsertRequest, } from '@easyai-ai-gateway/contracts'; import { Badge, Button, Card, CardContent, CardHeader, CardTitle, ConfirmDialog, FormDialog, Input, Label, Select, Tabs, Textarea } from '../../components/ui'; import type { LoadState } from '../../types'; type RuntimePanelTab = 'model' | 'runner'; type RunnerActionRule = { id: string; enabled: boolean; categories: string[]; codes: string[]; statusCodes: string[]; keywords: string[]; action: RunnerFailoverAction; target: RunnerFailoverTarget; demoteSteps: string; cooldownSeconds: string; }; export type RuntimePolicyForm = { policyKey: string; name: string; description: string; rpm: string; tpm: string; concurrency: string; queueSize: string; queueMaxWaitSeconds: string; rateLimitPolicyExtra: Record; rateLimitRuleExtras: Record>; rateLimitPreservedRules: Array>; retryEnabled: boolean; retryMaxAttempts: string; retryAllowKeywords: string[]; retryDenyKeywords: string[]; autoDisableEnabled: boolean; autoDisableThreshold: string; autoDisableKeywords: string[]; degradeEnabled: boolean; degradeCooldownSeconds: string; degradeKeywords: string[]; metadataJson: string; status: string; }; type RunnerPolicyForm = { name: string; description: string; maxPlatforms: string; maxDurationSeconds: string; singleSourceProtectionEnabled: boolean; actionRules: RunnerActionRule[]; cacheAffinityPolicyJson: string; metadataJson: string; status: string; }; const failoverActionOptions: Array<{ label: string; value: RunnerFailoverAction }> = [ { label: '仅重试下一个', value: 'next' }, { label: '冷却后重试', value: 'cooldown_and_next' }, { label: '降级后重试', value: 'demote_and_next' }, { label: '禁用后重试', value: 'disable_and_next' }, { label: '不再重试', value: 'stop' }, ]; const failoverActionText: Record = { next: '仅重试下一个', cooldown_and_next: '冷却后重试', demote_and_next: '降级后重试', disable_and_next: '禁用后重试', stop: '不再重试', }; const failoverTargetOptions: Array<{ label: string; value: RunnerFailoverTarget }> = [ { label: '平台', value: 'platform' }, { label: '模型', value: 'model' }, ]; const failoverCategoryOptions = [ 'network', 'timeout', 'stream_error', 'rate_limit', 'provider_5xx', 'provider_overloaded', 'auth_error', 'request_error', 'unsupported_model', 'user_permission', 'insufficient_balance', 'client_error', ].map((item) => ({ label: item, value: item })); const legacyRunnerFailoverFields = ['allowCategories', 'denyCategories', 'allowCodes', 'denyCodes', 'allowKeywords', 'denyKeywords', 'allowStatusCodes', 'denyStatusCodes', 'actions']; let runnerActionRuleId = 0; export function RuntimePoliciesPanel(props: { message: string; runnerPolicy: GatewayRunnerPolicy | null; runtimePolicySets: RuntimePolicySet[]; state: LoadState; onDeleteRuntimePolicySet: (policySetId: string) => Promise; onSaveRunnerPolicy: (input: GatewayRunnerPolicyUpsertRequest) => Promise; onSaveRuntimePolicySet: (input: RuntimePolicySetUpsertRequest, policySetId?: string) => Promise; }) { const [activeTab, setActiveTab] = useState('model'); const [dialogOpen, setDialogOpen] = useState(false); const [editingId, setEditingId] = useState(''); const [form, setForm] = useState(() => createDefaultForm()); const [runnerForm, setRunnerForm] = useState(() => runnerPolicyToForm(null)); const [localError, setLocalError] = useState(''); const [pendingDeletePolicy, setPendingDeletePolicy] = useState(null); useEffect(() => { setRunnerForm(runnerPolicyToForm(props.runnerPolicy)); }, [props.runnerPolicy?.id, props.runnerPolicy?.updatedAt]); function openCreateDialog() { setEditingId(''); setLocalError(''); setForm(createDefaultForm(`runtime-${Date.now().toString(36)}`)); setDialogOpen(true); } function editPolicy(policy: RuntimePolicySet) { setEditingId(policy.id); setLocalError(''); setForm(policyToForm(policy)); setDialogOpen(true); } function closeDialog() { setDialogOpen(false); setEditingId(''); setLocalError(''); setForm(createDefaultForm()); } async function submit(event: FormEvent) { event.preventDefault(); setLocalError(''); try { await props.onSaveRuntimePolicySet(formToPayload(form), editingId || undefined); closeDialog(); } catch (err) { setLocalError(err instanceof Error ? err.message : '运行策略保存失败'); } } async function deletePolicy(policy: RuntimePolicySet) { if (isDefaultPolicy(policy)) return; try { await props.onDeleteRuntimePolicySet(policy.id); setPendingDeletePolicy(null); } catch (err) { setLocalError(err instanceof Error ? err.message : '运行策略删除失败'); } } async function submitRunnerPolicy(event: FormEvent) { event.preventDefault(); setLocalError(''); try { if (runnerForm.actionRules.some((rule) => !hasRunnerActionRuleCondition(rule))) { throw new Error('每条故障动作规则至少需要一个匹配条件'); } await props.onSaveRunnerPolicy(runnerFormToPayload(runnerForm)); } catch (err) { setLocalError(err instanceof Error ? err.message : '全局调度策略保存失败'); } } return (
运行策略

模型运行策略维护限流和平台内重试;旧自动禁用、冷却字段仅兼容读取,全局 action rule 统一控制平台间动作。

{activeTab === 'model' && }
{(props.message || localError) &&

{localError || props.message}

} }, { value: 'runner', label: '全局调度策略', icon: }, ]} onValueChange={setActiveTab} />
{activeTab === 'runner' && ( )} {activeTab === 'model' && (
{props.runtimePolicySets.map((policy) => (
{policy.name} {policy.policyKey}
{policy.status}
{policy.description &&

{policy.description}

}
{rateLimitSummary(policy)} {retrySummary(policy)} {autoDisableSummary(policy)} {degradeSummary(policy)}
))}
)} )} open={dialogOpen} title={editingId ? '编辑运行策略' : '新增运行策略'} onClose={closeDialog} onSubmit={submit} >
限流策略TPM / RPM / 并发 / 非文本任务排队
平台内重试策略允许/拒绝关键词控制同一平台是否再次调用
setForm({ ...form, retryEnabled: checked })} /> setForm({ ...form, retryAllowKeywords: value })} /> setForm({ ...form, retryDenyKeywords: value })} />
兼容规则(已弃用)仅用于读取旧配置;新配置请在 Runner 的 action rule 编辑器中完成
undefined} /> undefined} /> undefined} /> undefined} />