Skip to content
Closed
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
23 changes: 18 additions & 5 deletions web/src/components/common/ui/JSONEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const JSONEditor = ({
editorType = 'keyValue',
rules = [],
formApi = null,
customSwitch = null,
...props
}) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -442,6 +443,7 @@ const JSONEditor = ({
keyValuePairs.slice(index + 1).every((p) => p.key !== pair.key);

return (

<Row key={pair.id} gutter={8} align='middle'>
<Col span={10}>
<div className='relative'>
Expand Down Expand Up @@ -642,11 +644,22 @@ const JSONEditor = ({
<TabPane tab={t('手动编辑')} itemKey='manual' />
</Tabs>

{template && templateLabel && (
<Button type='tertiary' onClick={fillTemplate} size='small'>
{templateLabel}
</Button>
)}
<div className="flex items-center gap-3">
{customSwitch && (
<div className="flex items-center gap-2">
{customSwitch}
</div>
)}
{template && templateLabel && (
<Button
type="tertiary"
onClick={fillTemplate}
size="small"
>
{templateLabel}
</Button>
)}
</div>
</div>
}
headerStyle={{ padding: '12px 16px' }}
Expand Down
203 changes: 202 additions & 1 deletion web/src/components/table/channels/modals/EditChannelModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
Button,
Typography,
Checkbox,
Switch,
Banner,
Modal,
ImagePreview,
Expand Down Expand Up @@ -167,6 +168,7 @@ const EditChannelModal = (props) => {
const [channelSearchValue, setChannelSearchValue] = useState('');
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
const [enableModelMappingSync, setEnableModelMappingSync] = useState(false); // 是否启用模型映射同步功能

// 2FA验证查看密钥相关状态
const [twoFAState, setTwoFAState] = useState({
Expand Down Expand Up @@ -261,6 +263,143 @@ const EditChannelModal = (props) => {
handleInputChange('settings', settingsJson);
};

// 用于追踪模型的原始名称映射关系 { displayName: originalName }
const [modelOriginalMapping, setModelOriginalMapping] = useState({});

// 解析模型映射配置的工具函数
const parseModelMapping = (mappingValue) => {
if (!mappingValue || typeof mappingValue !== 'string' || mappingValue.trim() === '') {
return null;
}

try {
const mapping = JSON.parse(mappingValue);
if (typeof mapping !== 'object' || mapping === null) {
return null;
}
return mapping;
} catch (error) {
console.warn('模型重定向 JSON 解析失败:', error);
return null;
}
};

// 获取当前模型列表的工具函数
const getCurrentModels = () => {
return formApiRef.current
? formApiRef.current.getValue('models') || []
: inputs.models || [];
};

// 更新模型列表的统一方法
const updateModelsList = (newModels, newMapping) => {
const uniqueModels = Array.from(new Set(newModels.map(m => (m || '').trim()).filter(Boolean)));

setInputs((inputs) => ({ ...inputs, models: uniqueModels }));
if (formApiRef.current) {
formApiRef.current.setValue('models', uniqueModels);
}
setModelOriginalMapping(newMapping);
};

// 恢复模型到原始名称
const restoreModelsToOriginalNames = () => {
const currentModels = getCurrentModels();
const restoredModels = currentModels.map(model => modelOriginalMapping[model] || model);

// 使用数组比较而不是JSON.stringify提高性能
const hasChanges = currentModels.length !== restoredModels.length ||
currentModels.some((model, index) => model !== restoredModels[index]);

if (hasChanges) {
updateModelsList(restoredModels, {});
}
};

// 应用模型映射的核心逻辑
const applyModelMapping = (mapping, currentModels, currentMapping) => {
// 只有在启用模型映射同步功能时才执行
if (!enableModelMappingSync) {
return { updatedModels: currentModels, newMapping: currentMapping, hasChanges: false };
}

let updatedModels = [...currentModels];
let newMapping = { ...currentMapping };
let hasChanges = false;

// 遍历重定向映射
Object.entries(mapping).forEach(([key, mappedValue]) => {
if (typeof key === 'string' && typeof mappedValue === 'string') {
const keyTrimmed = key.trim();
const valueTrimmed = mappedValue.trim();

if (keyTrimmed && valueTrimmed) {
// 查找模型配置中是否存在重定向的"值"(原始模型名)
const valueIndex = updatedModels.findIndex(model => {
return model === valueTrimmed || newMapping[model] === valueTrimmed;
});

if (valueIndex !== -1) {
const currentDisplayName = updatedModels[valueIndex];
if (currentDisplayName !== keyTrimmed) {
// 记录原始映射关系
if (!newMapping[keyTrimmed]) {
newMapping[keyTrimmed] = newMapping[currentDisplayName] || currentDisplayName;
}
// 清理旧的映射关系
if (newMapping[currentDisplayName]) {
delete newMapping[currentDisplayName];
}
// 更新显示名称为重定向的键
updatedModels[valueIndex] = keyTrimmed;
hasChanges = true;
}
}
}
}
});

// 处理不在映射中的模型,恢复为原始名称
const mappingKeys = new Set(Object.keys(mapping).map(key => key.trim()));
updatedModels = updatedModels.map(model => {
if (!mappingKeys.has(model) && newMapping[model]) {
const originalName = newMapping[model];
delete newMapping[model];
hasChanges = true;
return originalName;
}
return model;
});

return { updatedModels, newMapping, hasChanges };
};

// 实时同步模型重定向到模型配置的函数
const syncModelMappingToModels = (mappingValue) => {
// 只有在启用模型映射同步功能时才执行
if (!enableModelMappingSync) {
return;
}

const mapping = parseModelMapping(mappingValue);

if (!mapping) {
restoreModelsToOriginalNames();
return;
}

const currentModels = getCurrentModels();
const { updatedModels, newMapping, hasChanges } = applyModelMapping(
mapping,
currentModels,
modelOriginalMapping
);

if (hasChanges) {
updateModelsList(updatedModels, newMapping);
}
};

const handleInputChange = (name, value) => {
if (formApiRef.current) {
formApiRef.current.setValue(name, value);
Expand All @@ -280,6 +419,14 @@ const EditChannelModal = (props) => {
});
return;
}

// 处理模型重定向变更时自动同步模型配置(实时同步)
if (name === 'model_mapping') {
setInputs((inputs) => ({ ...inputs, [name]: value }));
syncModelMappingToModels(value);
return;
}

setInputs((inputs) => ({ ...inputs, [name]: value }));
if (name === 'type') {
let localModels = [];
Expand Down Expand Up @@ -426,6 +573,22 @@ const EditChannelModal = (props) => {
setAutoBan(true);
}
setBasicModels(getChannelModels(data.type));

// 初始化模型原始映射关系
const mapping = parseModelMapping(data.model_mapping);
if (mapping) {
const initialMapping = {};
// 根据当前的模型映射和模型列表,建立原始映射关系
Object.entries(mapping).forEach(([key, value]) => {
if (data.models.includes(key)) {
initialMapping[key] = value;
}
});
setModelOriginalMapping(initialMapping);
} else {
setModelOriginalMapping({});
}

Comment thread
AAEE86 marked this conversation as resolved.
// 同步更新channelSettings状态显示
setChannelSettings({
force_format: data.force_format,
Expand Down Expand Up @@ -694,8 +857,19 @@ const EditChannelModal = (props) => {
resetTwoFAState();
// 重置2FA验证状态
reset2FAVerifyState();
// 重置模型原始映射关系
setModelOriginalMapping({});
// 重置模型映射同步开关状态
setEnableModelMappingSync(false);
};

// 组件卸载时清理资源
useEffect(() => {
return () => {
setModelOriginalMapping({});
};
}, []);

const handleVertexUploadChange = ({ fileList }) => {
vertexErroredNames.current.clear();
(async () => {
Expand Down Expand Up @@ -1947,7 +2121,34 @@ const EditChannelModal = (props) => {
templateLabel={t('填入模板')}
editorType='keyValue'
formApi={formApiRef.current}
extraText={t('键为请求中的模型名称,值为要替换的模型名称')}
customSwitch={
<>
<Text className="text-sm">{t('启用自动同步到模型配置')}</Text>
<Switch
checked={enableModelMappingSync}
onChange={(checked) => {
setEnableModelMappingSync(checked);
const mappingStr =
formApiRef.current?.getValue('model_mapping') ?? inputs.model_mapping;
if (checked) {
syncModelMappingToModels(mappingStr);
} else {
restoreModelsToOriginalNames();
}
}}
/>
</>
}
extraText={
<div>
<div className="text-center">{t('键为请求中的模型名称,值为要替换的模型名称')}</div>
<div className="text-blue-600 text-xs mt-1">
{enableModelMappingSync
? t(' 提示:设置重定向后,系统自动将"模型配置"中对应的"值"替换为"键"')
: t(' 提示:当前已关闭自动同步,模型重定向不会影响模型配置列表')}
</div>
</div>
}
/>
</Card>

Expand Down
3 changes: 3 additions & 0 deletions web/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1979,6 +1979,7 @@
"浅色": "Light",
"点击复制模型名称": "Click to copy model name",
"已复制:{{name}}": "Copied: {{name}}",
Comment thread
AAEE86 marked this conversation as resolved.
"💡 提示:设置重定向后,系统自动将“模型配置”中对应的“值”替换为“键”": "💡 Tip: After setting the redirect, the system automatically replaces the corresponding \"value\" in \"Model Configuration\" with \"key\"",
"所有密钥已复制到剪贴板": "All keys have been copied to the clipboard",
"密钥已复制到剪贴板": "Key copied to clipboard",
"验证成功": "Verification successful",
Expand All @@ -1988,6 +1989,8 @@
"复制全部": "Copy all",
"JSON格式密钥,请确保格式正确": "JSON format key, please ensure the format is correct",
"检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。": "Detected multiple keys, you can copy each key individually or click Copy All to get the complete content.",
"启用自动同步到模型配置": "Enable auto sync to model configuration",
"💡 提示:当前已关闭自动同步,模型重定向不会影响模型配置列表": "💡 Tip: Auto sync is currently disabled, model redirects will not affect the model configuration list",
"安全提醒": "Security reminder",
"请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Keep key information secure, do not disclose to others. If there are security concerns, please change the key immediately.",
"安全验证": "Security verification",
Expand Down