Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 99 additions & 17 deletions web/classic/src/components/table/channels/modals/ModelTestModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ const ModelTestModal = ({
setSelectedEndpointType,
isStreamTest,
setIsStreamTest,
testTimeoutMs,
setTestTimeoutMs,
responseTimeFilter,
setResponseTimeFilter,
customResponseTimeSec,
setCustomResponseTimeSec,
allSelectingRef,
isMobile,
t,
Expand All @@ -71,13 +77,32 @@ const ModelTestModal = ({
}
}, [streamToggleDisabled, isStreamTest, setIsStreamTest]);

const filteredModels = hasChannel
? currentTestChannel.models
.split(',')
.filter((model) =>
model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
)
: [];
const getResponseTimeThresholdMs = (filter, customSec) => {
if (filter === 'custom') return Number(customSec) * 1000;
return Number(filter);
};

const filteredModels = (() => {
if (!hasChannel) return [];
let models = currentTestChannel.models
.split(',')
.filter((model) =>
model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
);
if (responseTimeFilter !== 'all') {
const maxMs = getResponseTimeThresholdMs(responseTimeFilter, customResponseTimeSec);
if (maxMs > 0) {
models = models.filter((model) => {
const r = modelTestResults[`${currentTestChannel.id}-${model}`];
if (!r) return true;
if (testingModels.has(model)) return true;
if (!r.success) return false;
return ((r.time || 0) * 1000) <= maxMs;
});
}
}
return models;
})();

const endpointTypeOptions = [
{ value: '', label: t('自动检测') },
Expand All @@ -100,6 +125,16 @@ const ModelTestModal = ({
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
];

const RESPONSE_TIME_FILTER_OPTIONS = [
{ value: 'all', label: t('全部') },
{ value: '500', label: '<=0.5s (' + t('优秀') + ')' },
{ value: '1000', label: '<=1s (' + t('良好') + ')' },
{ value: '2000', label: '<=2s (' + t('一般') + ')' },
{ value: '5000', label: '<=5s (' + t('较差') + ')' },
{ value: '10000', label: '<=10s' },
{ value: 'custom', label: t('自定义') },
];

const handleCopySelected = () => {
if (selectedModelKeys.length === 0) {
showError(t('请先选择模型!'));
Expand All @@ -119,19 +154,20 @@ const ModelTestModal = ({
});
};

const fittingModels = (() => {
if (!hasChannel) return [];
return filteredModels.filter((m) => {
const result = modelTestResults[`${currentTestChannel.id}-${m}`];
return result && result.success;
});
})();

const handleSelectSuccess = () => {
if (!currentTestChannel) return;
const successKeys = currentTestChannel.models
.split(',')
.filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()))
.filter((m) => {
const result = modelTestResults[`${currentTestChannel.id}-${m}`];
return result && result.success;
});
if (successKeys.length === 0) {
showInfo(t('暂无成功模型'));
if (fittingModels.length === 0) {
showInfo(t('暂无符合条件的模型'));
}
setSelectedModelKeys(successKeys);
setSelectedModelKeys(fittingModels);
};

const columns = [
Expand Down Expand Up @@ -362,6 +398,52 @@ const ModelTestModal = ({
</div>
</div>

{/* 超时设置与响应时间筛选 */}
<div className='flex flex-col sm:flex-row gap-2 w-full mb-2'>
<div className='flex items-center gap-2 flex-1 min-w-0'>
<Typography.Text strong className='shrink-0'>
{t('测试超时时间(秒)')}:
</Typography.Text>
<Input
type='number'
min={1}
step={1}
value={testTimeoutMs / 1000}
onChange={(v) => {
const sec = Number(v);
if (sec >= 1) setTestTimeoutMs(sec * 1000);
}}
className='!w-24'
/>
<Typography.Text type='tertiary' size='small' className='hidden sm:block'>
{t('超时未响应则自动判定测试失败。')}
</Typography.Text>
</div>
<div className='flex items-center gap-2 shrink-0'>
<Typography.Text strong className='shrink-0'>
{t('响应时间筛选')}:
</Typography.Text>
<Select
value={responseTimeFilter}
onChange={(v) => { if (v !== null) setResponseTimeFilter(v); }}
optionList={RESPONSE_TIME_FILTER_OPTIONS}
className='!w-44'
placeholder={t('全部')}
/>
{responseTimeFilter === 'custom' && (
<Input
type='number'
min={0.5}
step={0.5}
value={customResponseTimeSec}
onChange={(v) => setCustomResponseTimeSec(v)}
placeholder={t('最大秒数')}
className='!w-24'
/>
)}
</div>
</div>

<Table
columns={columns}
dataSource={dataSource}
Expand Down
75 changes: 61 additions & 14 deletions web/classic/src/hooks/channels/useChannelsData.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export const useChannelsData = () => {
const [modelTablePage, setModelTablePage] = useState(1);
const [selectedEndpointType, setSelectedEndpointType] = useState('');
const [isStreamTest, setIsStreamTest] = useState(false);
const [testTimeoutMs, setTestTimeoutMs] = useState(10000);
const [responseTimeFilter, setResponseTimeFilter] = useState('all');
const [customResponseTimeSec, setCustomResponseTimeSec] = useState('');
const abortControllerRef = useRef(null);
const [globalPassThroughEnabled, setGlobalPassThroughEnabled] =
useState(false);

Expand Down Expand Up @@ -875,6 +879,14 @@ export const useChannelsData = () => {
// 添加到正在测试的模型集合
setTestingModels((prev) => new Set([...prev, model]));

// 创建 AbortController 用于超时控制
const abortController = new AbortController();
abortControllerRef.current = abortController;

const timeoutId = testTimeoutMs > 0
? setTimeout(() => abortController.abort(), testTimeoutMs)
: null;

try {
let url = `/api/channel/test/${record.id}?model=${model}`;
if (endpointType) {
Expand All @@ -883,7 +895,9 @@ export const useChannelsData = () => {
if (stream) {
url += `&stream=true`;
}
const res = await API.get(url);
const res = await API.get(url, {
signal: abortController.signal,
});

// 检查是否在请求期间被停止
if (shouldStopBatchTestingRef.current && isBatchTesting) {
Expand Down Expand Up @@ -931,20 +945,42 @@ export const useChannelsData = () => {
showError(message);
}
} catch (error) {
// 处理网络错误
const testKey = `${record.id}-${model}`;
setModelTestResults((prev) => ({
...prev,
[testKey]: {
success: false,
message: error.message || t('网络错误'),
time: 0,
timestamp: Date.now(),
errorCode: null,
},
}));
showError(error.message || t('测试失败'));
// 处理超时错误
if (error.name === 'AbortError' || error.name === 'CanceledError') {
const isTimeout = abortController.signal.aborted;
setModelTestResults((prev) => ({
...prev,
[testKey]: {
success: false,
message: isTimeout
? t('测试超时 ({{timeout}}s)').replace('{{timeout}}', (testTimeoutMs / 1000).toFixed(0))
: t('测试已取消'),
time: 0,
timestamp: Date.now(),
errorCode: isTimeout ? 'test_timeout' : null,
},
}));
} else {
// 处理网络错误
const testKey = `${record.id}-${model}`;
setModelTestResults((prev) => ({
...prev,
[testKey]: {
success: false,
message: error.message || t('网络错误'),
time: 0,
timestamp: Date.now(),
errorCode: null,
},
}));
showError(error.message || t('测试失败'));
}
} finally {
// 清理超时定时器
if (timeoutId !== null) clearTimeout(timeoutId);
if (abortControllerRef.current === abortController) {
abortControllerRef.current = null;
}
// 从正在测试的模型集合中移除
setTestingModels((prev) => {
const newSet = new Set(prev);
Expand Down Expand Up @@ -1096,6 +1132,7 @@ export const useChannelsData = () => {
showInfo(t('关闭弹窗,已停止批量测试'));
}

abortControllerRef.current?.abort();
setShowModelTestModal(false);
setModelSearchKeyword('');
setIsBatchTesting(false);
Expand All @@ -1104,6 +1141,9 @@ export const useChannelsData = () => {
setModelTablePage(1);
setSelectedEndpointType('');
setIsStreamTest(false);
setTestTimeoutMs(10000);
setResponseTimeFilter('all');
setCustomResponseTimeSec('');
// 可选择性保留测试结果,这里不清空以便用户查看
};

Expand Down Expand Up @@ -1196,6 +1236,13 @@ export const useChannelsData = () => {
setSelectedEndpointType,
isStreamTest,
setIsStreamTest,
testTimeoutMs,
setTestTimeoutMs,
responseTimeFilter,
setResponseTimeFilter,
customResponseTimeSec,
setCustomResponseTimeSec,
abortControllerRef,
allSelectingRef,

// Multi-key management states
Expand Down
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3826,6 +3826,17 @@
"并确认自行承担部署": "confirm that I bear legal responsibility arising from deployment",
"运营和收费行为产生的法律责任": "operation and charging behavior",
",": ", ",
"、": ", "
"、": ", ",
"测试超时时间(秒)": "Test timeout (seconds)",
"超时未响应则自动判定测试失败。": "Fail the test automatically if no response within this time.",
"响应时间筛选": "Response time filter",
"最大秒数": "Max seconds",
"测试超时 ({{timeout}}s)": "Test timed out ({{timeout}}s)",
"测试已取消": "Test cancelled",
"暂无符合条件的模型": "No matching models",
"优秀": "Excellent",
"良好": "Good",
"一般": "Fair",
"较差": "Poor"
}
}
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -3680,6 +3680,17 @@
"并确认自行承担部署": "confirm that I bear legal responsibility arising from deployment",
"运营和收费行为产生的法律责任": "operation and charging behavior",
",": ", ",
"、": ", "
"、": ", ",
"测试超时时间(秒)": "Délai d'expiration (secondes)",
"超时未响应则自动判定测试失败。": "Échec automatique si aucune réponse dans ce délai.",
"响应时间筛选": "Filtre de temps de réponse",
"最大秒数": "Secondes max",
"测试超时 ({{timeout}}s)": "Test expiré ({{timeout}}s)",
"测试已取消": "Test annulé",
"暂无符合条件的模型": "Aucun modèle correspondant",
"优秀": "Excellent",
"良好": "Bon",
"一般": "Moyen",
"较差": "Médiocre"
}
}
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -3649,6 +3649,17 @@
"并确认自行承担部署": "confirm that I bear legal responsibility arising from deployment",
"运营和收费行为产生的法律责任": "operation and charging behavior",
",": ", ",
"、": ", "
"、": ", ",
"测试超时时间(秒)": "テストタイムアウト(秒)",
"超时未响应则自动判定测试失败。": "応答がない場合、自動的に失敗と判定されます。",
"响应时间筛选": "応答時間フィルター",
"最大秒数": "最大秒数",
"测试超时 ({{timeout}}s)": "テストタイムアウト ({{timeout}}s)",
"测试已取消": "テストがキャンセルされました",
"暂无符合条件的模型": "該当するモデルがありません",
"优秀": "優秀",
"良好": "良好",
"一般": "普通",
"较差": "やや悪い"
}
}
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -3700,6 +3700,17 @@
"并确认自行承担部署": "confirm that I bear legal responsibility arising from deployment",
"运营和收费行为产生的法律责任": "operation and charging behavior",
",": ", ",
"、": ", "
"、": ", ",
"测试超时时间(秒)": "Тайм-аут теста (секунд)",
"超时未响应则自动判定测试失败。": "Автоматический сбой при отсутствии ответа.",
"响应时间筛选": "Фильтр времени отклика",
"最大秒数": "Макс. секунд",
"测试超时 ({{timeout}}s)": "Тайм-аут теста ({{timeout}}s)",
"测试已取消": "Тест отменён",
"暂无符合条件的模型": "Нет подходящих моделей",
"优秀": "Отлично",
"良好": "Хорошо",
"一般": "Средне",
"较差": "Плохо"
}
}
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4214,6 +4214,17 @@
"并确认自行承担部署": "confirm that I bear legal responsibility arising from deployment",
"运营和收费行为产生的法律责任": "operation and charging behavior",
",": ", ",
"、": ", "
"、": ", ",
"测试超时时间(秒)": "Thời gian chờ (giây)",
"超时未响应则自动判定测试失败。": "Tự động thất bại nếu không có phản hồi.",
"响应时间筛选": "Bộ lọc thời gian phản hồi",
"最大秒数": "Số giây tối đa",
"测试超时 ({{timeout}}s)": "Hết thời gian chờ ({{timeout}}s)",
"测试已取消": "Đã hủy kiểm tra",
"暂无符合条件的模型": "Không có mô hình phù hợp",
"优秀": "Xuất sắc",
"良好": "Tốt",
"一般": "Trung bình",
"较差": "Kém"
}
}
13 changes: 12 additions & 1 deletion web/classic/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -3809,6 +3809,17 @@
"并确认自行承担部署": "并确认自行承担部署",
"运营和收费行为产生的法律责任": "运营和收费行为产生的法律责任",
",": ",",
"、": "、"
"、": "、",
"测试超时时间(秒)": "测试超时时间(秒)",
"超时未响应则自动判定测试失败。": "超时未响应则自动判定测试失败。",
"响应时间筛选": "响应时间筛选",
"最大秒数": "最大秒数",
"测试超时 ({{timeout}}s)": "测试超时 ({{timeout}}s)",
"测试已取消": "测试已取消",
"暂无符合条件的模型": "暂无符合条件的模型",
"优秀": "优秀",
"良好": "良好",
"一般": "一般",
"较差": "较差"
}
}
Loading