refactor(runner): 收敛上游失败决策与轮转策略
将同平台重试、跨平台动作、健康副作用和冷却排队统一到单一失败决策,避免旧降级策略与 failover 重复执行。\n\n增加事务级单源保护、候选实时刷新、冷却错误契约、管理接口严格校验及兼容策略只读展示。\n\n验证:真实 Gateway HTTP/PostgreSQL 接受测试 10 项通过;go test ./...、pnpm openapi、pnpm lint、pnpm test、pnpm build 均通过。
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
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, RuntimePolicySet, RuntimePolicySetUpsertRequest } from '@easyai-ai-gateway/contracts';
|
||||
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 RunnerFailoverAction = 'next' | 'cooldown_and_next' | 'demote_and_next' | 'disable_and_next' | 'stop';
|
||||
type RunnerFailoverTarget = 'platform' | 'model';
|
||||
type RunnerActionRule = {
|
||||
id: string;
|
||||
enabled: boolean;
|
||||
@@ -159,6 +164,9 @@ export function RuntimePoliciesPanel(props: {
|
||||
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 : '全局调度策略保存失败');
|
||||
@@ -171,7 +179,7 @@ export function RuntimePoliciesPanel(props: {
|
||||
<CardHeader>
|
||||
<div>
|
||||
<CardTitle>运行策略</CardTitle>
|
||||
<p className="mutedText">模型运行策略维护限流、平台内重试、自动禁用和降级;全局调度策略控制平台间切换、硬拒绝和失败平台优先级降级。</p>
|
||||
<p className="mutedText">模型运行策略维护限流和平台内重试;旧自动禁用、冷却字段仅兼容读取,全局 action rule 统一控制平台间动作。</p>
|
||||
</div>
|
||||
{activeTab === 'model' && <Button type="button" onClick={openCreateDialog}>
|
||||
<Plus size={15} />
|
||||
@@ -286,14 +294,14 @@ export function RuntimePoliciesPanel(props: {
|
||||
</section>
|
||||
|
||||
<section className="runtimePolicySection spanTwo">
|
||||
<header><strong>禁用与降级</strong><span>自动禁用错误关键词、优先级降级关键词</span></header>
|
||||
<header><strong>兼容规则(已弃用)</strong><span>仅用于读取旧配置;新配置请在 Runner 的 action rule 编辑器中完成</span></header>
|
||||
<div className="runtimePolicyRows">
|
||||
<Toggle checked={form.autoDisableEnabled} label="启用自动禁用" onChange={(checked) => setForm({ ...form, autoDisableEnabled: checked })} />
|
||||
<Label>禁用触发次数<Input value={form.autoDisableThreshold} inputMode="numeric" onChange={(event) => setForm({ ...form, autoDisableThreshold: event.target.value })} /></Label>
|
||||
<KeywordField label="自动禁用关键词" value={form.autoDisableKeywords} onChange={(value) => setForm({ ...form, autoDisableKeywords: value })} />
|
||||
<Toggle checked={form.degradeEnabled} label="启用优先级降级" onChange={(checked) => setForm({ ...form, degradeEnabled: checked })} />
|
||||
<Label>降级冷却秒数<Input value={form.degradeCooldownSeconds} inputMode="numeric" onChange={(event) => setForm({ ...form, degradeCooldownSeconds: event.target.value })} /></Label>
|
||||
<KeywordField label="降级关键词" value={form.degradeKeywords} onChange={(value) => setForm({ ...form, degradeKeywords: value })} />
|
||||
<Toggle checked={form.autoDisableEnabled} disabled label="旧自动禁用" onChange={() => undefined} />
|
||||
<Label>禁用触发次数<Input disabled value={form.autoDisableThreshold} inputMode="numeric" /></Label>
|
||||
<KeywordField disabled label="旧自动禁用关键词" value={form.autoDisableKeywords} onChange={() => undefined} />
|
||||
<Toggle checked={form.degradeEnabled} disabled label="旧模型冷却" onChange={() => undefined} />
|
||||
<Label>模型冷却秒数<Input disabled value={form.degradeCooldownSeconds} inputMode="numeric" /></Label>
|
||||
<KeywordField disabled label="旧模型冷却关键词" value={form.degradeKeywords} onChange={() => undefined} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -313,10 +321,10 @@ export function RuntimePoliciesPanel(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function Toggle(props: { checked: boolean; label: string; onChange: (checked: boolean) => void }) {
|
||||
function Toggle(props: { checked: boolean; disabled?: boolean; label: string; onChange: (checked: boolean) => void }) {
|
||||
return (
|
||||
<label className="platformToggle">
|
||||
<input type="checkbox" checked={props.checked} onChange={(event) => props.onChange(event.target.checked)} />
|
||||
<input type="checkbox" checked={props.checked} disabled={props.disabled} onChange={(event) => props.onChange(event.target.checked)} />
|
||||
<span><strong>{props.label}</strong></span>
|
||||
</label>
|
||||
);
|
||||
@@ -404,7 +412,7 @@ function RunnerPolicyEditor(props: {
|
||||
<Label>
|
||||
最大时间(秒)
|
||||
<Input value={props.form.maxDurationSeconds} inputMode="numeric" onChange={(event) => patch({ maxDurationSeconds: event.target.value })} />
|
||||
<span className="runtimeFieldHint">从任务开始执行计时,超过后不再继续重试,默认 600 秒。</span>
|
||||
<span className="runtimeFieldHint">从任务开始执行计时,仅限制是否进入下一个平台,不截断当前平台的重试,默认 600 秒。</span>
|
||||
</Label>
|
||||
<div className="runnerToggleField">
|
||||
<Toggle
|
||||
@@ -412,7 +420,7 @@ function RunnerPolicyEditor(props: {
|
||||
label="启用单一源保护"
|
||||
onChange={(checked) => patch({ singleSourceProtectionEnabled: checked })}
|
||||
/>
|
||||
<span className="runtimeFieldHint">开启后,禁用或冷却当前平台会导致只剩 1 个候选源时,仅继续重试,不自动修改状态。</span>
|
||||
<span className="runtimeFieldHint">开启后,数据库事务会按实时可用来源判断;没有其他来源时跳过冷却或禁用。</span>
|
||||
</div>
|
||||
<div className="spanTwo runnerRuleWorkbench">
|
||||
<aside className="runnerRuleList">
|
||||
@@ -500,7 +508,7 @@ function RunnerPolicyEditor(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function KeywordField(props: { label: string; value: string[]; options?: Array<{ label: string; value: string }>; wide?: boolean; onChange: (value: string[]) => void }) {
|
||||
function KeywordField(props: { disabled?: boolean; label: string; value: string[]; options?: Array<{ label: string; value: string }>; wide?: boolean; onChange: (value: string[]) => void }) {
|
||||
const known = new Set((props.options ?? []).map((item) => item.value));
|
||||
const options = [
|
||||
...(props.options ?? []),
|
||||
@@ -514,6 +522,7 @@ function KeywordField(props: { label: string; value: string[]; options?: Array<{
|
||||
<AntSelect
|
||||
allowClear
|
||||
className="runtimeTagInput"
|
||||
disabled={props.disabled}
|
||||
maxTagCount="responsive"
|
||||
mode="tags"
|
||||
options={options}
|
||||
@@ -611,11 +620,11 @@ function createDefaultForm(policyKey = 'default-runtime-v1'): RuntimePolicyForm
|
||||
retryAllowKeywords: ['rate_limit', 'timeout', 'server_error', 'network', '429', '5xx'],
|
||||
retryDenyKeywords: ['invalid_api_key', 'insufficient_quota', 'billing_not_active', 'permission_denied'],
|
||||
autoDisableEnabled: false,
|
||||
autoDisableThreshold: '3',
|
||||
autoDisableKeywords: ['invalid_api_key', 'account_deactivated', 'permission_denied', 'billing_not_active'],
|
||||
degradeEnabled: true,
|
||||
autoDisableThreshold: '1',
|
||||
autoDisableKeywords: [],
|
||||
degradeEnabled: false,
|
||||
degradeCooldownSeconds: '300',
|
||||
degradeKeywords: ['rate_limit', 'quota', 'timeout', 'temporarily_unavailable', 'overloaded'],
|
||||
degradeKeywords: [],
|
||||
metadataJson: '{}',
|
||||
status: 'active',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user