feat: channel affinity (#2669)
* feat: channel affinity * feat: channel affinity -> model setting * fix: channel affinity * feat: channel affinity op * feat: channel_type setting * feat: clean * feat: cache supports both memory and Redis. * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: Optimise codex usage ui/ux * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: Optimise ui/ux * feat: If the affinitized channel fails and a retry succeeds on another channel, update the affinity to the successful channel
This commit is contained in:
@@ -17,8 +17,9 @@ 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 { Modal, Button, Progress, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Modal, Button, Progress, Tag, Typography, Spin } from '@douyinfe/semi-ui';
|
||||
import { API, showError } from '../../../../helpers';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -101,7 +102,7 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const data = payload?.data ?? null;
|
||||
const rateLimit = data?.rate_limit ?? {};
|
||||
@@ -123,61 +124,159 @@ export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const rawText =
|
||||
typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
|
||||
|
||||
Modal.info({
|
||||
title: (
|
||||
<div className='flex items-center gap-2'>
|
||||
<span>{tt('Codex 用量')}</span>
|
||||
{statusTag}
|
||||
return (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<div className='flex items-center gap-2'>
|
||||
{statusTag}
|
||||
<Button size='small' type='tertiary' theme='borderless' onClick={onRefresh}>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
const [loading, setLoading] = useState(!initialPayload);
|
||||
const [payload, setPayload] = useState(initialPayload ?? null);
|
||||
const hasShownErrorRef = useRef(false);
|
||||
const mountedRef = useRef(true);
|
||||
const recordId = record?.id;
|
||||
|
||||
const fetchUsage = useCallback(async () => {
|
||||
if (!recordId) {
|
||||
if (mountedRef.current) setPayload(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mountedRef.current) setLoading(true);
|
||||
try {
|
||||
const res = await API.get(`/api/channel/${recordId}/codex/usage`, {
|
||||
skipErrorHandler: true,
|
||||
});
|
||||
if (!mountedRef.current) return;
|
||||
setPayload(res?.data ?? null);
|
||||
if (!res?.data?.success && !hasShownErrorRef.current) {
|
||||
hasShownErrorRef.current = true;
|
||||
showError(tt('获取用量失败'));
|
||||
}
|
||||
} catch (error) {
|
||||
if (!mountedRef.current) return;
|
||||
if (!hasShownErrorRef.current) {
|
||||
hasShownErrorRef.current = true;
|
||||
showError(tt('获取用量失败'));
|
||||
}
|
||||
setPayload({ success: false, message: String(error) });
|
||||
} finally {
|
||||
if (mountedRef.current) setLoading(false);
|
||||
}
|
||||
}, [recordId, tt]);
|
||||
|
||||
useEffect(() => {
|
||||
mountedRef.current = true;
|
||||
return () => {
|
||||
mountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialPayload) return;
|
||||
fetchUsage().catch(() => {});
|
||||
}, [fetchUsage, initialPayload]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className='flex items-center justify-center py-10'>
|
||||
<Spin spinning={true} size='large' tip={tt('加载中...')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload) {
|
||||
return (
|
||||
<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}>
|
||||
{tt('刷新')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CodexUsageView
|
||||
t={tt}
|
||||
record={record}
|
||||
payload={payload}
|
||||
onCopy={onCopy}
|
||||
onRefresh={fetchUsage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
|
||||
const tt = typeof t === 'function' ? t : (v) => v;
|
||||
|
||||
Modal.info({
|
||||
title: tt('Codex 用量'),
|
||||
centered: true,
|
||||
width: 900,
|
||||
style: { maxWidth: '95vw' },
|
||||
content: (
|
||||
<div className='flex flex-col gap-3'>
|
||||
<div className='flex flex-wrap items-center justify-between gap-2'>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('渠道:')}
|
||||
{record?.name || '-'} ({tt('编号:')}
|
||||
{record?.id || '-'})
|
||||
</Text>
|
||||
<Text type='tertiary' size='small'>
|
||||
{tt('上游状态码:')}
|
||||
{upstreamStatus ?? '-'}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('5小时窗口')}
|
||||
windowData={primary}
|
||||
/>
|
||||
<RateLimitWindowCard
|
||||
t={tt}
|
||||
title={tt('每周窗口')}
|
||||
windowData={secondary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className='mb-1 flex items-center justify-between gap-2'>
|
||||
<div className='text-sm font-medium'>{tt('原始 JSON')}</div>
|
||||
<Button
|
||||
size='small'
|
||||
type='primary'
|
||||
theme='outline'
|
||||
onClick={() => onCopy?.(rawText)}
|
||||
disabled={!rawText}
|
||||
>
|
||||
{tt('复制')}
|
||||
</Button>
|
||||
</div>
|
||||
<pre className='max-h-[50vh] overflow-auto rounded-lg bg-semi-color-fill-0 p-3 text-xs text-semi-color-text-0'>
|
||||
{rawText}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<CodexUsageLoader
|
||||
t={tt}
|
||||
record={record}
|
||||
initialPayload={payload}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
),
|
||||
footer: (
|
||||
<div className='flex justify-end gap-2'>
|
||||
|
||||
Reference in New Issue
Block a user