Skip to content

Commit a436c53

Browse files
committed
feat: ✨ 熔断器配置
resolve #168 resolve #157 resolve #118 resolve #177
1 parent da5fdae commit a436c53

File tree

6 files changed

+182
-0
lines changed

6 files changed

+182
-0
lines changed

web/public/locale/en.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@
127127
"hint": "Empty = deny all, * = allow all",
128128
"example": "Only allow specific domains: https://example.com,https://example2.com"
129129
},
130+
"circuitBreaker": {
131+
"title": "Circuit Breaker",
132+
"threshold": {
133+
"label": "Failure Threshold (consecutive failures)",
134+
"placeholder": "Enter threshold"
135+
},
136+
"cooldown": {
137+
"label": "Base Cooldown (seconds)",
138+
"placeholder": "Enter cooldown in seconds"
139+
},
140+
"maxCooldown": {
141+
"label": "Max Cooldown (seconds)",
142+
"placeholder": "Enter max cooldown in seconds"
143+
},
144+
"hint": "When a channel fails consecutively up to the threshold, circuit breaker trips. Cooldown grows exponentially up to the max cooldown"
145+
},
130146
"saved": "Saved",
131147
"backup": {
132148
"title": "Backup / Restore",

web/public/locale/zh_hans.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@
127127
"hint": "为空禁止跨域,* 允许所有",
128128
"example": "只允许指定域名: https://example.com,https://example2.com"
129129
},
130+
"circuitBreaker": {
131+
"title": "熔断器配置",
132+
"threshold": {
133+
"label": "熔断触发阈值(连续失败次数)",
134+
"placeholder": "请输入阈值"
135+
},
136+
"cooldown": {
137+
"label": "基础冷却时间(秒)",
138+
"placeholder": "请输入冷却时间"
139+
},
140+
"maxCooldown": {
141+
"label": "最大冷却时间(秒)",
142+
"placeholder": "请输入最大冷却时间"
143+
},
144+
"hint": "当渠道连续失败达到阈值后触发熔断,冷却时间按指数退避增长,上限为最大冷却时间"
145+
},
130146
"saved": "已保存",
131147
"backup": {
132148
"title": "备份 / 恢复",

web/public/locale/zh_hant.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,22 @@
127127
"hint": "為空禁止跨網域,* 允許所有",
128128
"example": "只允許指定域名: https://example.com,https://example2.com"
129129
},
130+
"circuitBreaker": {
131+
"title": "熔斷器配置",
132+
"threshold": {
133+
"label": "熔斷觸發閾值(連續失敗次數)",
134+
"placeholder": "請輸入閾值"
135+
},
136+
"cooldown": {
137+
"label": "基礎冷卻時間(秒)",
138+
"placeholder": "請輸入冷卻時間"
139+
},
140+
"maxCooldown": {
141+
"label": "最大冷卻時間(秒)",
142+
"placeholder": "請輸入最大冷卻時間"
143+
},
144+
"hint": "當渠道連續失敗達到閾值後觸發熔斷,冷卻時間按指數退避增長,上限為最大冷卻時間"
145+
},
130146
"saved": "已儲存",
131147
"backup": {
132148
"title": "備份 / 復原",

web/src/api/endpoints/setting.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ export const SettingKey = {
1919
RelayLogKeepEnabled: 'relay_log_keep_enabled',
2020
RelayLogKeepPeriod: 'relay_log_keep_period',
2121
CORSAllowOrigins: 'cors_allow_origins',
22+
CircuitBreakerThreshold: 'circuit_breaker_threshold',
23+
CircuitBreakerCooldown: 'circuit_breaker_cooldown',
24+
CircuitBreakerMaxCooldown: 'circuit_breaker_max_cooldown',
2225
} as const;
2326

2427
/**
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
'use client';
2+
3+
import { useEffect, useState, useRef } from 'react';
4+
import { useTranslations } from 'next-intl';
5+
import { Zap, Hash, Timer, TimerOff, HelpCircle } from 'lucide-react';
6+
import { Input } from '@/components/ui/input';
7+
import { useSettingList, useSetSetting, SettingKey } from '@/api/endpoints/setting';
8+
import { toast } from '@/components/common/Toast';
9+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/animate-ui/components/animate/tooltip';
10+
11+
export function SettingCircuitBreaker() {
12+
const t = useTranslations('setting');
13+
const { data: settings } = useSettingList();
14+
const setSetting = useSetSetting();
15+
16+
const [threshold, setThreshold] = useState('');
17+
const [cooldown, setCooldown] = useState('');
18+
const [maxCooldown, setMaxCooldown] = useState('');
19+
20+
const initialThreshold = useRef('');
21+
const initialCooldown = useRef('');
22+
const initialMaxCooldown = useRef('');
23+
24+
useEffect(() => {
25+
if (settings) {
26+
const th = settings.find(s => s.key === SettingKey.CircuitBreakerThreshold);
27+
const cd = settings.find(s => s.key === SettingKey.CircuitBreakerCooldown);
28+
const mcd = settings.find(s => s.key === SettingKey.CircuitBreakerMaxCooldown);
29+
if (th) {
30+
queueMicrotask(() => setThreshold(th.value));
31+
initialThreshold.current = th.value;
32+
}
33+
if (cd) {
34+
queueMicrotask(() => setCooldown(cd.value));
35+
initialCooldown.current = cd.value;
36+
}
37+
if (mcd) {
38+
queueMicrotask(() => setMaxCooldown(mcd.value));
39+
initialMaxCooldown.current = mcd.value;
40+
}
41+
}
42+
}, [settings]);
43+
44+
const handleSave = (key: string, value: string, initialValue: string) => {
45+
if (value === initialValue) return;
46+
47+
setSetting.mutate({ key, value }, {
48+
onSuccess: () => {
49+
toast.success(t('saved'));
50+
if (key === SettingKey.CircuitBreakerThreshold) {
51+
initialThreshold.current = value;
52+
} else if (key === SettingKey.CircuitBreakerCooldown) {
53+
initialCooldown.current = value;
54+
} else if (key === SettingKey.CircuitBreakerMaxCooldown) {
55+
initialMaxCooldown.current = value;
56+
}
57+
}
58+
});
59+
};
60+
61+
return (
62+
<div className="rounded-3xl border border-border bg-card p-6 custom-shadow space-y-5">
63+
<h2 className="text-lg font-bold text-card-foreground flex items-center gap-2">
64+
<Zap className="h-5 w-5" />
65+
{t('circuitBreaker.title')}
66+
<TooltipProvider>
67+
<Tooltip>
68+
<TooltipTrigger asChild>
69+
<HelpCircle className="size-4 text-muted-foreground cursor-help" />
70+
</TooltipTrigger>
71+
<TooltipContent>
72+
{t('circuitBreaker.hint')}
73+
</TooltipContent>
74+
</Tooltip>
75+
</TooltipProvider>
76+
</h2>
77+
78+
{/* 熔断触发阈值 */}
79+
<div className="flex items-center justify-between gap-4">
80+
<div className="flex items-center gap-3">
81+
<Hash className="h-5 w-5 text-muted-foreground" />
82+
<span className="text-sm font-medium">{t('circuitBreaker.threshold.label')}</span>
83+
</div>
84+
<Input
85+
type="number"
86+
value={threshold}
87+
onChange={(e) => setThreshold(e.target.value)}
88+
onBlur={() => handleSave(SettingKey.CircuitBreakerThreshold, threshold, initialThreshold.current)}
89+
placeholder={t('circuitBreaker.threshold.placeholder')}
90+
className="w-48 rounded-xl"
91+
/>
92+
</div>
93+
94+
{/* 基础冷却时间 */}
95+
<div className="flex items-center justify-between gap-4">
96+
<div className="flex items-center gap-3">
97+
<Timer className="h-5 w-5 text-muted-foreground" />
98+
<span className="text-sm font-medium">{t('circuitBreaker.cooldown.label')}</span>
99+
</div>
100+
<Input
101+
type="number"
102+
value={cooldown}
103+
onChange={(e) => setCooldown(e.target.value)}
104+
onBlur={() => handleSave(SettingKey.CircuitBreakerCooldown, cooldown, initialCooldown.current)}
105+
placeholder={t('circuitBreaker.cooldown.placeholder')}
106+
className="w-48 rounded-xl"
107+
/>
108+
</div>
109+
110+
{/* 最大冷却时间 */}
111+
<div className="flex items-center justify-between gap-4">
112+
<div className="flex items-center gap-3">
113+
<TimerOff className="h-5 w-5 text-muted-foreground" />
114+
<span className="text-sm font-medium">{t('circuitBreaker.maxCooldown.label')}</span>
115+
</div>
116+
<Input
117+
type="number"
118+
value={maxCooldown}
119+
onChange={(e) => setMaxCooldown(e.target.value)}
120+
onBlur={() => handleSave(SettingKey.CircuitBreakerMaxCooldown, maxCooldown, initialMaxCooldown.current)}
121+
placeholder={t('circuitBreaker.maxCooldown.placeholder')}
122+
className="w-48 rounded-xl"
123+
/>
124+
</div>
125+
</div>
126+
);
127+
}

web/src/components/modules/setting/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { SettingInfo } from './Info';
1010
import { SettingLLMSync } from './LLMSync';
1111
import { SettingLog } from './Log';
1212
import { SettingBackup } from './Backup';
13+
import { SettingCircuitBreaker } from './CircuitBreaker';
1314

1415
export function Setting() {
1516
return (
@@ -38,6 +39,9 @@ export function Setting() {
3839
<div>
3940
<SettingLLMSync key="setting-llmsync" />
4041
</div>
42+
<div>
43+
<SettingCircuitBreaker key="setting-circuit-breaker" />
44+
</div>
4145
<div>
4246
<SettingBackup key="setting-backup" />
4347
</div>

0 commit comments

Comments
 (0)