mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-03-30 02:05:21 +00:00
feat(channel): 添加2FA验证后查看渠道密钥功能
- 新增接口通过2FA验证后获取渠道密钥 - 统一实现2FA验证码和备用码的验证逻辑 - 记录用户查看密钥的操作日志 - 编辑渠道弹窗新增查看密钥按钮,触发2FA验证模态框 - 使用TwoFactorAuthModal进行验证码输入及验证 - 验证成功后弹出渠道密钥展示窗口 - 对渠道编辑模态框的状态进行了统一重置优化 - 添加相关国际化文案支持密钥查看功能
This commit is contained in:
@@ -380,6 +380,85 @@ func GetChannel(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// GetChannelKey 验证2FA后获取渠道密钥
|
||||
func GetChannelKey(c *gin.Context) {
|
||||
type GetChannelKeyRequest struct {
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
||||
|
||||
var req GetChannelKeyRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
common.ApiError(c, fmt.Errorf("参数错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
userId := c.GetInt("id")
|
||||
channelId, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
common.ApiError(c, fmt.Errorf("渠道ID格式错误: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取2FA记录并验证
|
||||
twoFA, err := model.GetTwoFAByUserId(userId)
|
||||
if err != nil {
|
||||
common.ApiError(c, fmt.Errorf("获取2FA信息失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if twoFA == nil || !twoFA.IsEnabled {
|
||||
common.ApiError(c, fmt.Errorf("用户未启用2FA,无法查看密钥"))
|
||||
return
|
||||
}
|
||||
|
||||
// 统一的2FA验证逻辑
|
||||
if !validateTwoFactorAuth(twoFA, req.Code) {
|
||||
common.ApiError(c, fmt.Errorf("验证码或备用码错误,请重试"))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取渠道信息(包含密钥)
|
||||
channel, err := model.GetChannelById(channelId, true)
|
||||
if err != nil {
|
||||
common.ApiError(c, fmt.Errorf("获取渠道信息失败: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if channel == nil {
|
||||
common.ApiError(c, fmt.Errorf("渠道不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
// 记录操作日志
|
||||
model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("查看渠道密钥信息 (渠道ID: %d)", channelId))
|
||||
|
||||
// 统一的成功响应格式
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "验证成功",
|
||||
"data": map[string]interface{}{
|
||||
"key": channel.Key,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// validateTwoFactorAuth 统一的2FA验证函数
|
||||
func validateTwoFactorAuth(twoFA *model.TwoFA, code string) bool {
|
||||
// 尝试验证TOTP
|
||||
if cleanCode, err := common.ValidateNumericCode(code); err == nil {
|
||||
if isValid, _ := twoFA.ValidateTOTPAndUpdateUsage(cleanCode); isValid {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试验证备用码
|
||||
if isValid, err := twoFA.ValidateBackupCodeAndUpdateUsage(code); err == nil && isValid {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// validateChannel 通用的渠道校验函数
|
||||
func validateChannel(channel *model.Channel, isAdd bool) error {
|
||||
// 校验 channel settings
|
||||
|
||||
@@ -114,6 +114,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
channelRoute.GET("/models", controller.ChannelListModels)
|
||||
channelRoute.GET("/models_enabled", controller.EnabledListModels)
|
||||
channelRoute.GET("/:id", controller.GetChannel)
|
||||
channelRoute.POST("/:id/key", controller.GetChannelKey)
|
||||
channelRoute.GET("/test", controller.TestAllChannels)
|
||||
channelRoute.GET("/test/:id", controller.TestChannel)
|
||||
channelRoute.GET("/update_balance", controller.UpdateAllChannelsBalance)
|
||||
|
||||
128
web/src/components/common/modals/TwoFactorAuthModal.jsx
Normal file
128
web/src/components/common/modals/TwoFactorAuthModal.jsx
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
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 from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Button, Input, Typography } from '@douyinfe/semi-ui';
|
||||
|
||||
/**
|
||||
* 可复用的两步验证模态框组件
|
||||
* @param {Object} props
|
||||
* @param {boolean} props.visible - 是否显示模态框
|
||||
* @param {string} props.code - 验证码值
|
||||
* @param {boolean} props.loading - 是否正在验证
|
||||
* @param {Function} props.onCodeChange - 验证码变化回调
|
||||
* @param {Function} props.onVerify - 验证回调
|
||||
* @param {Function} props.onCancel - 取消回调
|
||||
* @param {string} props.title - 模态框标题
|
||||
* @param {string} props.description - 验证描述文本
|
||||
* @param {string} props.placeholder - 输入框占位文本
|
||||
*/
|
||||
const TwoFactorAuthModal = ({
|
||||
visible,
|
||||
code,
|
||||
loading,
|
||||
onCodeChange,
|
||||
onVerify,
|
||||
onCancel,
|
||||
title,
|
||||
description,
|
||||
placeholder
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleKeyPress = (e) => {
|
||||
if (e.key === 'Enter' && code) {
|
||||
onVerify();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center mr-3">
|
||||
<svg className="w-4 h-4 text-blue-600 dark:text-blue-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{title || t('安全验证')}
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
onCancel={onCancel}
|
||||
footer={
|
||||
<>
|
||||
<Button onClick={onCancel}>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={loading}
|
||||
disabled={!code}
|
||||
onClick={onVerify}
|
||||
>
|
||||
{t('验证')}
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
width={500}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* 安全提示 */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-4">
|
||||
<div className="flex items-start">
|
||||
<svg className="w-5 h-5 text-blue-600 dark:text-blue-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<Typography.Text strong className="text-blue-800 dark:text-blue-200">
|
||||
{t('安全验证')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="block text-blue-700 dark:text-blue-300 text-sm mt-1">
|
||||
{description || t('为了保护账户安全,请验证您的两步验证码。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 验证码输入 */}
|
||||
<div>
|
||||
<Typography.Text strong className="block mb-2">
|
||||
{t('验证身份')}
|
||||
</Typography.Text>
|
||||
<Input
|
||||
placeholder={placeholder || t('请输入认证器验证码或备用码')}
|
||||
value={code}
|
||||
onChange={onCodeChange}
|
||||
size="large"
|
||||
maxLength={8}
|
||||
onKeyPress={handleKeyPress}
|
||||
/>
|
||||
<Typography.Text type="tertiary" size="small" className="mt-2 block">
|
||||
{t('支持6位TOTP验证码或8位备用码')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default TwoFactorAuthModal;
|
||||
224
web/src/components/common/ui/ChannelKeyDisplay.jsx
Normal file
224
web/src/components/common/ui/ChannelKeyDisplay.jsx
Normal file
@@ -0,0 +1,224 @@
|
||||
/*
|
||||
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 from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, Button, Typography, Tag } from '@douyinfe/semi-ui';
|
||||
import { copy, showSuccess } from '../../../helpers';
|
||||
|
||||
/**
|
||||
* 解析密钥数据,支持多种格式
|
||||
* @param {string} keyData - 密钥数据
|
||||
* @param {Function} t - 翻译函数
|
||||
* @returns {Array} 解析后的密钥数组
|
||||
*/
|
||||
const parseChannelKeys = (keyData, t) => {
|
||||
if (!keyData) return [];
|
||||
|
||||
const trimmed = keyData.trim();
|
||||
|
||||
// 检查是否是JSON数组格式(如Vertex AI)
|
||||
if (trimmed.startsWith('[')) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (Array.isArray(parsed)) {
|
||||
return parsed.map((item, index) => ({
|
||||
id: index,
|
||||
content: typeof item === 'string' ? item : JSON.stringify(item, null, 2),
|
||||
type: typeof item === 'string' ? 'text' : 'json',
|
||||
label: `${t('密钥')} ${index + 1}`
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
// 如果解析失败,按普通文本处理
|
||||
console.warn('Failed to parse JSON keys:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否是多行密钥(按换行符分割)
|
||||
const lines = trimmed.split('\n').filter(line => line.trim());
|
||||
if (lines.length > 1) {
|
||||
return lines.map((line, index) => ({
|
||||
id: index,
|
||||
content: line.trim(),
|
||||
type: 'text',
|
||||
label: `${t('密钥')} ${index + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
// 单个密钥
|
||||
return [{
|
||||
id: 0,
|
||||
content: trimmed,
|
||||
type: trimmed.startsWith('{') ? 'json' : 'text',
|
||||
label: t('密钥')
|
||||
}];
|
||||
};
|
||||
|
||||
/**
|
||||
* 可复用的密钥显示组件
|
||||
* @param {Object} props
|
||||
* @param {string} props.keyData - 密钥数据
|
||||
* @param {boolean} props.showSuccessIcon - 是否显示成功图标
|
||||
* @param {string} props.successText - 成功文本
|
||||
* @param {boolean} props.showWarning - 是否显示安全警告
|
||||
* @param {string} props.warningText - 警告文本
|
||||
*/
|
||||
const ChannelKeyDisplay = ({
|
||||
keyData,
|
||||
showSuccessIcon = true,
|
||||
successText,
|
||||
showWarning = true,
|
||||
warningText
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const parsedKeys = parseChannelKeys(keyData, t);
|
||||
const isMultipleKeys = parsedKeys.length > 1;
|
||||
|
||||
const handleCopyAll = () => {
|
||||
copy(keyData);
|
||||
showSuccess(t('所有密钥已复制到剪贴板'));
|
||||
};
|
||||
|
||||
const handleCopyKey = (content) => {
|
||||
copy(content);
|
||||
showSuccess(t('密钥已复制到剪贴板'));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* 成功状态 */}
|
||||
{showSuccessIcon && (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-green-600" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<Typography.Text strong className="text-green-700">
|
||||
{successText || t('验证成功')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 密钥内容 */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text strong>
|
||||
{isMultipleKeys ? t('渠道密钥列表') : t('渠道密钥')}
|
||||
</Typography.Text>
|
||||
{isMultipleKeys && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Typography.Text type="tertiary" size="small">
|
||||
{t('共 {{count}} 个密钥', { count: parsedKeys.length })}
|
||||
</Typography.Text>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
onClick={handleCopyAll}
|
||||
>
|
||||
{t('复制全部')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-80 overflow-auto">
|
||||
{parsedKeys.map((keyItem) => (
|
||||
<Card key={keyItem.id} className="!rounded-lg !border !border-gray-200 dark:!border-gray-700">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Typography.Text strong size="small" className="text-gray-700 dark:text-gray-300">
|
||||
{keyItem.label}
|
||||
</Typography.Text>
|
||||
<div className="flex items-center gap-2">
|
||||
{keyItem.type === 'json' && (
|
||||
<Tag size="small" color="blue">{t('JSON')}</Tag>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
icon={
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
||||
</svg>
|
||||
}
|
||||
onClick={() => handleCopyKey(keyItem.content)}
|
||||
>
|
||||
{t('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-800 rounded-lg p-3 max-h-40 overflow-auto">
|
||||
<Typography.Text
|
||||
code
|
||||
className="text-xs font-mono break-all whitespace-pre-wrap text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
{keyItem.content}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
|
||||
{keyItem.type === 'json' && (
|
||||
<Typography.Text type="tertiary" size="small" className="block">
|
||||
{t('JSON格式密钥,请确保格式正确')}
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isMultipleKeys && (
|
||||
<div className="bg-blue-50 dark:bg-blue-900 rounded-lg p-3">
|
||||
<Typography.Text type="tertiary" size="small" className="text-blue-700 dark:text-blue-300">
|
||||
<svg className="w-4 h-4 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clipRule="evenodd" />
|
||||
</svg>
|
||||
{t('检测到多个密钥,您可以单独复制每个密钥,或点击复制全部获取完整内容。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 安全警告 */}
|
||||
{showWarning && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900 rounded-lg p-4">
|
||||
<div className="flex items-start">
|
||||
<svg className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
<div>
|
||||
<Typography.Text strong className="text-yellow-800 dark:text-yellow-200">
|
||||
{t('安全提醒')}
|
||||
</Typography.Text>
|
||||
<Typography.Text className="block text-yellow-700 dark:text-yellow-300 text-sm mt-1">
|
||||
{warningText || t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelKeyDisplay;
|
||||
@@ -45,10 +45,13 @@ import {
|
||||
Row,
|
||||
Col,
|
||||
Highlight,
|
||||
Input,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { getChannelModels, copy, getChannelIcon, getModelCategories, selectFilter } from '../../../../helpers';
|
||||
import ModelSelectModal from './ModelSelectModal';
|
||||
import JSONEditor from '../../../common/ui/JSONEditor';
|
||||
import TwoFactorAuthModal from '../../../common/modals/TwoFactorAuthModal';
|
||||
import ChannelKeyDisplay from '../../../common/ui/ChannelKeyDisplay';
|
||||
import {
|
||||
IconSave,
|
||||
IconClose,
|
||||
@@ -158,6 +161,44 @@ const EditChannelModal = (props) => {
|
||||
const [channelSearchValue, setChannelSearchValue] = useState('');
|
||||
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
|
||||
const [keyMode, setKeyMode] = useState('append'); // 密钥模式:replace(覆盖)或 append(追加)
|
||||
|
||||
// 2FA验证查看密钥相关状态
|
||||
const [twoFAState, setTwoFAState] = useState({
|
||||
showModal: false,
|
||||
code: '',
|
||||
loading: false,
|
||||
showKey: false,
|
||||
keyData: ''
|
||||
});
|
||||
|
||||
// 专门的2FA验证状态(用于TwoFactorAuthModal)
|
||||
const [show2FAVerifyModal, setShow2FAVerifyModal] = useState(false);
|
||||
const [verifyCode, setVerifyCode] = useState('');
|
||||
const [verifyLoading, setVerifyLoading] = useState(false);
|
||||
|
||||
// 2FA状态更新辅助函数
|
||||
const updateTwoFAState = (updates) => {
|
||||
setTwoFAState(prev => ({ ...prev, ...updates }));
|
||||
};
|
||||
|
||||
// 重置2FA状态
|
||||
const resetTwoFAState = () => {
|
||||
setTwoFAState({
|
||||
showModal: false,
|
||||
code: '',
|
||||
loading: false,
|
||||
showKey: false,
|
||||
keyData: ''
|
||||
});
|
||||
};
|
||||
|
||||
// 重置2FA验证状态
|
||||
const reset2FAVerifyState = () => {
|
||||
setShow2FAVerifyModal(false);
|
||||
setVerifyCode('');
|
||||
setVerifyLoading(false);
|
||||
};
|
||||
|
||||
// 渠道额外设置状态
|
||||
const [channelSettings, setChannelSettings] = useState({
|
||||
force_format: false,
|
||||
@@ -500,6 +541,42 @@ const EditChannelModal = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
// 使用TwoFactorAuthModal的验证函数
|
||||
const handleVerify2FA = async () => {
|
||||
if (!verifyCode) {
|
||||
showError(t('请输入验证码或备用码'));
|
||||
return;
|
||||
}
|
||||
|
||||
setVerifyLoading(true);
|
||||
try {
|
||||
const res = await API.post(`/api/channel/${channelId}/key`, {
|
||||
code: verifyCode
|
||||
});
|
||||
if (res.data.success) {
|
||||
// 验证成功,显示密钥
|
||||
updateTwoFAState({
|
||||
showModal: true,
|
||||
showKey: true,
|
||||
keyData: res.data.data.key
|
||||
});
|
||||
reset2FAVerifyState();
|
||||
showSuccess(t('验证成功'));
|
||||
} else {
|
||||
showError(res.data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
showError(t('获取密钥失败'));
|
||||
} finally {
|
||||
setVerifyLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 显示2FA验证模态框 - 使用TwoFactorAuthModal
|
||||
const handleShow2FAModal = () => {
|
||||
setShow2FAVerifyModal(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const modelMap = new Map();
|
||||
|
||||
@@ -576,27 +653,37 @@ const EditChannelModal = (props) => {
|
||||
// 重置手动输入模式状态
|
||||
setUseManualInput(false);
|
||||
} else {
|
||||
formApiRef.current?.reset();
|
||||
// 重置渠道设置状态
|
||||
setChannelSettings({
|
||||
force_format: false,
|
||||
thinking_to_content: false,
|
||||
proxy: '',
|
||||
pass_through_body_enabled: false,
|
||||
system_prompt: '',
|
||||
system_prompt_override: false,
|
||||
});
|
||||
// 重置密钥模式状态
|
||||
setKeyMode('append');
|
||||
// 清空表单中的key_mode字段
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key_mode', undefined);
|
||||
}
|
||||
// 重置本地输入,避免下次打开残留上一次的 JSON 字段值
|
||||
setInputs(getInitValues());
|
||||
// 统一的模态框关闭重置逻辑
|
||||
resetModalState();
|
||||
}
|
||||
}, [props.visible, channelId]);
|
||||
|
||||
// 统一的模态框重置函数
|
||||
const resetModalState = () => {
|
||||
formApiRef.current?.reset();
|
||||
// 重置渠道设置状态
|
||||
setChannelSettings({
|
||||
force_format: false,
|
||||
thinking_to_content: false,
|
||||
proxy: '',
|
||||
pass_through_body_enabled: false,
|
||||
system_prompt: '',
|
||||
system_prompt_override: false,
|
||||
});
|
||||
// 重置密钥模式状态
|
||||
setKeyMode('append');
|
||||
// 清空表单中的key_mode字段
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key_mode', undefined);
|
||||
}
|
||||
// 重置本地输入,避免下次打开残留上一次的 JSON 字段值
|
||||
setInputs(getInitValues());
|
||||
// 重置2FA状态
|
||||
resetTwoFAState();
|
||||
// 重置2FA验证状态
|
||||
reset2FAVerifyState();
|
||||
};
|
||||
|
||||
const handleVertexUploadChange = ({ fileList }) => {
|
||||
vertexErroredNames.current.clear();
|
||||
(async () => {
|
||||
@@ -1080,6 +1167,16 @@ const EditChannelModal = (props) => {
|
||||
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
|
||||
</Text>
|
||||
)}
|
||||
{isEdit && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
onClick={handleShow2FAModal}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</div>
|
||||
}
|
||||
@@ -1154,6 +1251,16 @@ const EditChannelModal = (props) => {
|
||||
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
|
||||
</Text>
|
||||
)}
|
||||
{isEdit && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
onClick={handleShow2FAModal}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</div>
|
||||
}
|
||||
@@ -1194,6 +1301,16 @@ const EditChannelModal = (props) => {
|
||||
{t('追加模式:新密钥将添加到现有密钥列表的末尾')}
|
||||
</Text>
|
||||
)}
|
||||
{isEdit && (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
theme="outline"
|
||||
onClick={handleShow2FAModal}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</div>
|
||||
}
|
||||
@@ -1846,6 +1963,53 @@ const EditChannelModal = (props) => {
|
||||
onVisibleChange={(visible) => setIsModalOpenurl(visible)}
|
||||
/>
|
||||
</SideSheet>
|
||||
{/* 使用TwoFactorAuthModal组件进行2FA验证 */}
|
||||
<TwoFactorAuthModal
|
||||
visible={show2FAVerifyModal}
|
||||
code={verifyCode}
|
||||
loading={verifyLoading}
|
||||
onCodeChange={setVerifyCode}
|
||||
onVerify={handleVerify2FA}
|
||||
onCancel={reset2FAVerifyState}
|
||||
title={t('查看渠道密钥')}
|
||||
description={t('为了保护账户安全,请验证您的两步验证码。')}
|
||||
placeholder={t('请输入验证码或备用码')}
|
||||
/>
|
||||
|
||||
{/* 使用ChannelKeyDisplay组件显示密钥 */}
|
||||
<Modal
|
||||
title={
|
||||
<div className="flex items-center">
|
||||
<div className="w-8 h-8 rounded-full bg-green-100 dark:bg-green-900 flex items-center justify-center mr-3">
|
||||
<svg className="w-4 h-4 text-green-600 dark:text-green-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{t('渠道密钥信息')}
|
||||
</div>
|
||||
}
|
||||
visible={twoFAState.showModal && twoFAState.showKey}
|
||||
onCancel={resetTwoFAState}
|
||||
footer={
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={resetTwoFAState}
|
||||
>
|
||||
{t('完成')}
|
||||
</Button>
|
||||
}
|
||||
width={700}
|
||||
style={{ maxWidth: '90vw' }}
|
||||
>
|
||||
<ChannelKeyDisplay
|
||||
keyData={twoFAState.keyData}
|
||||
showSuccessIcon={true}
|
||||
successText={t('密钥获取成功')}
|
||||
showWarning={true}
|
||||
warningText={t('请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。')}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<ModelSelectModal
|
||||
visible={modelModalVisible}
|
||||
models={fetchedModels}
|
||||
|
||||
@@ -1997,5 +1997,25 @@
|
||||
"深色": "Dark",
|
||||
"浅色": "Light",
|
||||
"点击复制模型名称": "Click to copy model name",
|
||||
"已复制:{{name}}": "Copied: {{name}}"
|
||||
"已复制:{{name}}": "Copied: {{name}}",
|
||||
"所有密钥已复制到剪贴板": "All keys have been copied to the clipboard",
|
||||
"密钥已复制到剪贴板": "Key copied to clipboard",
|
||||
"验证成功": "Verification successful",
|
||||
"渠道密钥列表": "Channel key list",
|
||||
"渠道密钥": "Channel key",
|
||||
"共 {{count}} 个密钥": "{{count}} keys in total",
|
||||
"复制全部": "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.",
|
||||
"安全提醒": "Security reminder",
|
||||
"请妥善保管密钥信息,不要泄露给他人。如有安全疑虑,请及时更换密钥。": "Keep key information secure, do not disclose to others. If there are security concerns, please change the key immediately.",
|
||||
"安全验证": "Security verification",
|
||||
"验证": "Verify",
|
||||
"为了保护账户安全,请验证您的两步验证码。": "To protect account security, please verify your two-factor authentication code.",
|
||||
"支持6位TOTP验证码或8位备用码": "Supports 6-digit TOTP verification code or 8-digit backup code",
|
||||
"获取密钥失败": "Failed to get key",
|
||||
"查看密钥": "View key",
|
||||
"查看渠道密钥": "View channel key",
|
||||
"渠道密钥信息": "Channel key information",
|
||||
"密钥获取成功": "Key acquisition successful"
|
||||
}
|
||||
Reference in New Issue
Block a user