mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 04:40:59 +00:00
feat: guard new 504/524 status remaps with risk confirmation
This commit is contained in:
@@ -614,7 +614,7 @@ func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *dto.TaskError,
|
|||||||
}
|
}
|
||||||
if taskErr.StatusCode/100 == 5 {
|
if taskErr.StatusCode/100 == 5 {
|
||||||
// 超时不重试
|
// 超时不重试
|
||||||
if taskErr.StatusCode == 504 || taskErr.StatusCode == 524 {
|
if operation_setting.IsAlwaysSkipRetryStatusCode(taskErr.StatusCode) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ var AutomaticRetryStatusCodeRanges = []StatusCodeRange{
|
|||||||
{Start: 525, End: 599},
|
{Start: 525, End: 599},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var alwaysSkipRetryStatusCodes = map[int]struct{}{
|
||||||
|
504: {},
|
||||||
|
524: {},
|
||||||
|
}
|
||||||
|
|
||||||
func AutomaticDisableStatusCodesToString() string {
|
func AutomaticDisableStatusCodesToString() string {
|
||||||
return statusCodeRangesToString(AutomaticDisableStatusCodeRanges)
|
return statusCodeRangesToString(AutomaticDisableStatusCodeRanges)
|
||||||
}
|
}
|
||||||
@@ -56,7 +61,15 @@ func AutomaticRetryStatusCodesFromString(s string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsAlwaysSkipRetryStatusCode(code int) bool {
|
||||||
|
_, exists := alwaysSkipRetryStatusCodes[code]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
func ShouldRetryByStatusCode(code int) bool {
|
func ShouldRetryByStatusCode(code int) bool {
|
||||||
|
if IsAlwaysSkipRetryStatusCode(code) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return shouldMatchStatusCodeRanges(AutomaticRetryStatusCodeRanges, code)
|
return shouldMatchStatusCodeRanges(AutomaticRetryStatusCodeRanges, code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ func TestShouldRetryByStatusCode(t *testing.T) {
|
|||||||
|
|
||||||
require.True(t, ShouldRetryByStatusCode(429))
|
require.True(t, ShouldRetryByStatusCode(429))
|
||||||
require.True(t, ShouldRetryByStatusCode(500))
|
require.True(t, ShouldRetryByStatusCode(500))
|
||||||
|
require.False(t, ShouldRetryByStatusCode(504))
|
||||||
|
require.False(t, ShouldRetryByStatusCode(524))
|
||||||
require.False(t, ShouldRetryByStatusCode(400))
|
require.False(t, ShouldRetryByStatusCode(400))
|
||||||
require.False(t, ShouldRetryByStatusCode(200))
|
require.False(t, ShouldRetryByStatusCode(200))
|
||||||
}
|
}
|
||||||
@@ -77,3 +79,9 @@ func TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior(t *testing.T) {
|
|||||||
require.False(t, ShouldRetryByStatusCode(524))
|
require.False(t, ShouldRetryByStatusCode(524))
|
||||||
require.True(t, ShouldRetryByStatusCode(599))
|
require.True(t, ShouldRetryByStatusCode(599))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsAlwaysSkipRetryStatusCode(t *testing.T) {
|
||||||
|
require.True(t, IsAlwaysSkipRetryStatusCode(504))
|
||||||
|
require.True(t, IsAlwaysSkipRetryStatusCode(524))
|
||||||
|
require.False(t, IsAlwaysSkipRetryStatusCode(500))
|
||||||
|
}
|
||||||
|
|||||||
165
web/src/components/common/modals/RiskAcknowledgementModal.jsx
Normal file
165
web/src/components/common/modals/RiskAcknowledgementModal.jsx
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
/*
|
||||||
|
Copyright (C) 2025 QuantumNous
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as
|
||||||
|
published by the Free Software Foundation, either version 3 of the
|
||||||
|
License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Checkbox,
|
||||||
|
Input,
|
||||||
|
Space,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import { IconAlertTriangle } from '@douyinfe/semi-icons';
|
||||||
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
|
import MarkdownRenderer from '../markdown/MarkdownRenderer';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
const RiskAcknowledgementModal = ({
|
||||||
|
visible,
|
||||||
|
title,
|
||||||
|
markdownContent = '',
|
||||||
|
detailTitle = '',
|
||||||
|
detailItems = [],
|
||||||
|
checklist = [],
|
||||||
|
inputPrompt = '',
|
||||||
|
requiredText = '',
|
||||||
|
inputPlaceholder = '',
|
||||||
|
mismatchText = '',
|
||||||
|
cancelText = '',
|
||||||
|
confirmText = '',
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [checkedItems, setCheckedItems] = useState([]);
|
||||||
|
const [typedText, setTypedText] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return;
|
||||||
|
setCheckedItems(Array(checklist.length).fill(false));
|
||||||
|
setTypedText('');
|
||||||
|
}, [visible, checklist.length]);
|
||||||
|
|
||||||
|
const allChecked = useMemo(() => {
|
||||||
|
if (checklist.length === 0) return true;
|
||||||
|
return checkedItems.length === checklist.length && checkedItems.every(Boolean);
|
||||||
|
}, [checkedItems, checklist.length]);
|
||||||
|
|
||||||
|
const typedMatched = useMemo(() => {
|
||||||
|
if (!requiredText) return true;
|
||||||
|
return typedText.trim() === requiredText.trim();
|
||||||
|
}, [typedText, requiredText]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
title={
|
||||||
|
<Space align='center'>
|
||||||
|
<IconAlertTriangle style={{ color: 'var(--semi-color-warning)' }} />
|
||||||
|
<span>{title}</span>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
width={isMobile ? '100%' : 860}
|
||||||
|
centered
|
||||||
|
maskClosable={false}
|
||||||
|
closeOnEsc={false}
|
||||||
|
onCancel={onCancel}
|
||||||
|
bodyStyle={{
|
||||||
|
maxHeight: isMobile ? '70vh' : '72vh',
|
||||||
|
overflowY: 'auto',
|
||||||
|
padding: isMobile ? '12px 16px' : '16px 20px',
|
||||||
|
}}
|
||||||
|
footer={
|
||||||
|
<Space>
|
||||||
|
<Button onClick={onCancel}>{cancelText}</Button>
|
||||||
|
<Button
|
||||||
|
theme='solid'
|
||||||
|
type='danger'
|
||||||
|
disabled={!allChecked || !typedMatched}
|
||||||
|
onClick={onConfirm}
|
||||||
|
>
|
||||||
|
{confirmText}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className='flex flex-col gap-4'>
|
||||||
|
{markdownContent ? (
|
||||||
|
<div className='border border-warning-200 bg-warning-50 rounded-md px-3 py-2'>
|
||||||
|
<MarkdownRenderer content={markdownContent} />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{detailItems.length > 0 ? (
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{detailTitle ? <Text strong>{detailTitle}</Text> : null}
|
||||||
|
<div className='font-mono text-xs break-all bg-orange-50 border border-orange-200 rounded-md p-2'>
|
||||||
|
{detailItems.join(', ')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{checklist.length > 0 ? (
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{checklist.map((item, index) => (
|
||||||
|
<Checkbox
|
||||||
|
key={`risk-check-${index}`}
|
||||||
|
checked={!!checkedItems[index]}
|
||||||
|
onChange={(event) => {
|
||||||
|
const next = [...checkedItems];
|
||||||
|
next[index] = event.target.checked;
|
||||||
|
setCheckedItems(next);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Checkbox>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{requiredText ? (
|
||||||
|
<div className='flex flex-col gap-2'>
|
||||||
|
{inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
|
||||||
|
<div className='font-mono text-xs break-all bg-gray-50 border border-gray-200 rounded-md p-2'>
|
||||||
|
{requiredText}
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
value={typedText}
|
||||||
|
onChange={setTypedText}
|
||||||
|
placeholder={inputPlaceholder}
|
||||||
|
onCopy={(event) => event.preventDefault()}
|
||||||
|
onCut={(event) => event.preventDefault()}
|
||||||
|
onPaste={(event) => event.preventDefault()}
|
||||||
|
onDrop={(event) => event.preventDefault()}
|
||||||
|
/>
|
||||||
|
{!typedMatched && typedText ? (
|
||||||
|
<Text type='danger' size='small'>
|
||||||
|
{mismatchText}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RiskAcknowledgementModal;
|
||||||
@@ -61,9 +61,11 @@ import OllamaModelModal from './OllamaModelModal';
|
|||||||
import CodexOAuthModal from './CodexOAuthModal';
|
import CodexOAuthModal from './CodexOAuthModal';
|
||||||
import JSONEditor from '../../../common/ui/JSONEditor';
|
import JSONEditor from '../../../common/ui/JSONEditor';
|
||||||
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
|
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
|
||||||
|
import StatusCodeRiskGuardModal from './StatusCodeRiskGuardModal';
|
||||||
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
||||||
import { useSecureVerification } from '../../../../hooks/common/useSecureVerification';
|
import { useSecureVerification } from '../../../../hooks/common/useSecureVerification';
|
||||||
import { createApiCalls } from '../../../../services/secureVerification';
|
import { createApiCalls } from '../../../../services/secureVerification';
|
||||||
|
import { collectNewDisallowedStatusCodeRedirects } from './statusCodeRiskGuard';
|
||||||
import {
|
import {
|
||||||
IconSave,
|
IconSave,
|
||||||
IconClose,
|
IconClose,
|
||||||
@@ -255,6 +257,12 @@ const EditChannelModal = (props) => {
|
|||||||
window.open(targetUrl, '_blank', 'noopener');
|
window.open(targetUrl, '_blank', 'noopener');
|
||||||
};
|
};
|
||||||
const [verifyLoading, setVerifyLoading] = useState(false);
|
const [verifyLoading, setVerifyLoading] = useState(false);
|
||||||
|
const statusCodeRiskConfirmResolverRef = useRef(null);
|
||||||
|
const [statusCodeRiskConfirmVisible, setStatusCodeRiskConfirmVisible] =
|
||||||
|
useState(false);
|
||||||
|
const [statusCodeRiskDetailItems, setStatusCodeRiskDetailItems] = useState(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
// 表单块导航相关状态
|
// 表单块导航相关状态
|
||||||
const formSectionRefs = useRef({
|
const formSectionRefs = useRef({
|
||||||
@@ -276,6 +284,7 @@ const EditChannelModal = (props) => {
|
|||||||
const doubaoApiClickCountRef = useRef(0);
|
const doubaoApiClickCountRef = useRef(0);
|
||||||
const initialModelsRef = useRef([]);
|
const initialModelsRef = useRef([]);
|
||||||
const initialModelMappingRef = useRef('');
|
const initialModelMappingRef = useRef('');
|
||||||
|
const initialStatusCodeMappingRef = useRef('');
|
||||||
|
|
||||||
// 2FA状态更新辅助函数
|
// 2FA状态更新辅助函数
|
||||||
const updateTwoFAState = (updates) => {
|
const updateTwoFAState = (updates) => {
|
||||||
@@ -691,6 +700,7 @@ const EditChannelModal = (props) => {
|
|||||||
.map((model) => (model || '').trim())
|
.map((model) => (model || '').trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
initialModelMappingRef.current = data.model_mapping || '';
|
initialModelMappingRef.current = data.model_mapping || '';
|
||||||
|
initialStatusCodeMappingRef.current = data.status_code_mapping || '';
|
||||||
|
|
||||||
let parsedIonet = null;
|
let parsedIonet = null;
|
||||||
if (data.other_info) {
|
if (data.other_info) {
|
||||||
@@ -1017,11 +1027,22 @@ const EditChannelModal = (props) => {
|
|||||||
if (!isEdit) {
|
if (!isEdit) {
|
||||||
initialModelsRef.current = [];
|
initialModelsRef.current = [];
|
||||||
initialModelMappingRef.current = '';
|
initialModelMappingRef.current = '';
|
||||||
|
initialStatusCodeMappingRef.current = '';
|
||||||
}
|
}
|
||||||
}, [isEdit, props.visible]);
|
}, [isEdit, props.visible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (statusCodeRiskConfirmResolverRef.current) {
|
||||||
|
statusCodeRiskConfirmResolverRef.current(false);
|
||||||
|
statusCodeRiskConfirmResolverRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 统一的模态框重置函数
|
// 统一的模态框重置函数
|
||||||
const resetModalState = () => {
|
const resetModalState = () => {
|
||||||
|
resolveStatusCodeRiskConfirm(false);
|
||||||
formApiRef.current?.reset();
|
formApiRef.current?.reset();
|
||||||
// 重置渠道设置状态
|
// 重置渠道设置状态
|
||||||
setChannelSettings({
|
setChannelSettings({
|
||||||
@@ -1151,6 +1172,22 @@ const EditChannelModal = (props) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolveStatusCodeRiskConfirm = (confirmed) => {
|
||||||
|
setStatusCodeRiskConfirmVisible(false);
|
||||||
|
setStatusCodeRiskDetailItems([]);
|
||||||
|
if (statusCodeRiskConfirmResolverRef.current) {
|
||||||
|
statusCodeRiskConfirmResolverRef.current(confirmed);
|
||||||
|
statusCodeRiskConfirmResolverRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmStatusCodeRisk = (detailItems) =>
|
||||||
|
new Promise((resolve) => {
|
||||||
|
statusCodeRiskConfirmResolverRef.current = resolve;
|
||||||
|
setStatusCodeRiskDetailItems(detailItems);
|
||||||
|
setStatusCodeRiskConfirmVisible(true);
|
||||||
|
});
|
||||||
|
|
||||||
const hasModelConfigChanged = (normalizedModels, modelMappingStr) => {
|
const hasModelConfigChanged = (normalizedModels, modelMappingStr) => {
|
||||||
if (!isEdit) return true;
|
if (!isEdit) return true;
|
||||||
const initialModels = initialModelsRef.current;
|
const initialModels = initialModelsRef.current;
|
||||||
@@ -1340,6 +1377,17 @@ const EditChannelModal = (props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const riskyStatusCodeRedirects = collectNewDisallowedStatusCodeRedirects(
|
||||||
|
initialStatusCodeMappingRef.current,
|
||||||
|
localInputs.status_code_mapping,
|
||||||
|
);
|
||||||
|
if (riskyStatusCodeRedirects.length > 0) {
|
||||||
|
const confirmed = await confirmStatusCodeRisk(riskyStatusCodeRedirects);
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
if (localInputs.base_url && localInputs.base_url.endsWith('/')) {
|
||||||
localInputs.base_url = localInputs.base_url.slice(
|
localInputs.base_url = localInputs.base_url.slice(
|
||||||
0,
|
0,
|
||||||
@@ -3440,6 +3488,12 @@ const EditChannelModal = (props) => {
|
|||||||
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
||||||
/>
|
/>
|
||||||
</SideSheet>
|
</SideSheet>
|
||||||
|
<StatusCodeRiskGuardModal
|
||||||
|
visible={statusCodeRiskConfirmVisible}
|
||||||
|
detailItems={statusCodeRiskDetailItems}
|
||||||
|
onCancel={() => resolveStatusCodeRiskConfirm(false)}
|
||||||
|
onConfirm={() => resolveStatusCodeRiskConfirm(true)}
|
||||||
|
/>
|
||||||
{/* 使用通用安全验证模态框 */}
|
{/* 使用通用安全验证模态框 */}
|
||||||
<SecureVerificationModal
|
<SecureVerificationModal
|
||||||
visible={isModalVisible}
|
visible={isModalVisible}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import RiskAcknowledgementModal from '../../../common/modals/RiskAcknowledgementModal';
|
||||||
|
import {
|
||||||
|
STATUS_CODE_RISK_I18N_KEYS,
|
||||||
|
STATUS_CODE_RISK_CHECKLIST_KEYS,
|
||||||
|
} from './statusCodeRiskGuard';
|
||||||
|
|
||||||
|
const StatusCodeRiskGuardModal = ({
|
||||||
|
visible,
|
||||||
|
detailItems,
|
||||||
|
onCancel,
|
||||||
|
onConfirm,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RiskAcknowledgementModal
|
||||||
|
visible={visible}
|
||||||
|
title={t(STATUS_CODE_RISK_I18N_KEYS.title)}
|
||||||
|
markdownContent={t(STATUS_CODE_RISK_I18N_KEYS.markdown)}
|
||||||
|
detailTitle={t(STATUS_CODE_RISK_I18N_KEYS.detailTitle)}
|
||||||
|
detailItems={detailItems}
|
||||||
|
checklist={STATUS_CODE_RISK_CHECKLIST_KEYS.map((item) => t(item))}
|
||||||
|
inputPrompt={t(STATUS_CODE_RISK_I18N_KEYS.inputPrompt)}
|
||||||
|
requiredText={t(STATUS_CODE_RISK_I18N_KEYS.confirmText)}
|
||||||
|
inputPlaceholder={t(STATUS_CODE_RISK_I18N_KEYS.inputPlaceholder)}
|
||||||
|
mismatchText={t(STATUS_CODE_RISK_I18N_KEYS.mismatchText)}
|
||||||
|
cancelText={t('取消')}
|
||||||
|
confirmText={t(STATUS_CODE_RISK_I18N_KEYS.confirmButton)}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StatusCodeRiskGuardModal;
|
||||||
101
web/src/components/table/channels/modals/statusCodeRiskGuard.js
Normal file
101
web/src/components/table/channels/modals/statusCodeRiskGuard.js
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
const NON_REDIRECTABLE_STATUS_CODES = new Set([504, 524]);
|
||||||
|
|
||||||
|
export const STATUS_CODE_RISK_I18N_KEYS = {
|
||||||
|
title: '高危操作确认',
|
||||||
|
detailTitle: '检测到以下高危状态码重定向规则',
|
||||||
|
inputPrompt: '操作确认',
|
||||||
|
confirmButton: '我确认开启高危重试',
|
||||||
|
markdown: '高危状态码重试风险告知与免责声明Markdown',
|
||||||
|
confirmText: '高危状态码重试风险确认输入文本',
|
||||||
|
inputPlaceholder: '高危状态码重试风险输入框占位文案',
|
||||||
|
mismatchText: '高危状态码重试风险输入不匹配提示',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const STATUS_CODE_RISK_CHECKLIST_KEYS = [
|
||||||
|
'高危状态码重试风险确认项1',
|
||||||
|
'高危状态码重试风险确认项2',
|
||||||
|
'高危状态码重试风险确认项3',
|
||||||
|
'高危状态码重试风险确认项4',
|
||||||
|
];
|
||||||
|
|
||||||
|
function parseStatusCodeKey(rawKey) {
|
||||||
|
if (typeof rawKey !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = rawKey.trim();
|
||||||
|
if (!/^[1-5]\d{2}$/.test(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Number.parseInt(normalized, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStatusCodeMappingTarget(rawValue) {
|
||||||
|
if (typeof rawValue === 'number' && Number.isInteger(rawValue)) {
|
||||||
|
return rawValue >= 100 && rawValue <= 599 ? rawValue : null;
|
||||||
|
}
|
||||||
|
if (typeof rawValue === 'string') {
|
||||||
|
const normalized = rawValue.trim();
|
||||||
|
if (!/^[1-5]\d{2}$/.test(normalized)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const code = Number.parseInt(normalized, 10);
|
||||||
|
return code >= 100 && code <= 599 ? code : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectDisallowedStatusCodeRedirects(statusCodeMappingStr) {
|
||||||
|
if (
|
||||||
|
typeof statusCodeMappingStr !== 'string' ||
|
||||||
|
statusCodeMappingStr.trim() === ''
|
||||||
|
) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(statusCodeMappingStr);
|
||||||
|
} catch (error) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const riskyMappings = [];
|
||||||
|
Object.entries(parsed).forEach(([rawFrom, rawTo]) => {
|
||||||
|
const fromCode = parseStatusCodeKey(rawFrom);
|
||||||
|
const toCode = parseStatusCodeMappingTarget(rawTo);
|
||||||
|
if (fromCode === null || toCode === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!NON_REDIRECTABLE_STATUS_CODES.has(fromCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fromCode === toCode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
riskyMappings.push(`${fromCode} -> ${toCode}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(new Set(riskyMappings)).sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectNewDisallowedStatusCodeRedirects(
|
||||||
|
originalStatusCodeMappingStr,
|
||||||
|
currentStatusCodeMappingStr,
|
||||||
|
) {
|
||||||
|
const currentRisky = collectDisallowedStatusCodeRedirects(
|
||||||
|
currentStatusCodeMappingStr,
|
||||||
|
);
|
||||||
|
if (currentRisky.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalRiskySet = new Set(
|
||||||
|
collectDisallowedStatusCodeRedirects(originalStatusCodeMappingStr),
|
||||||
|
);
|
||||||
|
|
||||||
|
return currentRisky.filter((mapping) => !originalRiskySet.has(mapping));
|
||||||
|
}
|
||||||
@@ -1949,6 +1949,19 @@
|
|||||||
"自动重试状态码": "Auto-retry status codes",
|
"自动重试状态码": "Auto-retry status codes",
|
||||||
"自动重试状态码格式不正确": "Invalid auto-retry status code format",
|
"自动重试状态码格式不正确": "Invalid auto-retry status code format",
|
||||||
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "Supports single status codes or inclusive ranges; separate with commas",
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "Supports single status codes or inclusive ranges; separate with commas",
|
||||||
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响": "Supports single status codes or inclusive ranges; separate with commas. 504 and 524 are never retried and are not affected by this setting",
|
||||||
|
"高危操作确认": "High-risk operation confirmation",
|
||||||
|
"检测到以下高危状态码重定向规则": "Detected high-risk status-code redirect rules",
|
||||||
|
"操作确认": "Operation confirmation",
|
||||||
|
"我确认开启高危重试": "I confirm enabling high-risk retry",
|
||||||
|
"高危状态码重试风险告知与免责声明Markdown": "### ⚠️ High-Risk Operation: Risk Notice and Disclaimer for 504/524 Retry\n\n[Background]\nBy default, this project does not retry for status codes `400` (bad request), `504` (gateway timeout), and `524` (timeout occurred). In many cases, `504` and `524` mean the request has reached the upstream AI service and processing has started, but the connection was closed due to long processing time.\n\nEnabling redirection/retry for these timeout status codes is a **high-risk operation**. Before enabling it, you must read and understand the consequences below:\n\n#### 1. Core Risks (Read Carefully)\n1. 💸 Duplicate/multiple billing risk: Most upstream AI providers **still charge** for requests that started processing but got interrupted by network timeout (`504`/`524`). If retry is triggered, a new upstream request will be sent, which can lead to **duplicate or multiple charges**.\n2. ⏳ Severe client timeout: If a single request already timed out, adding retries can multiply total latency and cause severe or unacceptable timeout behavior for your final client/caller.\n3. 💥 Request backlog and system crash risk: Forcing retries on timeout requests keeps threads and connections occupied for longer. Under high concurrency, this can cause serious backlog, exhaust system resources, trigger a cascading failure, and crash your proxy service.\n\n#### 2. Risk Acknowledgement\nIf you still choose to enable this feature, you acknowledge all of the following:\n\n- [ ] I have fully read and understood the risks and fully understand the destructive consequences of forcing retries for status codes `504` and `524`.\n- [ ] I have communicated with the upstream provider and confirmed that the timeout issue is an upstream bottleneck and cannot be resolved upstream at this time.\n- [ ] I voluntarily accept all duplicate/multiple billing risks and will not file issues or complaints in this project repository regarding billing anomalies caused by this retry behavior.\n- [ ] I voluntarily accept system stability risks, including severe client timeout and possible service crash. Any consequences caused by enabling this feature are my own responsibility.\n\n> **[Operation Confirmation]**\n> To unlock this feature, manually type the text below in the input box:\n> I understand the duplicate billing and crash risks, and confirm enabling it.",
|
||||||
|
"高危状态码重试风险确认输入文本": "I understand the duplicate billing and crash risks, and confirm enabling it.",
|
||||||
|
"高危状态码重试风险确认项1": "I have fully read and understood the risks and fully understand the destructive consequences of forcing retries for status codes 504 and 524.",
|
||||||
|
"高危状态码重试风险确认项2": "I have communicated with the upstream provider and confirmed that the timeout issue is an upstream bottleneck and cannot be resolved upstream at this time.",
|
||||||
|
"高危状态码重试风险确认项3": "I voluntarily accept all duplicate/multiple billing risks and will not file issues or complaints in this project repository regarding billing anomalies caused by this retry behavior.",
|
||||||
|
"高危状态码重试风险确认项4": "I voluntarily accept system stability risks, including severe client timeout and possible service crash. Any consequences caused by enabling this feature are my own responsibility.",
|
||||||
|
"高危状态码重试风险输入框占位文案": "Please type the exact text above",
|
||||||
|
"高危状态码重试风险输入不匹配提示": "The input does not match the required text",
|
||||||
"例如:401, 403, 429, 500-599": "e.g. 401,403,429,500-599",
|
"例如:401, 403, 429, 500-599": "e.g. 401,403,429,500-599",
|
||||||
"自动选择": "Auto Select",
|
"自动选择": "Auto Select",
|
||||||
"自定义充值数量选项": "Custom Recharge Amount Options",
|
"自定义充值数量选项": "Custom Recharge Amount Options",
|
||||||
|
|||||||
@@ -1936,6 +1936,19 @@
|
|||||||
"自动重试状态码": "自动重试状态码",
|
"自动重试状态码": "自动重试状态码",
|
||||||
"自动重试状态码格式不正确": "自动重试状态码格式不正确",
|
"自动重试状态码格式不正确": "自动重试状态码格式不正确",
|
||||||
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "支持填写单个状态码或范围(含首尾),使用逗号分隔",
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "支持填写单个状态码或范围(含首尾),使用逗号分隔",
|
||||||
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响": "支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响",
|
||||||
|
"高危操作确认": "高危操作确认",
|
||||||
|
"检测到以下高危状态码重定向规则": "检测到以下高危状态码重定向规则",
|
||||||
|
"操作确认": "操作确认",
|
||||||
|
"我确认开启高危重试": "我确认开启高危重试",
|
||||||
|
"高危状态码重试风险告知与免责声明Markdown": "### ⚠️ 高危操作:504/524 状态码重试风险告知与免责声明\n\n【背景提示】\n本项目默认对 `400`(请求错误)、`504`(网关超时)和 `524`(发生超时)状态码不进行重试。504 和 524 错误通常意味着**请求已成功送达上游 AI 服务,且上游正在处理,但因处理时间过长导致连接断开**。\n\n开启对此类超时状态码的重定向/重试属于**极高风险操作**。作为本开源项目的使用者,在开启该功能前,您必须仔细阅读并知悉以下严重后果:\n\n#### 一、 核心风险告知(请仔细阅读)\n1. 💸 双重/多重计费风险: 绝大多数 AI 上游厂商对于已经开始处理但因网络原因中断(504/524)的请求**依然会进行扣费**。此时若触发重试,将会向上游发起全新请求,导致您被**双重甚至多重计费**。\n2. ⏳ 客户端严重超时: 单次请求已经触发超时,叠加重试机制将会使总请求耗时成倍增加,导致您的最终客户端(或调用方)出现严重甚至完全无法接受的超时现象。\n3. 💥 请求积压与系统崩溃风险: 强制重试超时请求会长时间占用系统线程和连接数。在高并发场景下,这会导致严重的**请求积压**,进而耗尽系统资源,引发雪崩效应,导致您的整个代理服务崩溃。\n\n#### 二、 风险确认声明\n如果您坚持开启该功能,即代表您作出以下确认:\n\n- [ ] 我已充分阅读并理解**:本人已完整阅读上述全部风险提示,完全理解强制重试 `504` 和 `524` 状态码可能带来的破坏性后果。\n- [ ] **我已与上游沟通并确认**:本人确认,当前出现的超时问题属于上游服务的瓶颈。**本人已与上游提供商进行过沟通,确认上游无法解决该超时问题**,因此才采取强制重试方案作为妥协手段。\n- [ ] **我自愿承担计费损失**:本人知晓并接受由此产生的全部双重/多重计费风险,**承诺不会因重试导致的账单异常在本项目仓库中提交 Issue 或抱怨**。\n- [ ] **我自愿承担系统稳定性风险**:本人知晓该操作可能导致客户端严重超时及服务崩溃。若因本人开启此功能导致请求积压或服务不可用,后果由本人自行承担。\n\n> **【操作确认】\n> 为确认您已清晰了解上述风险,请在下方输入框内手动输入以下文字以解锁功能:\n> 我已了解多重计费与崩溃风险,确认开启",
|
||||||
|
"高危状态码重试风险确认输入文本": "我已了解多重计费与崩溃风险,确认开启",
|
||||||
|
"高危状态码重试风险确认项1": "我已充分阅读并理解:本人已完整阅读上述全部风险提示,完全理解强制重试 504 和 524 状态码可能带来的破坏性后果。",
|
||||||
|
"高危状态码重试风险确认项2": "我已与上游沟通并确认:本人确认,当前出现的超时问题属于上游服务的瓶颈。本人已与上游提供商进行过沟通,确认上游无法解决该超时问题,因此才采取强制重试方案作为妥协手段。",
|
||||||
|
"高危状态码重试风险确认项3": "我自愿承担计费损失:本人知晓并接受由此产生的全部双重/多重计费风险,承诺不会因重试导致的账单异常在本项目仓库中提交 Issue 或抱怨。",
|
||||||
|
"高危状态码重试风险确认项4": "我自愿承担系统稳定性风险:本人知晓该操作可能导致客户端严重超时及服务崩溃。若因本人开启此功能导致请求积压或服务不可用,后果由本人自行承担。",
|
||||||
|
"高危状态码重试风险输入框占位文案": "请完整输入上方文字",
|
||||||
|
"高危状态码重试风险输入不匹配提示": "输入内容与要求不一致",
|
||||||
"例如:401, 403, 429, 500-599": "例如:401,403,429,500-599",
|
"例如:401, 403, 429, 500-599": "例如:401,403,429,500-599",
|
||||||
"自动选择": "自动选择",
|
"自动选择": "自动选择",
|
||||||
"自定义充值数量选项": "自定义充值数量选项",
|
"自定义充值数量选项": "自定义充值数量选项",
|
||||||
|
|||||||
@@ -1942,6 +1942,19 @@
|
|||||||
"自动重试状态码": "自動重試狀態碼",
|
"自动重试状态码": "自動重試狀態碼",
|
||||||
"自动重试状态码格式不正确": "自動重試狀態碼格式不正確",
|
"自动重试状态码格式不正确": "自動重試狀態碼格式不正確",
|
||||||
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "支援填寫單個狀態碼或範圍(含首尾),使用逗號分隔",
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔": "支援填寫單個狀態碼或範圍(含首尾),使用逗號分隔",
|
||||||
|
"支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响": "支援填寫單個狀態碼或範圍(含首尾),使用逗號分隔;504 和 524 一律不重試,不受此處設定影響",
|
||||||
|
"高危操作确认": "高風險操作確認",
|
||||||
|
"检测到以下高危状态码重定向规则": "檢測到以下高風險狀態碼重定向規則",
|
||||||
|
"操作确认": "操作確認",
|
||||||
|
"我确认开启高危重试": "我確認開啟高風險重試",
|
||||||
|
"高危状态码重试风险告知与免责声明Markdown": "### ⚠️ 高風險操作:504/524 狀態碼重試風險告知與免責聲明\n\n【背景提示】\n本專案預設對 `400`(請求錯誤)、`504`(閘道逾時)與 `524`(發生逾時)狀態碼不進行重試。504 與 524 錯誤通常代表**請求已成功送達上游 AI 服務,且上游正在處理,但因處理時間過長導致連線中斷**。\n\n開啟此類逾時狀態碼的重定向/重試屬於**極高風險操作**。作為本開源專案使用者,在開啟該功能前,您必須仔細閱讀並知悉以下嚴重後果:\n\n#### 一、 核心風險告知(請仔細閱讀)\n1. 💸 雙重/多重計費風險:多數 AI 上游廠商對於已開始處理但因網路原因中斷(504/524)的請求**仍然會扣費**。此時若觸發重試,將會向上游發起全新請求,導致您被**雙重甚至多重計費**。\n2. ⏳ 用戶端嚴重逾時:單次請求已觸發逾時,疊加重試機制會使總請求耗時成倍增加,導致最終用戶端(或呼叫方)出現嚴重甚至無法接受的逾時現象。\n3. 💥 請求積壓與系統崩潰風險:強制重試逾時請求會長時間占用系統執行緒與連線數。在高併發場景下,這將導致嚴重**請求積壓**,進而耗盡系統資源,引發雪崩效應,造成整個代理服務崩潰。\n\n#### 二、 風險確認聲明\n若您堅持開啟該功能,即代表您作出以下確認:\n\n- [ ] 我已充分閱讀並理解:本人已完整閱讀上述全部風險提示,完全理解強制重試 `504` 與 `524` 狀態碼可能帶來的破壞性後果。\n- [ ] 我已與上游溝通並確認:本人確認,當前逾時問題屬於上游服務瓶頸。本人已與上游供應商溝通,確認上游無法解決該逾時問題,因此才採取強制重試方案作為妥協手段。\n- [ ] 我自願承擔計費損失:本人知悉並接受由此產生的全部雙重/多重計費風險,承諾不會因重試導致的帳單異常在本專案倉庫提交 Issue 或抱怨。\n- [ ] 我自願承擔系統穩定性風險:本人知悉該操作可能導致用戶端嚴重逾時及服務崩潰。若因本人開啟此功能導致請求積壓或服務不可用,後果由本人自行承擔。\n\n> **【操作確認】**\n> 為確認您已清楚了解上述風險,請在下方輸入框內手動輸入以下文字以解鎖功能:\n> 我已了解多重計費與崩潰風險,確認開啟",
|
||||||
|
"高危状态码重试风险确认输入文本": "我已了解多重計費與崩潰風險,確認開啟",
|
||||||
|
"高危状态码重试风险确认项1": "我已充分閱讀並理解:本人已完整閱讀上述全部風險提示,完全理解強制重試 504 與 524 狀態碼可能帶來的破壞性後果。",
|
||||||
|
"高危状态码重试风险确认项2": "我已與上游溝通並確認:本人確認,當前逾時問題屬於上游服務瓶頸。本人已與上游供應商溝通,確認上游無法解決該逾時問題,因此才採取強制重試方案作為妥協手段。",
|
||||||
|
"高危状态码重试风险确认项3": "我自願承擔計費損失:本人知悉並接受由此產生的全部雙重/多重計費風險,承諾不會因重試導致的帳單異常在本專案倉庫提交 Issue 或抱怨。",
|
||||||
|
"高危状态码重试风险确认项4": "我自願承擔系統穩定性風險:本人知悉該操作可能導致用戶端嚴重逾時及服務崩潰。若因本人開啟此功能導致請求積壓或服務不可用,後果由本人自行承擔。",
|
||||||
|
"高危状态码重试风险输入框占位文案": "請完整輸入上方文字",
|
||||||
|
"高危状态码重试风险输入不匹配提示": "輸入內容與要求不一致",
|
||||||
"例如:401, 403, 429, 500-599": "例如:401,403,429,500-599",
|
"例如:401, 403, 429, 500-599": "例如:401,403,429,500-599",
|
||||||
"自动选择": "自動選擇",
|
"自动选择": "自動選擇",
|
||||||
"自定义充值数量选项": "自訂儲值數量選項",
|
"自定义充值数量选项": "自訂儲值數量選項",
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export default function SettingsMonitoring(props) {
|
|||||||
label={t('自动重试状态码')}
|
label={t('自动重试状态码')}
|
||||||
placeholder={t('例如:401, 403, 429, 500-599')}
|
placeholder={t('例如:401, 403, 429, 500-599')}
|
||||||
extraText={t(
|
extraText={t(
|
||||||
'支持填写单个状态码或范围(含首尾),使用逗号分隔',
|
'支持填写单个状态码或范围(含首尾),使用逗号分隔;504 和 524 始终不重试,不受此处配置影响',
|
||||||
)}
|
)}
|
||||||
field={'AutomaticRetryStatusCodes'}
|
field={'AutomaticRetryStatusCodes'}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user