Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion controller/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,7 @@ func shouldRetryTaskRelay(c *gin.Context, channelId int, taskErr *dto.TaskError,
}
if taskErr.StatusCode/100 == 5 {
// 超时不重试
if taskErr.StatusCode == 504 || taskErr.StatusCode == 524 {
if operation_setting.IsAlwaysSkipRetryStatusCode(taskErr.StatusCode) {
return false
}
return true
Expand Down
13 changes: 13 additions & 0 deletions setting/operation_setting/status_code_ranges.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ var AutomaticRetryStatusCodeRanges = []StatusCodeRange{
{Start: 525, End: 599},
}

var alwaysSkipRetryStatusCodes = map[int]struct{}{
504: {},
524: {},
}

func AutomaticDisableStatusCodesToString() string {
return statusCodeRangesToString(AutomaticDisableStatusCodeRanges)
}
Expand Down Expand Up @@ -56,7 +61,15 @@ func AutomaticRetryStatusCodesFromString(s string) error {
return nil
}

func IsAlwaysSkipRetryStatusCode(code int) bool {
_, exists := alwaysSkipRetryStatusCodes[code]
return exists
}

func ShouldRetryByStatusCode(code int) bool {
if IsAlwaysSkipRetryStatusCode(code) {
return false
}
return shouldMatchStatusCodeRanges(AutomaticRetryStatusCodeRanges, code)
}

Expand Down
8 changes: 8 additions & 0 deletions setting/operation_setting/status_code_ranges_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ func TestShouldRetryByStatusCode(t *testing.T) {

require.True(t, ShouldRetryByStatusCode(429))
require.True(t, ShouldRetryByStatusCode(500))
require.False(t, ShouldRetryByStatusCode(504))
require.False(t, ShouldRetryByStatusCode(524))
require.False(t, ShouldRetryByStatusCode(400))
require.False(t, ShouldRetryByStatusCode(200))
}
Expand All @@ -77,3 +79,9 @@ func TestShouldRetryByStatusCode_DefaultMatchesLegacyBehavior(t *testing.T) {
require.False(t, ShouldRetryByStatusCode(524))
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))
}
225 changes: 225 additions & 0 deletions web/src/components/common/modals/RiskAcknowledgementModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
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, { useCallback, 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 RiskMarkdownBlock = React.memo(function RiskMarkdownBlock({
markdownContent,
}) {
if (!markdownContent) {
return null;
}

return (
<div
className='rounded-lg'
style={{
border: '1px solid var(--semi-color-warning-light-hover)',
background:
'linear-gradient(180deg, var(--semi-color-warning-light-default) 0%, var(--semi-color-fill-0) 100%)',
padding: '12px',
contentVisibility: 'auto',
}}
>
<MarkdownRenderer content={markdownContent} />
</div>
);
});

const RiskAcknowledgementModal = React.memo(function 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]);

const detailText = useMemo(() => detailItems.join(', '), [detailItems]);
const canConfirm = allChecked && typedMatched;

const handleChecklistChange = useCallback((index, checked) => {
setCheckedItems((previous) => {
const next = [...previous];
next[index] = checked;
return next;
});
}, []);

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' : '18px 22px',
}}
footer={
<Space>
<Button onClick={onCancel}>{cancelText}</Button>
<Button
theme='solid'
type='danger'
disabled={!canConfirm}
onClick={onConfirm}
>
{confirmText}
</Button>
</Space>
}
>
<div className='flex flex-col gap-4'>
<div
className='rounded-lg'
style={{
border: '1px solid var(--semi-color-warning-light-hover)',
background: 'var(--semi-color-warning-light-default)',
padding: isMobile ? '10px 12px' : '12px 14px',
}}
>
</div>
Comment on lines +139 to +147

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove or populate the empty warning banner.
Lines 139-147 render a styled container with no content, which shows as a blank block in the modal.

🧹 Proposed cleanup
-        <div
-          className='rounded-lg'
-          style={{
-            border: '1px solid var(--semi-color-warning-light-hover)',
-            background: 'var(--semi-color-warning-light-default)',
-            padding: isMobile ? '10px 12px' : '12px 14px',
-          }}
-        >
-        </div>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
className='rounded-lg'
style={{
border: '1px solid var(--semi-color-warning-light-hover)',
background: 'var(--semi-color-warning-light-default)',
padding: isMobile ? '10px 12px' : '12px 14px',
}}
>
</div>
{/* warning banner removed - was empty */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/common/modals/RiskAcknowledgementModal.jsx` around lines
139 - 147, The empty styled warning container inside RiskAcknowledgementModal
(the div with className 'rounded-lg' and inline styles using isMobile) should be
removed or populated; either delete that div entirely to avoid rendering a blank
block, or add the intended warning content (e.g., the modal’s warning
title/message or an i18n string) inside it so the banner displays meaningful
text and actions; locate this div in RiskAcknowledgementModal.jsx (the element
using isMobile for padding) and implement the chosen change.


<RiskMarkdownBlock markdownContent={markdownContent} />

{detailItems.length > 0 ? (
<div
className='flex flex-col gap-2 rounded-lg'
style={{
border: '1px solid var(--semi-color-warning-light-hover)',
background: 'var(--semi-color-fill-0)',
padding: isMobile ? '10px 12px' : '12px 14px',
}}
>
{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'>
{detailText}
</div>
</div>
) : null}

{checklist.length > 0 ? (
<div
className='flex flex-col gap-2 rounded-lg'
style={{
border: '1px solid var(--semi-color-border)',
background: 'var(--semi-color-fill-0)',
padding: isMobile ? '10px 12px' : '12px 14px',
}}
>
{checklist.map((item, index) => (
<Checkbox
key={`risk-check-${index}`}
checked={!!checkedItems[index]}
onChange={(event) => {
handleChecklistChange(index, event.target.checked);
}}
>
{item}
</Checkbox>
))}
</div>
) : null}

{requiredText ? (
<div
className='flex flex-col gap-2 rounded-lg'
style={{
border: '1px solid var(--semi-color-danger-light-hover)',
background: 'var(--semi-color-danger-light-default)',
padding: isMobile ? '10px 12px' : '12px 14px',
}}
>
{inputPrompt ? <Text strong>{inputPrompt}</Text> : null}
<div className='font-mono text-xs break-all rounded-md p-2 bg-gray-50 border border-gray-200'>
{requiredText}
</div>
<Input
value={typedText}
onChange={setTypedText}
placeholder={inputPlaceholder}
autoFocus={visible}
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;
54 changes: 54 additions & 0 deletions web/src/components/table/channels/modals/EditChannelModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,11 @@ import OllamaModelModal from './OllamaModelModal';
import CodexOAuthModal from './CodexOAuthModal';
import JSONEditor from '../../../common/ui/JSONEditor';
import SecureVerificationModal from '../../../common/modals/SecureVerificationModal';
import StatusCodeRiskGuardModal from './StatusCodeRiskGuardModal';
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
import { useSecureVerification } from '../../../../hooks/common/useSecureVerification';
import { createApiCalls } from '../../../../services/secureVerification';
import { collectNewDisallowedStatusCodeRedirects } from './statusCodeRiskGuard';
import {
IconSave,
IconClose,
Expand Down Expand Up @@ -255,6 +257,12 @@ const EditChannelModal = (props) => {
window.open(targetUrl, '_blank', 'noopener');
};
const [verifyLoading, setVerifyLoading] = useState(false);
const statusCodeRiskConfirmResolverRef = useRef(null);
const [statusCodeRiskConfirmVisible, setStatusCodeRiskConfirmVisible] =
useState(false);
const [statusCodeRiskDetailItems, setStatusCodeRiskDetailItems] = useState(
[],
);

// 表单块导航相关状态
const formSectionRefs = useRef({
Expand All @@ -276,6 +284,7 @@ const EditChannelModal = (props) => {
const doubaoApiClickCountRef = useRef(0);
const initialModelsRef = useRef([]);
const initialModelMappingRef = useRef('');
const initialStatusCodeMappingRef = useRef('');

// 2FA状态更新辅助函数
const updateTwoFAState = (updates) => {
Expand Down Expand Up @@ -691,6 +700,7 @@ const EditChannelModal = (props) => {
.map((model) => (model || '').trim())
.filter(Boolean);
initialModelMappingRef.current = data.model_mapping || '';
initialStatusCodeMappingRef.current = data.status_code_mapping || '';

let parsedIonet = null;
if (data.other_info) {
Expand Down Expand Up @@ -1017,11 +1027,22 @@ const EditChannelModal = (props) => {
if (!isEdit) {
initialModelsRef.current = [];
initialModelMappingRef.current = '';
initialStatusCodeMappingRef.current = '';
}
}, [isEdit, props.visible]);

useEffect(() => {
return () => {
if (statusCodeRiskConfirmResolverRef.current) {
statusCodeRiskConfirmResolverRef.current(false);
statusCodeRiskConfirmResolverRef.current = null;
}
};
}, []);

// 统一的模态框重置函数
const resetModalState = () => {
resolveStatusCodeRiskConfirm(false);
formApiRef.current?.reset();
// 重置渠道设置状态
setChannelSettings({
Expand Down Expand Up @@ -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) => {
if (!isEdit) return true;
const initialModels = initialModelsRef.current;
Expand Down Expand Up @@ -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('/')) {
localInputs.base_url = localInputs.base_url.slice(
0,
Expand Down Expand Up @@ -3440,6 +3488,12 @@ const EditChannelModal = (props) => {
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
/>
</SideSheet>
<StatusCodeRiskGuardModal
visible={statusCodeRiskConfirmVisible}
detailItems={statusCodeRiskDetailItems}
onCancel={() => resolveStatusCodeRiskConfirm(false)}
onConfirm={() => resolveStatusCodeRiskConfirm(true)}
/>
{/* 使用通用安全验证模态框 */}
<SecureVerificationModal
visible={isModalVisible}
Expand Down
Loading