@@ -1,9 +1,13 @@
import { useState , type FormEvent } from 'react' ;
import { Gauge , Pencil , Plus , RotateCcw , ShieldCheck , Trash2 } from 'lucide-react ';
import type { RuntimePolicySet , RuntimePolicySetUpsertRequest } from '@easyai-ai-gateway/contr acts ';
import { Badge , Button , Card , CardContent , CardHeader , CardTitle , ConfirmDialog , FormDialog , Input , Label , Textarea } from '../../components/ui ' ;
import { useEffect , useState , type FormEvent } from 'react' ;
import { Select as AntSelect } from 'antd ';
import { Gauge , Pencil , Plus , RotateCcw , Route , Save , ShieldCheck , Trash2 } from 'lucide-re act';
import type { GatewayRunnerPolicy , GatewayRunnerPolicyUpsertRequest , 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 RunnerPolicyStrategy = 'failover' | 'hardStop' | 'priorityDemote' ;
type RuntimePolicyForm = {
policyKey : string ;
name : string ;
@@ -13,31 +17,69 @@ type RuntimePolicyForm = {
concurrency : string ;
retryEnabled : boolean ;
retryMaxAttempts : string ;
retryAllowKeywords : string ;
retryDenyKeywords : string ;
retryAllowKeywords : string [ ] ;
retryDenyKeywords : string [ ] ;
autoDisableEnabled : boolean ;
autoDisableThreshold : string ;
autoDisableKeywords : string ;
autoDisableKeywords : string [ ] ;
degradeEnabled : boolean ;
degradeCooldownSeconds : string ;
degradeKeywords : string ;
degradeKeywords : string [ ] ;
metadataJson : string ;
status : string ;
} ;
type RunnerPolicyForm = {
name : string ;
description : string ;
failoverEnabled : boolean ;
maxPlatforms : string ;
maxDurationSeconds : string ;
allowCategories : string [ ] ;
denyCategories : string [ ] ;
allowCodes : string [ ] ;
denyCodes : string [ ] ;
allowKeywords : string [ ] ;
denyKeywords : string [ ] ;
allowStatusCodes : string [ ] ;
denyStatusCodes : string [ ] ;
failoverActions : Record < string , unknown > ;
hardStopEnabled : boolean ;
hardStopCategories : string [ ] ;
hardStopCodes : string [ ] ;
hardStopStatusCodes : string [ ] ;
hardStopKeywords : string [ ] ;
priorityDemoteEnabled : boolean ;
priorityDemoteStep : string ;
priorityDemoteCategories : string [ ] ;
priorityDemoteCodes : string [ ] ;
priorityDemoteStatusCodes : string [ ] ;
priorityDemoteKeywords : string [ ] ;
metadataJson : string ;
status : string ;
} ;
export function RuntimePoliciesPanel ( props : {
message : string ;
runnerPolicy : GatewayRunnerPolicy | null ;
runtimePolicySets : RuntimePolicySet [ ] ;
state : LoadState ;
onDeleteRuntimePolicySet : ( policySetId : string ) = > Promise < void > ;
onSaveRunnerPolicy : ( input : GatewayRunnerPolicyUpsertRequest ) = > Promise < void > ;
onSaveRuntimePolicySet : ( input : RuntimePolicySetUpsertRequest , policySetId? : string ) = > Promise < void > ;
} ) {
const [ activeTab , setActiveTab ] = useState < RuntimePanelTab > ( 'model' ) ;
const [ dialogOpen , setDialogOpen ] = useState ( false ) ;
const [ editingId , setEditingId ] = useState ( '' ) ;
const [ form , setForm ] = useState < RuntimePolicyForm > ( ( ) = > createDefaultForm ( ) ) ;
const [ runnerForm , setRunnerForm ] = useState < RunnerPolicyForm > ( ( ) = > runnerPolicyToForm ( null ) ) ;
const [ localError , setLocalError ] = useState ( '' ) ;
const [ pendingDeletePolicy , setPendingDeletePolicy ] = useState < RuntimePolicySet | null > ( null ) ;
useEffect ( ( ) = > {
setRunnerForm ( runnerPolicyToForm ( props . runnerPolicy ) ) ;
} , [ props . runnerPolicy ? . id , props . runnerPolicy ? . updatedAt ] ) ;
function openCreateDialog() {
setEditingId ( '' ) ;
setLocalError ( '' ) ;
@@ -80,62 +122,91 @@ export function RuntimePoliciesPanel(props: {
}
}
async function submitRunnerPolicy ( event : FormEvent < HTMLFormElement > ) {
event . preventDefault ( ) ;
setLocalError ( '' ) ;
try {
await props . onSaveRunnerPolicy ( runnerFormToPayload ( runnerForm ) ) ;
} catch ( err ) {
setLocalError ( err instanceof Error ? err . message : '全局调度策略保存失败' ) ;
}
}
return (
< div className = "pageStack" >
< Card >
< CardHeader >
< div >
< CardTitle > 运 行 策 略 < / CardTitle >
< p className = "mutedText" > 统 一 维 护 限 流 、 重 试 、 自 动 禁 用 和 优 先 级 降 级 关 键 词 ; 基 准 模 型 可 绑 定 默 认 策 略 , 模 型 可 覆 盖 其 中 某 几 项 。 < / p >
< p className = "mutedText" > 模 型 运 行 策 略 维 护 限 流 、 平 台 内 重 试 、 自 动 禁 用 和 降 级 ; 全 局 调 度 策 略 控 制 平 台 间 切 换 、 硬 拒 绝 和 失 败 平 台 优 先 级 降 级 。 < / p >
< / div >
< Button type = "button" onClick = { openCreateDialog } >
{ activeTab === 'model' && < Button type = "button" onClick = { openCreateDialog } >
< Plus size = { 15 } / >
新 增 策 略
< / Button >
< / Button > }
< / CardHeader >
< CardContent >
{ ( props . message || localError ) && < p className = "formMessage" > { localError || props . message } < / p > }
< Tabs
value = { activeTab }
tabs = { [
{ value : 'model' , label : '模型运行策略' , icon : < ShieldCheck size = { 15 } / > } ,
{ value : 'runner' , label : '全局调度策略' , icon : < Route size = { 15 } / > } ,
] }
onValueChange = { setActiveTab }
/ >
< / CardContent >
< / Card >
< section className = "runtimePolicyGrid" >
{ props . runtimePolicySets . map ( ( policy ) = > (
< article className = "runtimePolicyCard" key = { policy . id } >
< header >
< div className = "iconBox" > < ShieldCheck size = { 18 } / > < / div >
< div >
< strong > { policy . name } < / strong >
< span > { policy . policyKey } < / span >
{ activeTab === 'runner' && (
< RunnerPolicyEditor
form = { runnerForm }
loading = { props . state === 'loading' }
onChange = { setRunnerForm }
onSubmit = { submitRunnerPolicy }
/ >
) }
{ activeTab === 'model' && (
< section className = "runtimePolicyGrid" >
{ props . runtimePolicySets . map ( ( policy ) = > (
< article className = "runtimePolicyCard" key = { policy . id } >
< header >
< div className = "iconBox" > < ShieldCheck size = { 18 } / > < / div >
< div >
< strong > { policy . name } < / strong >
< span > { policy . policyKey } < / span >
< / div >
< Badge variant = { policy . status === 'active' ? 'success' : 'secondary' } > { policy . status } < / Badge >
< / header >
{ policy . description && < p > { policy . description } < / p > }
< div className = "runtimePolicySummary" >
< span > < Gauge size = { 13 } / > { rateLimitSummary ( policy ) } < / span >
< span > { retrySummary ( policy ) } < / span >
< span > { autoDisableSummary ( policy ) } < / span >
< span > { degradeSummary ( policy ) } < / span >
< / div >
< Badge variant = { policy . status === 'active' ? 'success' : 'secondary' } > { policy . status } < / Badge >
< / header >
{ policy . description && < p > { policy . description } < / p > }
< div className = "runtimePolicySummary" >
< span > < Gauge size = { 13 } / > { rateLimitSummary ( policy ) } < / span >
< span > { retrySummary ( policy ) } < / span >
< span > { autoDisableSummary ( policy ) } < / span >
< span > { degradeSummary ( policy ) } < / span >
< / div >
< footer >
< Button type = "button" variant = "outline" size = "sm" onClick = { ( ) = > editPolicy ( policy ) } >
< Pencil size = { 14 } / >
修 改
< / Button >
< Button
type = "button"
variant = "destructive"
size = "sm"
disabled = { isDefaultPolicy ( policy ) }
title = { isDefaultPolicy ( policy ) ? '默认运行策略不能删除' : undefined }
onClick = { ( ) = > setPendingDeletePolicy ( policy ) }
>
< Trash2 size = { 14 } / >
删 除
< / Button >
< / footer >
< / article >
) ) }
< / section >
< footer >
< Button type = "button" variant = "outline" size = "sm" onClick = { ( ) = > editPolicy ( policy ) } >
< Pencil size = { 14 } / >
修 改
< / Button >
< Button
type = "button"
variant = "destructive"
size = "sm"
disabled = { isDefaultPolicy ( policy ) }
title = { isDefaultPolicy ( policy ) ? '默认运行策略不能删除' : undefined }
onClick = { ( ) = > setPendingDeletePolicy ( policy ) }
>
< Trash2 size = { 14 } / >
删 除
< / Button >
< / footer >
< / article >
) ) }
< / section >
) }
< FormDialog
bodyClassName = "runtimePolicyFormBody"
@@ -172,9 +243,9 @@ export function RuntimePoliciesPanel(props: {
< / section >
< section className = "runtimePolicySection spanTwo" >
< header > < strong > 重 试 策 略 < / strong > < span > 允 许 / 拒 绝 关 键 词 控 制 是 否 继 续 尝 试 下 一 个 客 户 端 < / span > < / header >
< header > < strong > 平 台 内 重 试 策 略 < / strong > < span > 允 许 / 拒 绝 关 键 词 控 制 同 一 平 台 是 否 再 次 调 用 < / span > < / header >
< div className = "runtimePolicyRows" >
< Toggle checked = { form . retryEnabled } label = "允许失败 重试" onChange = { ( checked ) = > setForm ( { . . . form , retryEnabled : checked } ) } / >
< Toggle checked = { form . retryEnabled } label = "允许同平台 重试" onChange = { ( checked ) = > setForm ( { . . . form , retryEnabled : checked } ) } / >
< Label > 最 大 尝 试 次 数 < Input value = { form . retryMaxAttempts } inputMode = "numeric" onChange = { ( event ) = > setForm ( { . . . form , retryMaxAttempts : event.target.value } ) } / > < / Label >
< KeywordField label = "允许重试关键词" value = { form . retryAllowKeywords } onChange = { ( value ) = > setForm ( { . . . form , retryAllowKeywords : value } ) } / >
< KeywordField label = "拒绝重试关键词" value = { form . retryDenyKeywords } onChange = { ( value ) = > setForm ( { . . . form , retryDenyKeywords : value } ) } / >
@@ -218,15 +289,244 @@ function Toggle(props: { checked: boolean; label: string; onChange: (checked: bo
) ;
}
function KeywordField ( props : { label : string ; value : string ; onChange : ( value : string ) = > void } ) {
function RunnerPolicyEditor ( props : {
form : RunnerPolicyForm ;
loading : boolean ;
onChange : ( form : RunnerPolicyForm ) = > void ;
onSubmit : ( event : FormEvent < HTMLFormElement > ) = > void ;
} ) {
const [ activeStrategy , setActiveStrategy ] = useState < RunnerPolicyStrategy > ( 'failover' ) ;
const patch = ( next : Partial < RunnerPolicyForm > ) = > props . onChange ( { . . . props . form , . . . next } ) ;
const strategyDefinitions = [
{
value : 'failover' as const ,
title : '平台间故障切换' ,
description : '当前平台内部重试耗尽后是否尝试下一个候选平台' ,
enabled : props.form.failoverEnabled ,
icon : < Route size = { 15 } / > ,
} ,
{
value : 'hardStop' as const ,
title : '硬拒绝规则' ,
description : '参数、余额、权限等错误直接失败,不受模型覆盖影响' ,
enabled : props.form.hardStopEnabled ,
icon : < ShieldCheck size = { 15 } / > ,
} ,
{
value : 'priorityDemote' as const ,
title : '优先级降级' ,
description : '命中后将失败平台的动态优先级调整到当前最后' ,
enabled : props.form.priorityDemoteEnabled ,
icon : < Gauge size = { 15 } / > ,
} ,
] ;
const activeDefinition = strategyDefinitions . find ( ( item ) = > item . value === activeStrategy ) ? ? strategyDefinitions [ 0 ] ;
return (
< Label className = "spanTwo" >
< Card >
< CardHeader className = "runnerPolicyHeader" >
< div className = "runnerPolicyHeaderText" >
< CardTitle > { props . form . name } < / CardTitle >
< p className = "mutedText" > { props . form . description } < / p >
< / div >
< Label className = "runnerPolicyHeaderStatus" >
状 态
< Select value = { props . form . status } onChange = { ( event ) = > patch ( { status : event.target.value } ) } >
< option value = "active" > 启 用 < / option >
< option value = "disabled" > 停 用 < / option >
< / Select >
< / Label >
< / CardHeader >
< CardContent >
< form className = "runtimePolicyFormBody runnerPolicyForm" onSubmit = { props . onSubmit } >
< div className = "capabilityWorkbench runnerPolicyWorkbench spanTwo" >
< aside className = "capabilitySidebar" >
< div className = "capabilitySidebarTitle" >
< Route size = { 15 } / >
< strong > 策 略 列 表 < / strong >
< / div >
< div className = "capabilityList" >
{ strategyDefinitions . map ( ( item ) = > (
< button className = "capabilityListItem" data-active = { item . value === activeStrategy } key = { item . value } type = "button" onClick = { ( ) = > setActiveStrategy ( item . value ) } >
< span >
< strong > { item . title } < / strong >
< small > { item . description } < / small >
< / span >
< Badge variant = { item . enabled ? 'success' : 'secondary' } > { item . enabled ? '启用' : '关闭' } < / Badge >
< / button >
) ) }
< / div >
< / aside >
< div className = "capabilityFormPanel runnerPolicyDetailPanel" >
< header >
{ activeDefinition . icon }
< div >
< strong > { activeDefinition . title } < / strong >
< small > { activeDefinition . description } < / small >
< / div >
< span > { activeDefinition . enabled ? '启用' : '关闭' } < / span >
< / header >
{ activeStrategy === 'failover' && (
< div className = "runtimePolicyRows runnerPolicyDetailRows" >
< Toggle checked = { props . form . failoverEnabled } label = "启用平台间切换" onChange = { ( checked ) = > patch ( { failoverEnabled : checked } ) } / >
< Label >
最 大 平 台 数
< Input value = { props . form . maxPlatforms } inputMode = "numeric" onChange = { ( event ) = > patch ( { maxPlatforms : event.target.value } ) } / >
< span className = "runtimeFieldHint" > 本 次 任 务 最 多 尝 试 的 候 选 平 台 数 量 , 默 认 99 。 < / span >
< / Label >
< Label >
最 大 时 间 ( 秒 )
< Input value = { props . form . maxDurationSeconds } inputMode = "numeric" onChange = { ( event ) = > patch ( { maxDurationSeconds : event.target.value } ) } / >
< span className = "runtimeFieldHint" > 从 任 务 开 始 执 行 计 时 , 超 过 后 不 再 继 续 重 试 , 默 认 600 秒 。 < / span >
< / Label >
< KeywordField label = "允许分类" value = { props . form . allowCategories } onChange = { ( value ) = > patch ( { allowCategories : value } ) } / >
< KeywordField label = "拒绝分类" value = { props . form . denyCategories } onChange = { ( value ) = > patch ( { denyCategories : value } ) } / >
< KeywordField label = "允许错误码" value = { props . form . allowCodes } onChange = { ( value ) = > patch ( { allowCodes : value } ) } / >
< KeywordField label = "拒绝错误码" value = { props . form . denyCodes } onChange = { ( value ) = > patch ( { denyCodes : value } ) } / >
< KeywordField label = "允许关键词" value = { props . form . allowKeywords } onChange = { ( value ) = > patch ( { allowKeywords : value } ) } / >
< KeywordField label = "拒绝关键词" value = { props . form . denyKeywords } onChange = { ( value ) = > patch ( { denyKeywords : value } ) } / >
< KeywordField label = "允许状态码" value = { props . form . allowStatusCodes } onChange = { ( value ) = > patch ( { allowStatusCodes : value } ) } / >
< KeywordField label = "拒绝状态码" value = { props . form . denyStatusCodes } onChange = { ( value ) = > patch ( { denyStatusCodes : value } ) } / >
< / div >
) }
{ activeStrategy === 'hardStop' && (
< div className = "runtimePolicyRows runnerPolicyDetailRows" >
< Toggle checked = { props . form . hardStopEnabled } label = "启用硬拒绝" onChange = { ( checked ) = > patch ( { hardStopEnabled : checked } ) } / >
< KeywordField label = "硬拒绝分类" value = { props . form . hardStopCategories } onChange = { ( value ) = > patch ( { hardStopCategories : value } ) } / >
< KeywordField label = "硬拒绝错误码" value = { props . form . hardStopCodes } onChange = { ( value ) = > patch ( { hardStopCodes : value } ) } / >
< KeywordField label = "硬拒绝状态码" value = { props . form . hardStopStatusCodes } onChange = { ( value ) = > patch ( { hardStopStatusCodes : value } ) } / >
< span className = "runtimeFieldHint spanTwo" > 建 议 只 用 错 误 码 、 分 类 和 关 键 词 做 硬 拒 绝 ; 401 / 403 这 类 平 台 鉴 权 错 误 默 认 走 平 台 间 切 换 。 < / span >
< KeywordField label = "硬拒绝关键词" value = { props . form . hardStopKeywords } onChange = { ( value ) = > patch ( { hardStopKeywords : value } ) } / >
< / div >
) }
{ activeStrategy === 'priorityDemote' && (
< div className = "runtimePolicyRows runnerPolicyDetailRows" >
< Toggle checked = { props . form . priorityDemoteEnabled } label = "启用优先级降级" onChange = { ( checked ) = > patch ( { priorityDemoteEnabled : checked } ) } / >
< Label >
降 级 步 长
< Input value = { props . form . priorityDemoteStep } inputMode = "numeric" onChange = { ( event ) = > patch ( { priorityDemoteStep : event.target.value } ) } / >
< span className = "runtimeFieldHint" > 命 中 降 级 规 则 后 , 失 败 平 台 动 态 优 先 级 向 后 调 整 的 幅 度 , 默 认 100 。 < / span >
< / Label >
< KeywordField label = "降级分类" value = { props . form . priorityDemoteCategories } onChange = { ( value ) = > patch ( { priorityDemoteCategories : value } ) } / >
< KeywordField label = "降级错误码" value = { props . form . priorityDemoteCodes } onChange = { ( value ) = > patch ( { priorityDemoteCodes : value } ) } / >
< KeywordField label = "降级状态码" value = { props . form . priorityDemoteStatusCodes } onChange = { ( value ) = > patch ( { priorityDemoteStatusCodes : value } ) } / >
< KeywordField label = "降级关键词" value = { props . form . priorityDemoteKeywords } onChange = { ( value ) = > patch ( { priorityDemoteKeywords : value } ) } / >
< / div >
) }
< / div >
< / div >
< Label className = "spanTwo" > 元 数 据 JSON < Textarea value = { props . form . metadataJson } rows = { 4 } onChange = { ( event ) = > patch ( { metadataJson : event.target.value } ) } / > < / Label >
< div className = "runtimePolicyActions spanTwo" >
< Button type = "submit" disabled = { props . loading } >
< Save size = { 15 } / >
保 存 全 局 调 度 策 略
< / Button >
< / div >
< / form >
< / CardContent >
< / Card >
) ;
}
function KeywordField ( props : { label : string ; value : string [ ] ; onChange : ( value : string [ ] ) = > void } ) {
const options = props . value . map ( ( item ) = > ( { label : item , value : item } ) ) ;
return (
< Label className = "spanTwo runtimeTagField" >
{ props . label }
< Input value = { props . value } placeholder = "多个关键词用逗号或换行分隔" onChange = { ( event ) = > props . onChange ( event . target . value ) } / >
< AntSelect
allowClear
className = "runtimeTagInput"
maxTagCount = "responsive"
mode = "tags"
options = { options }
placeholder = "输入后回车生成标签"
tokenSeparators = { [ ',' , '\n' ] }
value = { props . value }
onChange = { ( value ) = > props . onChange ( cleanTags ( value ) ) }
/ >
< / Label >
) ;
}
function runnerPolicyToForm ( policy : GatewayRunnerPolicy | null ) : RunnerPolicyForm {
const failover = readObject ( policy ? . failoverPolicy ) ;
const hardStop = readObject ( policy ? . hardStopPolicy ) ;
const priorityDemote = readObject ( policy ? . priorityDemotePolicy ) ;
return {
name : policy?.name ? ? '默认全局调度策略' ,
description : policy?.description ? ? '控制多个候选平台之间的故障切换;模型运行策略只可覆盖 failoverPolicy,不能覆盖 hardStopPolicy。' ,
failoverEnabled : readBool ( failover . enabled , true ) ,
maxPlatforms : String ( readNumber ( failover . maxPlatforms , 99 ) ) ,
maxDurationSeconds : String ( readNumber ( failover . maxDurationSeconds , 600 ) ) ,
allowCategories : tagsFromValue ( failover . allowCategories ? ? [ 'network' , 'timeout' , 'stream_error' , 'rate_limit' , 'provider_5xx' , 'provider_overloaded' , 'auth_error' ] ) ,
denyCategories : tagsFromValue ( failover . denyCategories ? ? [ 'request_error' , 'unsupported_model' , 'user_permission' , 'insufficient_balance' ] ) ,
allowCodes : tagsFromValue ( failover . allowCodes ? ? [ 'auth_failed' , 'invalid_api_key' , 'missing_credentials' ] ) ,
denyCodes : tagsFromValue ( failover . denyCodes ? ? [ ] ) ,
allowKeywords : tagsFromValue ( failover . allowKeywords ? ? [ 'timeout' , 'network' , 'rate_limit' , 'overloaded' , 'temporarily_unavailable' , 'server_error' , 'auth_failed' , 'invalid_api_key' , 'missing_credentials' , 'unauthorized' , 'forbidden' , '429' , '5xx' ] ) ,
denyKeywords : tagsFromValue ( failover . denyKeywords ? ? [ 'invalid_parameter' , 'missing required' , 'bad request' ] ) ,
allowStatusCodes : tagsFromValue ( failover . allowStatusCodes ? ? [ 401 , 403 , 408 , 429 , 500 , 502 , 503 , 504 ] ) ,
denyStatusCodes : tagsFromValue ( failover . denyStatusCodes ? ? [ ] ) ,
failoverActions : Object.keys ( readObject ( failover . actions ) ) . length > 0 ? readObject ( failover . actions ) : defaultFailoverActions ( ) ,
hardStopEnabled : readBool ( hardStop . enabled , true ) ,
hardStopCategories : tagsFromValue ( hardStop . categories ? ? [ 'request_error' , 'unsupported_model' , 'user_permission' , 'insufficient_balance' ] ) ,
hardStopCodes : tagsFromValue ( hardStop . codes ? ? [ 'bad_request' , 'invalid_request' , 'invalid_parameter' , 'missing_required' , 'unsupported_kind' , 'unsupported_model' , 'insufficient_balance' , 'permission_denied' ] ) ,
hardStopStatusCodes : tagsFromValue ( hardStop . statusCodes ? ? [ ] ) ,
hardStopKeywords : tagsFromValue ( hardStop . keywords ? ? [ 'invalid_parameter' , 'missing required' , 'bad request' , 'insufficient balance' ] ) ,
priorityDemoteEnabled : readBool ( priorityDemote . enabled , true ) ,
priorityDemoteStep : String ( readNumber ( priorityDemote . demoteStep , 100 ) ) ,
priorityDemoteCategories : tagsFromValue ( priorityDemote . categories ? ? [ 'network' , 'timeout' , 'stream_error' , 'rate_limit' , 'provider_5xx' , 'provider_overloaded' ] ) ,
priorityDemoteCodes : tagsFromValue ( priorityDemote . codes ? ? [ 'network' , 'timeout' , 'stream_read_error' , 'rate_limit' , 'server_error' , 'overloaded' ] ) ,
priorityDemoteStatusCodes : tagsFromValue ( priorityDemote . statusCodes ? ? [ 408 , 429 , 500 , 502 , 503 , 504 ] ) ,
priorityDemoteKeywords : tagsFromValue ( priorityDemote . keywords ? ? [ 'timeout' , 'network' , 'rate_limit' , 'overloaded' , 'temporarily_unavailable' , 'server_error' , '429' , '5xx' ] ) ,
metadataJson : JSON.stringify ( policy ? . metadata ? ? { } , null , 2 ) ,
status : policy?.status ? ? 'active' ,
} ;
}
function runnerFormToPayload ( form : RunnerPolicyForm ) : GatewayRunnerPolicyUpsertRequest {
return {
policyKey : 'default-runner-v1' ,
name : form.name.trim ( ) || '默认全局调度策略' ,
description : form.description.trim ( ) || undefined ,
failoverPolicy : {
enabled : form.failoverEnabled ,
maxPlatforms : positiveInt ( form . maxPlatforms , 99 ) ,
maxDurationSeconds : positiveInt ( form . maxDurationSeconds , 600 ) ,
allowCategories : cleanTags ( form . allowCategories ) ,
denyCategories : cleanTags ( form . denyCategories ) ,
allowCodes : cleanTags ( form . allowCodes ) ,
denyCodes : cleanTags ( form . denyCodes ) ,
allowKeywords : cleanTags ( form . allowKeywords ) ,
denyKeywords : cleanTags ( form . denyKeywords ) ,
allowStatusCodes : parseNumberTags ( form . allowStatusCodes ) ,
denyStatusCodes : parseNumberTags ( form . denyStatusCodes ) ,
actions : Object.keys ( form . failoverActions ) . length > 0 ? form.failoverActions : defaultFailoverActions ( ) ,
} ,
hardStopPolicy : {
enabled : form.hardStopEnabled ,
categories : cleanTags ( form . hardStopCategories ) ,
codes : cleanTags ( form . hardStopCodes ) ,
statusCodes : parseNumberTags ( form . hardStopStatusCodes ) ,
keywords : cleanTags ( form . hardStopKeywords ) ,
} ,
priorityDemotePolicy : {
enabled : form.priorityDemoteEnabled ,
demoteStep : positiveInt ( form . priorityDemoteStep , 100 ) ,
categories : cleanTags ( form . priorityDemoteCategories ) ,
codes : cleanTags ( form . priorityDemoteCodes ) ,
statusCodes : parseNumberTags ( form . priorityDemoteStatusCodes ) ,
keywords : cleanTags ( form . priorityDemoteKeywords ) ,
} ,
metadata : parseJson ( form . metadataJson ) ,
status : form.status.trim ( ) || 'active' ,
} ;
}
function createDefaultForm ( policyKey = 'default-runtime-v1' ) : RuntimePolicyForm {
return {
policyKey ,
@@ -237,19 +537,28 @@ function createDefaultForm(policyKey = 'default-runtime-v1'): RuntimePolicyForm
concurrency : '6' ,
retryEnabled : true ,
retryMaxAttempts : '2' ,
retryAllowKeywords : 'rate_limit, timeout, server_error, network, 429, 5xx' ,
retryDenyKeywords : 'invalid_api_key, insufficient_quota, billing_not_active, permission_denied' ,
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' ,
autoDisableKeywords : [ 'invalid_api_key' , ' account_deactivated' , ' permission_denied' , ' billing_not_active'] ,
degradeEnabled : true ,
degradeCooldownSeconds : '300' ,
degradeKeywords : 'rate_limit, quota, timeout, temporarily_unavailable, overloaded' ,
degradeKeywords : [ 'rate_limit' , 'quota' , 'timeout' , ' temporarily_unavailable' , ' overloaded'] ,
metadataJson : '{}' ,
status : 'active' ,
} ;
}
function defaultFailoverActions ( ) : Record < string , unknown > {
return {
auth_error : 'disable_and_next' ,
rate_limit : 'cooldown_and_next' ,
provider_5xx : 'next' ,
request_error : 'stop' ,
} ;
}
function policyToForm ( policy : RuntimePolicySet ) : RuntimePolicyForm {
const rateRules = Array . isArray ( policy . rateLimitPolicy ? . rules ) ? policy . rateLimitPolicy . rules : [ ] ;
const retry = readObject ( policy . retryPolicy ) ;
@@ -264,14 +573,14 @@ function policyToForm(policy: RuntimePolicySet): RuntimePolicyForm {
concurrency : String ( readRateLimit ( rateRules , 'concurrent' ) || '' ) ,
retryEnabled : readBool ( retry . enabled , true ) ,
retryMaxAttempts : String ( readNumber ( retry . maxAttempts , 2 ) ) ,
retryAllowKeywords : stringifyKeywords ( retry . allowKeywords ) ,
retryDenyKeywords : stringifyKeywords ( retry . denyKeywords ) ,
retryAllowKeywords : tagsFromValue ( retry . allowKeywords ) ,
retryDenyKeywords : tagsFromValue ( retry . denyKeywords ) ,
autoDisableEnabled : readBool ( disable . enabled , false ) ,
autoDisableThreshold : String ( readNumber ( disable . threshold , 3 ) ) ,
autoDisableKeywords : stringifyKeywords ( disable . keywords ) ,
autoDisableKeywords : tagsFromValue ( disable . keywords ) ,
degradeEnabled : readBool ( degrade . enabled , true ) ,
degradeCooldownSeconds : String ( readNumber ( degrade . cooldownSeconds , 300 ) ) ,
degradeKeywords : stringifyKeywords ( degrade . keywords ) ,
degradeKeywords : tagsFromValue ( degrade . keywords ) ,
metadataJson : JSON.stringify ( policy . metadata ? ? { } , null , 2 ) ,
status : policy.status || 'active' ,
} ;
@@ -286,18 +595,18 @@ function formToPayload(form: RuntimePolicyForm): RuntimePolicySetUpsertRequest {
retryPolicy : {
enabled : form.retryEnabled ,
maxAttempts : positiveInt ( form . retryMaxAttempts , 2 ) ,
allowKeywords : parseKeywords ( form . retryAllowKeywords ) ,
denyKeywords : parseKeywords ( form . retryDenyKeywords ) ,
allowKeywords : cleanTags ( form . retryAllowKeywords ) ,
denyKeywords : cleanTags ( form . retryDenyKeywords ) ,
} ,
autoDisablePolicy : {
enabled : form.autoDisableEnabled ,
threshold : positiveInt ( form . autoDisableThreshold , 3 ) ,
keywords : parseKeywords ( form . autoDisableKeywords ) ,
keywords : cleanTags ( form . autoDisableKeywords ) ,
} ,
degradePolicy : {
enabled : form.degradeEnabled ,
cooldownSeconds : positiveInt ( form . degradeCooldownSeconds , 300 ) ,
keywords : parseKeywords ( form . degradeKeywords ) ,
keywords : cleanTags ( form . degradeKeywords ) ,
} ,
metadata : parseJson ( form . metadataJson ) ,
status : form.status.trim ( ) || 'active' ,
@@ -332,7 +641,7 @@ function rateLimitSummary(policy: RuntimePolicySet) {
function retrySummary ( policy : RuntimePolicySet ) {
const retry = readObject ( policy . retryPolicy ) ;
return readBool ( retry . enabled , false ) ? ` 重 试 ${ readNumber ( retry . maxAttempts , 2 ) } 次 ` : '不重试' ;
return readBool ( retry . enabled , false ) ? ` 同平台尝 试 ${ readNumber ( retry . maxAttempts , 2 ) } 次 ` : '同平台 不重试' ;
}
function autoDisableSummary ( policy : RuntimePolicySet ) {
@@ -350,12 +659,31 @@ function readRateLimit(rules: unknown[], metric: string) {
return readNumber ( readObject ( rule ) . limit , 0 ) ;
}
function parseKeywords ( value : string ) {
return value . split ( /[,\n]/ ) . map ( ( item ) = > item . trim ( ) ) . filter ( Boolean ) ;
function stringifyKeywords ( value : unknown ) {
return tagsFromValue ( value ) . join ( ', ' ) ;
}
function stringifyKeywords ( value : unknown ) {
return Array . isArray ( value ) ? value . map ( String ) . join ( ', ' ) : '' ;
function tagsFromValue ( value : unknown ) {
if ( Array . isArray ( value ) ) return cleanTags ( value . map ( ( item ) = > String ( item ) ) ) ;
return typeof value === 'string' ? cleanTags ( [ value ] ) : [ ] ;
}
function cleanTags ( value : string [ ] ) {
const tags : string [ ] = [ ] ;
const seen = new Set < string > ( ) ;
for ( const raw of value ) {
for ( const item of raw . split ( /[,\n]/ ) ) {
const tag = item . trim ( ) ;
if ( ! tag || seen . has ( tag ) ) continue ;
seen . add ( tag ) ;
tags . push ( tag ) ;
}
}
return tags ;
}
function parseNumberTags ( value : string [ ] ) {
return cleanTags ( value ) . map ( ( item ) = > Number . parseInt ( item , 10 ) ) . filter ( ( item ) = > Number . isFinite ( item ) && item > 0 ) ;
}
function positiveInt ( value : string , fallback : number ) {