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:
Seefs
2026-02-02 14:37:31 +08:00
committed by GitHub
parent 6c0e9403a2
commit 540cf6c991
61 changed files with 2012 additions and 1004 deletions

View File

@@ -18,8 +18,22 @@ For commercial licensing, please contact support@quantumnous.com
*/
import React, { useState, useMemo, useCallback } from 'react';
import { Button, Tooltip, Toast, Collapse, Badge, Typography } from '@douyinfe/semi-ui';
import { Copy, ChevronDown, ChevronUp, Zap, CheckCircle, XCircle } from 'lucide-react';
import {
Button,
Tooltip,
Toast,
Collapse,
Badge,
Typography,
} from '@douyinfe/semi-ui';
import {
Copy,
ChevronDown,
ChevronUp,
Zap,
CheckCircle,
XCircle,
} from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { copy } from '../../helpers';
@@ -67,19 +81,19 @@ const SSEViewer = ({ sseData }) => {
const stats = useMemo(() => {
const total = parsedSSEData.length;
const errors = parsedSSEData.filter(item => item.error).length;
const done = parsedSSEData.filter(item => item.isDone).length;
const errors = parsedSSEData.filter((item) => item.error).length;
const done = parsedSSEData.filter((item) => item.isDone).length;
const valid = total - errors - done;
return { total, errors, done, valid };
}, [parsedSSEData]);
const handleToggleAll = useCallback(() => {
setExpandedKeys(prev => {
setExpandedKeys((prev) => {
if (prev.length === parsedSSEData.length) {
return [];
} else {
return parsedSSEData.map(item => item.key);
return parsedSSEData.map((item) => item.key);
}
});
}, [parsedSSEData]);
@@ -87,7 +101,9 @@ const SSEViewer = ({ sseData }) => {
const handleCopyAll = useCallback(async () => {
try {
const allData = parsedSSEData
.map(item => (item.parsed ? JSON.stringify(item.parsed, null, 2) : item.raw))
.map((item) =>
item.parsed ? JSON.stringify(item.parsed, null, 2) : item.raw,
)
.join('\n\n');
await copy(allData);
@@ -100,15 +116,20 @@ const SSEViewer = ({ sseData }) => {
}
}, [parsedSSEData, t]);
const handleCopySingle = useCallback(async (item) => {
try {
const textToCopy = item.parsed ? JSON.stringify(item.parsed, null, 2) : item.raw;
await copy(textToCopy);
Toast.success(t('已复制'));
} catch (err) {
Toast.error(t('复制失败'));
}
}, [t]);
const handleCopySingle = useCallback(
async (item) => {
try {
const textToCopy = item.parsed
? JSON.stringify(item.parsed, null, 2)
: item.raw;
await copy(textToCopy);
Toast.success(t('已复制'));
} catch (err) {
Toast.error(t('复制失败'));
}
},
[t],
);
const renderSSEItem = (item) => {
if (item.isDone) {
@@ -158,18 +179,24 @@ const SSEViewer = ({ sseData }) => {
{item.parsed?.choices?.[0] && (
<div className='flex flex-wrap gap-2 text-xs'>
{item.parsed.choices[0].delta?.content && (
<Badge count={`${t('内容')}: "${String(item.parsed.choices[0].delta.content).substring(0, 20)}..."`} type='primary' />
<Badge
count={`${t('内容')}: "${String(item.parsed.choices[0].delta.content).substring(0, 20)}..."`}
type='primary'
/>
)}
{item.parsed.choices[0].delta?.reasoning_content && (
<Badge count={t('有 Reasoning')} type='warning' />
)}
{item.parsed.choices[0].finish_reason && (
<Badge count={`${t('完成')}: ${item.parsed.choices[0].finish_reason}`} type='success' />
<Badge
count={`${t('完成')}: ${item.parsed.choices[0].finish_reason}`}
type='success'
/>
)}
{item.parsed.usage && (
<Badge
count={`${t('令牌')}: ${item.parsed.usage.prompt_tokens || 0}/${item.parsed.usage.completion_tokens || 0}`}
type='tertiary'
<Badge
count={`${t('令牌')}: ${item.parsed.usage.prompt_tokens || 0}/${item.parsed.usage.completion_tokens || 0}`}
type='tertiary'
/>
)}
</div>
@@ -194,7 +221,9 @@ const SSEViewer = ({ sseData }) => {
<Zap size={16} className='text-blue-500' />
<Typography.Text strong>{t('SSE数据流')}</Typography.Text>
<Badge count={stats.total} type='primary' />
{stats.errors > 0 && <Badge count={`${stats.errors} ${t('错误')}`} type='danger' />}
{stats.errors > 0 && (
<Badge count={`${stats.errors} ${t('错误')}`} type='danger' />
)}
</div>
<div className='flex items-center gap-2'>
@@ -208,14 +237,28 @@ const SSEViewer = ({ sseData }) => {
{copied ? t('已复制') : t('复制全部')}
</Button>
</Tooltip>
<Tooltip content={expandedKeys.length === parsedSSEData.length ? t('全部收起') : t('全部展开')}>
<Tooltip
content={
expandedKeys.length === parsedSSEData.length
? t('全部收起')
: t('全部展开')
}
>
<Button
icon={expandedKeys.length === parsedSSEData.length ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
icon={
expandedKeys.length === parsedSSEData.length ? (
<ChevronUp size={14} />
) : (
<ChevronDown size={14} />
)
}
size='small'
onClick={handleToggleAll}
theme='borderless'
>
{expandedKeys.length === parsedSSEData.length ? t('收起') : t('展开')}
{expandedKeys.length === parsedSSEData.length
? t('收起')
: t('展开')}
</Button>
</Tooltip>
</div>
@@ -242,11 +285,16 @@ const SSEViewer = ({ sseData }) => {
) : (
<>
<span className='text-gray-600'>
{item.parsed?.id || item.parsed?.object || t('SSE 事件')}
{item.parsed?.id ||
item.parsed?.object ||
t('SSE 事件')}
</span>
{item.parsed?.choices?.[0]?.delta && (
<span className='text-xs text-gray-400'>
{Object.keys(item.parsed.choices[0].delta).filter(k => item.parsed.choices[0].delta[k]).join(', ')}
{' '}
{Object.keys(item.parsed.choices[0].delta)
.filter((k) => item.parsed.choices[0].delta[k])
.join(', ')}
</span>
)}
</>