feat(channel): 添加2FA验证后查看渠道密钥功能

- 新增接口通过2FA验证后获取渠道密钥
- 统一实现2FA验证码和备用码的验证逻辑
- 记录用户查看密钥的操作日志
- 编辑渠道弹窗新增查看密钥按钮,触发2FA验证模态框
- 使用TwoFactorAuthModal进行验证码输入及验证
- 验证成功后弹出渠道密钥展示窗口
- 对渠道编辑模态框的状态进行了统一重置优化
- 添加相关国际化文案支持密钥查看功能
This commit is contained in:
AAEE86
2025-08-25 14:45:48 +08:00
parent c9f5b1de1a
commit dc4f5750af
6 changed files with 635 additions and 19 deletions

View File

@@ -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

View File

@@ -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)

View 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;

View 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;

View File

@@ -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}

View File

@@ -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"
}