mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-20 00:18:37 +00:00
fix: channel affinity (#2799)
* fix: channel affinity log styles * fix: Issue with incorrect data storage when switching key sources * feat: support not retrying after a single rule configuration fails * fix: render channel affinity tooltip as multiline content * feat: channel affinity cache hit * fix: prevent ChannelAffinityUsageCacheModal infinite loading and hide data before fetch * chore: format backend with gofmt and frontend with prettier/eslint autofix
This commit is contained in:
@@ -19,7 +19,14 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Modal, Button, Space, Typography, Input, Banner } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Space,
|
||||
Typography,
|
||||
Input,
|
||||
Banner,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, copy, showError, showSuccess } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -33,14 +40,21 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
|
||||
const startOAuth = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await API.post('/api/channel/codex/oauth/start', {}, { skipErrorHandler: true });
|
||||
const res = await API.post(
|
||||
'/api/channel/codex/oauth/start',
|
||||
{},
|
||||
{ skipErrorHandler: true },
|
||||
);
|
||||
if (!res?.data?.success) {
|
||||
console.error('Codex OAuth start failed:', res?.data?.message);
|
||||
throw new Error(t('启动授权失败'));
|
||||
}
|
||||
const url = res?.data?.data?.authorize_url || '';
|
||||
if (!url) {
|
||||
console.error('Codex OAuth start response missing authorize_url:', res?.data);
|
||||
console.error(
|
||||
'Codex OAuth start response missing authorize_url:',
|
||||
res?.data,
|
||||
);
|
||||
throw new Error(t('响应缺少授权链接'));
|
||||
}
|
||||
setAuthorizeUrl(url);
|
||||
@@ -106,7 +120,12 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
|
||||
<Button theme='borderless' onClick={onCancel} disabled={loading}>
|
||||
{t('取消')}
|
||||
</Button>
|
||||
<Button theme='solid' type='primary' onClick={completeOAuth} loading={loading}>
|
||||
<Button
|
||||
theme='solid'
|
||||
type='primary'
|
||||
onClick={completeOAuth}
|
||||
loading={loading}
|
||||
>
|
||||
{t('生成并填入')}
|
||||
</Button>
|
||||
</Space>
|
||||
@@ -141,7 +160,9 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => {
|
||||
/>
|
||||
|
||||
<Text type='tertiary' size='small'>
|
||||
{t('说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。')}
|
||||
{t(
|
||||
'说明:生成结果是可直接粘贴到渠道密钥里的 JSON(包含 access_token / refresh_token / account_id)。',
|
||||
)}
|
||||
</Text>
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
@@ -18,7 +18,14 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Modal, Button, Progress, Tag, Typography, Spin } from '@douyinfe/semi-ui';
|
||||
import {
|
||||
Modal,
|
||||
Button,
|
||||
Progress,
|
||||
Tag,
|
||||
Typography,
|
||||
Spin,
|
||||
} from '@douyinfe/semi-ui';
|
||||
import { API, showError } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -134,7 +141,12 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
{statusTag}
|
||||
<Button size='small' type='tertiary' theme='borderless' onClick={onRefresh}>
|
||||
<Button
|
||||
size='small'
|
||||
type='tertiary'
|
||||
theme='borderless'
|
||||
onClick={onRefresh}
|
||||
>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -243,7 +255,12 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
|
||||
<div className='flex flex-col gap-3'>
|
||||
<Text type='danger'>{tt('获取用量失败')}</Text>
|
||||
<div className='flex justify-end'>
|
||||
<Button size='small' type='primary' theme='outline' onClick={fetchUsage}>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={fetchUsage}
|
||||
>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -2000,171 +2000,180 @@ const EditChannelModal = (props) => {
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
disabled={isIonetLocked}
|
||||
extraText={
|
||||
<div className='flex items-center gap-2 flex-wrap'>
|
||||
{isEdit &&
|
||||
isMultiKeyChannel &&
|
||||
keyMode === 'append' && (
|
||||
<Text type='warning' size='small'>
|
||||
{t(
|
||||
'追加模式:新密钥将添加到现有密钥列表的末尾',
|
||||
)}
|
||||
</Text>
|
||||
extraText={
|
||||
<div className='flex items-center gap-2 flex-wrap'>
|
||||
{isEdit &&
|
||||
isMultiKeyChannel &&
|
||||
keyMode === 'append' && (
|
||||
<Text type='warning' size='small'>
|
||||
{t(
|
||||
'追加模式:新密钥将添加到现有密钥列表的末尾',
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleShow2FAModal}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleShow2FAModal}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 57 ? (
|
||||
<>
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={
|
||||
isEdit
|
||||
? t('密钥(编辑模式下,保存的密钥不会显示)')
|
||||
: t('密钥')
|
||||
}
|
||||
placeholder={t(
|
||||
'请输入 JSON 格式的 OAuth 凭据,例如:\n{\n "access_token": "...",\n "account_id": "..." \n}',
|
||||
)}
|
||||
rules={
|
||||
isEdit
|
||||
? []
|
||||
: [{ required: true, message: t('请输入密钥') }]
|
||||
}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) => handleInputChange('key', value)}
|
||||
disabled={isIonetLocked}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t(
|
||||
'仅支持 JSON 对象,必须包含 access_token 与 account_id',
|
||||
)}
|
||||
</Text>
|
||||
{batchExtra}
|
||||
</div>
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{inputs.type === 57 ? (
|
||||
<>
|
||||
<Form.TextArea
|
||||
field='key'
|
||||
label={
|
||||
isEdit
|
||||
? t('密钥(编辑模式下,保存的密钥不会显示)')
|
||||
: t('密钥')
|
||||
}
|
||||
placeholder={t(
|
||||
'请输入 JSON 格式的 OAuth 凭据,例如:\n{\n "access_token": "...",\n "account_id": "..." \n}',
|
||||
)}
|
||||
rules={
|
||||
isEdit
|
||||
? []
|
||||
: [
|
||||
{
|
||||
required: true,
|
||||
message: t('请输入密钥'),
|
||||
},
|
||||
]
|
||||
}
|
||||
autoComplete='new-password'
|
||||
onChange={(value) =>
|
||||
handleInputChange('key', value)
|
||||
}
|
||||
disabled={isIonetLocked}
|
||||
extraText={
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{t(
|
||||
'仅支持 JSON 对象,必须包含 access_token 与 account_id',
|
||||
)}
|
||||
</Text>
|
||||
|
||||
<Space wrap spacing='tight'>
|
||||
<Space wrap spacing='tight'>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() =>
|
||||
setCodexOAuthModalVisible(true)
|
||||
}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('Codex 授权')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleRefreshCodexCredential}
|
||||
loading={codexCredentialRefreshing}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('刷新凭证')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => formatJsonField('key')}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('格式化')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleShow2FAModal}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
autosize
|
||||
showClear
|
||||
/>
|
||||
|
||||
<CodexOAuthModal
|
||||
visible={codexOAuthModalVisible}
|
||||
onCancel={() => setCodexOAuthModalVisible(false)}
|
||||
onSuccess={handleCodexOAuthGenerated}
|
||||
/>
|
||||
</>
|
||||
) : inputs.type === 41 &&
|
||||
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||
<>
|
||||
{!batch && (
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<Text className='text-sm font-medium'>
|
||||
{t('密钥输入方式')}
|
||||
</Text>
|
||||
<Space>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() =>
|
||||
setCodexOAuthModalVisible(true)
|
||||
type={
|
||||
!useManualInput ? 'primary' : 'tertiary'
|
||||
}
|
||||
disabled={isIonetLocked}
|
||||
onClick={() => {
|
||||
setUseManualInput(false);
|
||||
// 切换到文件上传模式时清空手动输入的密钥
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key', '');
|
||||
}
|
||||
handleInputChange('key', '');
|
||||
}}
|
||||
>
|
||||
{t('Codex 授权')}
|
||||
{t('文件上传')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleRefreshCodexCredential}
|
||||
loading={codexCredentialRefreshing}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('刷新凭证')}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => formatJsonField('key')}
|
||||
disabled={isIonetLocked}
|
||||
type={
|
||||
useManualInput ? 'primary' : 'tertiary'
|
||||
}
|
||||
onClick={() => {
|
||||
setUseManualInput(true);
|
||||
// 切换到手动输入模式时清空文件上传相关状态
|
||||
setVertexKeys([]);
|
||||
setVertexFileList([]);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue(
|
||||
'vertex_files',
|
||||
[],
|
||||
);
|
||||
}
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
vertex_files: [],
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{t('格式化')}
|
||||
{t('手动输入')}
|
||||
</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={handleShow2FAModal}
|
||||
disabled={isIonetLocked}
|
||||
>
|
||||
{t('查看密钥')}
|
||||
</Button>
|
||||
)}
|
||||
{batchExtra}
|
||||
</Space>
|
||||
</div>
|
||||
}
|
||||
autosize
|
||||
showClear
|
||||
/>
|
||||
|
||||
<CodexOAuthModal
|
||||
visible={codexOAuthModalVisible}
|
||||
onCancel={() => setCodexOAuthModalVisible(false)}
|
||||
onSuccess={handleCodexOAuthGenerated}
|
||||
/>
|
||||
</>
|
||||
) : inputs.type === 41 &&
|
||||
(inputs.vertex_key_type || 'json') === 'json' ? (
|
||||
<>
|
||||
{!batch && (
|
||||
<div className='flex items-center justify-between mb-3'>
|
||||
<Text className='text-sm font-medium'>
|
||||
{t('密钥输入方式')}
|
||||
</Text>
|
||||
<Space>
|
||||
<Button
|
||||
size='small'
|
||||
type={
|
||||
!useManualInput ? 'primary' : 'tertiary'
|
||||
}
|
||||
onClick={() => {
|
||||
setUseManualInput(false);
|
||||
// 切换到文件上传模式时清空手动输入的密钥
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue('key', '');
|
||||
}
|
||||
handleInputChange('key', '');
|
||||
}}
|
||||
>
|
||||
{t('文件上传')}
|
||||
</Button>
|
||||
<Button
|
||||
size='small'
|
||||
type={useManualInput ? 'primary' : 'tertiary'}
|
||||
onClick={() => {
|
||||
setUseManualInput(true);
|
||||
// 切换到手动输入模式时清空文件上传相关状态
|
||||
setVertexKeys([]);
|
||||
setVertexFileList([]);
|
||||
if (formApiRef.current) {
|
||||
formApiRef.current.setValue(
|
||||
'vertex_files',
|
||||
[],
|
||||
);
|
||||
}
|
||||
setInputs((prev) => ({
|
||||
...prev,
|
||||
vertex_files: [],
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{t('手动输入')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{batch && (
|
||||
<Banner
|
||||
|
||||
@@ -533,7 +533,11 @@ const EditTagModal = (props) => {
|
||||
<Card className='!rounded-2xl shadow-sm border-0 mb-6'>
|
||||
{/* Header: Advanced Settings */}
|
||||
<div className='flex items-center mb-2'>
|
||||
<Avatar size='small' color='orange' className='mr-2 shadow-md'>
|
||||
<Avatar
|
||||
size='small'
|
||||
color='orange'
|
||||
className='mr-2 shadow-md'
|
||||
>
|
||||
<IconSetting size={16} />
|
||||
</Avatar>
|
||||
<div>
|
||||
@@ -549,9 +553,7 @@ const EditTagModal = (props) => {
|
||||
field='param_override'
|
||||
label={t('参数覆盖')}
|
||||
placeholder={
|
||||
t(
|
||||
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
|
||||
) +
|
||||
t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数') +
|
||||
'\n' +
|
||||
t('旧格式(直接覆盖):') +
|
||||
'\n{\n "temperature": 0,\n "max_tokens": 1000\n}' +
|
||||
|
||||
@@ -104,7 +104,9 @@ const ModelSelectModal = ({
|
||||
}, [normalizedRedirectModels, normalizedSelectedSet]);
|
||||
|
||||
const filteredModels = models.filter((m) =>
|
||||
String(m || '').toLowerCase().includes(keyword.toLowerCase()),
|
||||
String(m || '')
|
||||
.toLowerCase()
|
||||
.includes(keyword.toLowerCase()),
|
||||
);
|
||||
|
||||
// 分类模型:新获取的模型和已有模型
|
||||
|
||||
Reference in New Issue
Block a user