Merge branch 'upstream-main' into feature/improve-param-override
# Conflicts: # relay/channel/api_request_test.go # relay/common/override_test.go # web/src/components/table/channels/modals/EditChannelModal.jsx
This commit is contained in:
@@ -62,9 +62,14 @@ import CodexOAuthModal from './CodexOAuthModal';
|
||||
import ParamOverrideEditorModal from './ParamOverrideEditorModal';
|
||||
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 {
|
||||
collectInvalidStatusCodeEntries,
|
||||
collectNewDisallowedStatusCodeRedirects,
|
||||
} from './statusCodeRiskGuard';
|
||||
import {
|
||||
IconSave,
|
||||
IconClose,
|
||||
@@ -195,6 +200,8 @@ const EditChannelModal = (props) => {
|
||||
allow_service_tier: false,
|
||||
disable_store: false, // false = 允许透传(默认开启)
|
||||
allow_safety_identifier: false,
|
||||
allow_include_obfuscation: false,
|
||||
allow_inference_geo: false,
|
||||
claude_beta_query: false,
|
||||
};
|
||||
const [batch, setBatch] = useState(false);
|
||||
@@ -209,6 +216,7 @@ const EditChannelModal = (props) => {
|
||||
const [fullModels, setFullModels] = useState([]);
|
||||
const [modelGroups, setModelGroups] = useState([]);
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const [modelSearchValue, setModelSearchValue] = useState('');
|
||||
const [modalImageUrl, setModalImageUrl] = useState('');
|
||||
const [isModalOpenurl, setIsModalOpenurl] = useState(false);
|
||||
const [modelModalVisible, setModelModalVisible] = useState(false);
|
||||
@@ -249,6 +257,25 @@ const EditChannelModal = (props) => {
|
||||
return [];
|
||||
}
|
||||
}, [inputs.model_mapping]);
|
||||
const modelSearchMatchedCount = useMemo(() => {
|
||||
const keyword = modelSearchValue.trim();
|
||||
if (!keyword) {
|
||||
return modelOptions.length;
|
||||
}
|
||||
return modelOptions.reduce(
|
||||
(count, option) => count + (selectFilter(keyword, option) ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
}, [modelOptions, modelSearchValue]);
|
||||
const modelSearchHintText = useMemo(() => {
|
||||
const keyword = modelSearchValue.trim();
|
||||
if (!keyword || modelSearchMatchedCount !== 0) {
|
||||
return '';
|
||||
}
|
||||
return t('未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加', {
|
||||
name: keyword,
|
||||
});
|
||||
}, [modelSearchMatchedCount, modelSearchValue, t]);
|
||||
const paramOverrideMeta = useMemo(() => {
|
||||
const raw =
|
||||
typeof inputs.param_override === 'string'
|
||||
@@ -338,6 +365,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({
|
||||
@@ -359,6 +392,7 @@ const EditChannelModal = (props) => {
|
||||
const doubaoApiClickCountRef = useRef(0);
|
||||
const initialModelsRef = useRef([]);
|
||||
const initialModelMappingRef = useRef('');
|
||||
const initialStatusCodeMappingRef = useRef('');
|
||||
|
||||
// 2FA状态更新辅助函数
|
||||
const updateTwoFAState = (updates) => {
|
||||
@@ -811,6 +845,10 @@ const EditChannelModal = (props) => {
|
||||
data.disable_store = parsedSettings.disable_store || false;
|
||||
data.allow_safety_identifier =
|
||||
parsedSettings.allow_safety_identifier || false;
|
||||
data.allow_include_obfuscation =
|
||||
parsedSettings.allow_include_obfuscation || false;
|
||||
data.allow_inference_geo =
|
||||
parsedSettings.allow_inference_geo || false;
|
||||
data.claude_beta_query = parsedSettings.claude_beta_query || false;
|
||||
} catch (error) {
|
||||
console.error('解析其他设置失败:', error);
|
||||
@@ -822,6 +860,8 @@ const EditChannelModal = (props) => {
|
||||
data.allow_service_tier = false;
|
||||
data.disable_store = false;
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.claude_beta_query = false;
|
||||
}
|
||||
} else {
|
||||
@@ -832,6 +872,8 @@ const EditChannelModal = (props) => {
|
||||
data.allow_service_tier = false;
|
||||
data.disable_store = false;
|
||||
data.allow_safety_identifier = false;
|
||||
data.allow_include_obfuscation = false;
|
||||
data.allow_inference_geo = false;
|
||||
data.claude_beta_query = false;
|
||||
}
|
||||
|
||||
@@ -868,6 +910,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) {
|
||||
@@ -1173,6 +1216,7 @@ const EditChannelModal = (props) => {
|
||||
}, [inputs]);
|
||||
|
||||
useEffect(() => {
|
||||
setModelSearchValue('');
|
||||
if (props.visible) {
|
||||
if (isEdit) {
|
||||
loadChannel();
|
||||
@@ -1194,11 +1238,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({
|
||||
@@ -1216,6 +1271,7 @@ const EditChannelModal = (props) => {
|
||||
// 重置豆包隐藏入口状态
|
||||
setDoubaoApiEditUnlocked(false);
|
||||
doubaoApiClickCountRef.current = 0;
|
||||
setModelSearchValue('');
|
||||
// 清空表单中的key_mode字段
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key_mode', undefined);
|
||||
@@ -1328,6 +1384,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;
|
||||
@@ -1518,6 +1590,27 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
const invalidStatusCodeEntries = collectInvalidStatusCodeEntries(
|
||||
localInputs.status_code_mapping,
|
||||
);
|
||||
if (invalidStatusCodeEntries.length > 0) {
|
||||
showError(
|
||||
`${t('状态码复写包含无效的状态码')}: ${invalidStatusCodeEntries.join(', ')}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -1570,13 +1663,16 @@ const EditChannelModal = (props) => {
|
||||
// type === 1 (OpenAI) 或 type === 14 (Claude): 设置字段透传控制(显式保存布尔值)
|
||||
if (localInputs.type === 1 || localInputs.type === 14) {
|
||||
settings.allow_service_tier = localInputs.allow_service_tier === true;
|
||||
// 仅 OpenAI 渠道需要 store 和 safety_identifier
|
||||
// 仅 OpenAI 渠道需要 store / safety_identifier / include_obfuscation
|
||||
if (localInputs.type === 1) {
|
||||
settings.disable_store = localInputs.disable_store === true;
|
||||
settings.allow_safety_identifier =
|
||||
localInputs.allow_safety_identifier === true;
|
||||
settings.allow_include_obfuscation =
|
||||
localInputs.allow_include_obfuscation === true;
|
||||
}
|
||||
if (localInputs.type === 14) {
|
||||
settings.allow_inference_geo = localInputs.allow_inference_geo === true;
|
||||
settings.claude_beta_query = localInputs.claude_beta_query === true;
|
||||
}
|
||||
}
|
||||
@@ -1599,6 +1695,8 @@ const EditChannelModal = (props) => {
|
||||
delete localInputs.allow_service_tier;
|
||||
delete localInputs.disable_store;
|
||||
delete localInputs.allow_safety_identifier;
|
||||
delete localInputs.allow_include_obfuscation;
|
||||
delete localInputs.allow_inference_geo;
|
||||
delete localInputs.claude_beta_query;
|
||||
|
||||
let res;
|
||||
@@ -2917,9 +3015,18 @@ const EditChannelModal = (props) => {
|
||||
rules={[{ required: true, message: t('请选择模型') }]}
|
||||
multiple
|
||||
filter={selectFilter}
|
||||
allowCreate
|
||||
autoClearSearchValue={false}
|
||||
searchPosition='dropdown'
|
||||
optionList={modelOptions}
|
||||
onSearch={(value) => setModelSearchValue(value)}
|
||||
innerBottomSlot={
|
||||
modelSearchHintText ? (
|
||||
<Text className='px-3 py-2 block text-xs !text-semi-color-text-2'>
|
||||
{modelSearchHintText}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => handleInputChange('models', value)}
|
||||
renderSelectedItem={(optionNode) => {
|
||||
@@ -3444,6 +3551,24 @@ const EditChannelModal = (props) => {
|
||||
'safety_identifier 字段用于帮助 OpenAI 识别可能违反使用政策的应用程序用户。默认关闭以保护用户隐私',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='allow_include_obfuscation'
|
||||
label={t(
|
||||
'允许 stream_options.include_obfuscation 透传',
|
||||
)}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'allow_include_obfuscation',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'include_obfuscation 用于控制 Responses 流混淆字段。默认关闭以避免客户端关闭该安全保护',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -3469,6 +3594,22 @@ const EditChannelModal = (props) => {
|
||||
'service_tier 字段用于指定服务层级,允许透传可能导致实际计费高于预期。默认关闭以避免额外费用',
|
||||
)}
|
||||
/>
|
||||
|
||||
<Form.Switch
|
||||
field='allow_inference_geo'
|
||||
label={t('允许 inference_geo 透传')}
|
||||
checkedText={t('开')}
|
||||
uncheckedText={t('关')}
|
||||
onChange={(value) =>
|
||||
handleChannelOtherSettingsChange(
|
||||
'allow_inference_geo',
|
||||
value,
|
||||
)
|
||||
}
|
||||
extraText={t(
|
||||
'inference_geo 字段用于控制 Claude 数据驻留推理区域。默认关闭以避免未经授权透传地域信息',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
@@ -3613,6 +3754,12 @@ const EditChannelModal = (props) => {
|
||||
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
||||
/>
|
||||
</SideSheet>
|
||||
<StatusCodeRiskGuardModal
|
||||
visible={statusCodeRiskConfirmVisible}
|
||||
detailItems={statusCodeRiskDetailItems}
|
||||
onCancel={() => resolveStatusCodeRiskConfirm(false)}
|
||||
onConfirm={() => resolveStatusCodeRiskConfirm(true)}
|
||||
/>
|
||||
{/* 使用通用安全验证模态框 */}
|
||||
<SecureVerificationModal
|
||||
visible={isModalVisible}
|
||||
|
||||
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import {
|
||||
API,
|
||||
showError,
|
||||
@@ -64,6 +64,7 @@ const EditTagModal = (props) => {
|
||||
const [modelOptions, setModelOptions] = useState([]);
|
||||
const [groupOptions, setGroupOptions] = useState([]);
|
||||
const [customModel, setCustomModel] = useState('');
|
||||
const [modelSearchValue, setModelSearchValue] = useState('');
|
||||
const originInputs = {
|
||||
tag: '',
|
||||
new_tag: null,
|
||||
@@ -74,6 +75,25 @@ const EditTagModal = (props) => {
|
||||
header_override: null,
|
||||
};
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
const modelSearchMatchedCount = useMemo(() => {
|
||||
const keyword = modelSearchValue.trim();
|
||||
if (!keyword) {
|
||||
return modelOptions.length;
|
||||
}
|
||||
return modelOptions.reduce(
|
||||
(count, option) => count + (selectFilter(keyword, option) ? 1 : 0),
|
||||
0,
|
||||
);
|
||||
}, [modelOptions, modelSearchValue]);
|
||||
const modelSearchHintText = useMemo(() => {
|
||||
const keyword = modelSearchValue.trim();
|
||||
if (!keyword || modelSearchMatchedCount !== 0) {
|
||||
return '';
|
||||
}
|
||||
return t('未匹配到模型,按回车键可将「{{name}}」作为自定义模型名添加', {
|
||||
name: keyword,
|
||||
});
|
||||
}, [modelSearchMatchedCount, modelSearchValue, t]);
|
||||
const formApiRef = useRef(null);
|
||||
const getInitValues = () => ({ ...originInputs });
|
||||
|
||||
@@ -292,6 +312,7 @@ const EditTagModal = (props) => {
|
||||
fetchModels().then();
|
||||
fetchGroups().then();
|
||||
fetchTagModels().then();
|
||||
setModelSearchValue('');
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValues({
|
||||
...getInitValues(),
|
||||
@@ -461,9 +482,18 @@ const EditTagModal = (props) => {
|
||||
placeholder={t('请选择该渠道所支持的模型,留空则不更改')}
|
||||
multiple
|
||||
filter={selectFilter}
|
||||
allowCreate
|
||||
autoClearSearchValue={false}
|
||||
searchPosition='dropdown'
|
||||
optionList={modelOptions}
|
||||
onSearch={(value) => setModelSearchValue(value)}
|
||||
innerBottomSlot={
|
||||
modelSearchHintText ? (
|
||||
<Text className='px-3 py-2 block text-xs !text-semi-color-text-2'>
|
||||
{modelSearchHintText}
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => handleInputChange('models', value)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { useMemo } 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 = React.memo(function StatusCodeRiskGuardModal({
|
||||
visible,
|
||||
detailItems,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const checklist = useMemo(
|
||||
() => STATUS_CODE_RISK_CHECKLIST_KEYS.map((item) => t(item)),
|
||||
[t, i18n.language],
|
||||
);
|
||||
|
||||
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={checklist}
|
||||
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;
|
||||
132
web/src/components/table/channels/modals/statusCodeRiskGuard.js
Normal file
132
web/src/components/table/channels/modals/statusCodeRiskGuard.js
Normal file
@@ -0,0 +1,132 @@
|
||||
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 collectInvalidStatusCodeEntries(statusCodeMappingStr) {
|
||||
if (
|
||||
typeof statusCodeMappingStr !== 'string' ||
|
||||
statusCodeMappingStr.trim() === ''
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(statusCodeMappingStr);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const invalid = [];
|
||||
for (const [rawKey, rawValue] of Object.entries(parsed)) {
|
||||
const fromCode = parseStatusCodeKey(rawKey);
|
||||
const toCode = parseStatusCodeMappingTarget(rawValue);
|
||||
if (fromCode === null || toCode === null) {
|
||||
invalid.push(`${rawKey} → ${rawValue}`);
|
||||
}
|
||||
}
|
||||
|
||||
return invalid;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user