Merge branch 'main-upstream' into fix/volcengine_default_baseurl

# Conflicts:
#	main.go
This commit is contained in:
Seefs
2025-09-29 12:08:52 +08:00
16 changed files with 880 additions and 348 deletions

View File

@@ -164,6 +164,8 @@ const EditChannelModal = (props) => {
settings: '',
// 仅 Vertex: 密钥格式(存入 settings.vertex_key_type
vertex_key_type: 'json',
// 企业账户设置
is_enterprise_account: false,
};
const [batch, setBatch] = useState(false);
const [multiToSingle, setMultiToSingle] = useState(false);
@@ -189,6 +191,7 @@ const EditChannelModal = (props) => {
const [channelSearchValue, setChannelSearchValue] = useState('');
const [useManualInput, setUseManualInput] = useState(false); // 是否使用手动输入模式
const [keyMode, setKeyMode] = useState('append'); // 密钥模式replace覆盖或 append追加
const [isEnterpriseAccount, setIsEnterpriseAccount] = useState(false); // 是否为企业账户
// 2FA验证查看密钥相关状态
const [twoFAState, setTwoFAState] = useState({
@@ -235,7 +238,7 @@ const EditChannelModal = (props) => {
pass_through_body_enabled: false,
system_prompt: '',
});
const showApiConfigCard = inputs.type !== 45; // 控制是否显示 API 配置卡片(仅当渠道类型不是 豆包 时显示)
const showApiConfigCard = true; // 控制是否显示 API 配置卡片
const getInitValues = () => ({ ...originInputs });
// 处理渠道额外设置的更新
@@ -342,6 +345,10 @@ const EditChannelModal = (props) => {
case 36:
localModels = ['suno_music', 'suno_lyrics'];
break;
case 45:
localModels = getChannelModels(value);
setInputs((prevInputs) => ({ ...prevInputs, base_url: 'https://ark.cn-beijing.volces.com' }));
break;
default:
localModels = getChannelModels(value);
break;
@@ -433,15 +440,19 @@ const EditChannelModal = (props) => {
parsedSettings.azure_responses_version || '';
// 读取 Vertex 密钥格式
data.vertex_key_type = parsedSettings.vertex_key_type || 'json';
// 读取企业账户设置
data.is_enterprise_account = parsedSettings.openrouter_enterprise === true;
} catch (error) {
console.error('解析其他设置失败:', error);
data.azure_responses_version = '';
data.region = '';
data.vertex_key_type = 'json';
data.is_enterprise_account = false;
}
} else {
// 兼容历史数据:老渠道没有 settings 时,默认按 json 展示
data.vertex_key_type = 'json';
data.is_enterprise_account = false;
}
setInputs(data);
@@ -453,6 +464,8 @@ const EditChannelModal = (props) => {
} else {
setAutoBan(true);
}
// 同步企业账户状态
setIsEnterpriseAccount(data.is_enterprise_account || false);
setBasicModels(getChannelModels(data.type));
// 同步更新channelSettings状态显示
setChannelSettings({
@@ -712,6 +725,8 @@ const EditChannelModal = (props) => {
});
// 重置密钥模式状态
setKeyMode('append');
// 重置企业账户状态
setIsEnterpriseAccount(false);
// 清空表单中的key_mode字段
if (formApiRef.current) {
formApiRef.current.setValue('key_mode', undefined);
@@ -844,6 +859,10 @@ const EditChannelModal = (props) => {
showInfo(t('请至少选择一个模型!'));
return;
}
if (localInputs.type === 45 && (!localInputs.base_url || localInputs.base_url.trim() === '')) {
showInfo(t('请输入API地址'));
return;
}
if (
localInputs.model_mapping &&
localInputs.model_mapping !== '' &&
@@ -873,6 +892,21 @@ const EditChannelModal = (props) => {
};
localInputs.setting = JSON.stringify(channelExtraSettings);
// 处理type === 20的企业账户设置
if (localInputs.type === 20) {
let settings = {};
if (localInputs.settings) {
try {
settings = JSON.parse(localInputs.settings);
} catch (error) {
console.error('解析settings失败:', error);
}
}
// 设置企业账户标识无论是true还是false都要传到后端
settings.openrouter_enterprise = localInputs.is_enterprise_account === true;
localInputs.settings = JSON.stringify(settings);
}
// 清理不需要发送到后端的字段
delete localInputs.force_format;
delete localInputs.thinking_to_content;
@@ -880,6 +914,7 @@ const EditChannelModal = (props) => {
delete localInputs.pass_through_body_enabled;
delete localInputs.system_prompt;
delete localInputs.system_prompt_override;
delete localInputs.is_enterprise_account;
// 顶层的 vertex_key_type 不应发送给后端
delete localInputs.vertex_key_type;
@@ -1264,6 +1299,21 @@ const EditChannelModal = (props) => {
onChange={(value) => handleInputChange('type', value)}
/>
{inputs.type === 20 && (
<Form.Switch
field='is_enterprise_account'
label={t('是否为企业账户')}
checkedText={t('是')}
uncheckedText={t('否')}
onChange={(value) => {
setIsEnterpriseAccount(value);
handleInputChange('is_enterprise_account', value);
}}
extraText={t('企业账户为特殊返回格式,需要特殊处理,如果非企业账户,请勿勾选')}
initValue={inputs.is_enterprise_account}
/>
)}
<Form.Input
field='name'
label={t('名称')}
@@ -1883,6 +1933,30 @@ const EditChannelModal = (props) => {
/>
</div>
)}
{inputs.type === 45 && (
<div>
<Form.Select
field='base_url'
label={t('API地址')}
placeholder={t('请选择API地址')}
onChange={(value) =>
handleInputChange('base_url', value)
}
optionList={[
{
value: 'https://ark.cn-beijing.volces.com',
label: 'https://ark.cn-beijing.volces.com'
},
{
value: 'https://ark.ap-southeast.bytepluses.com',
label: 'https://ark.ap-southeast.bytepluses.com'
}
]}
defaultValue='https://ark.cn-beijing.volces.com'
/>
</div>
)}
</Card>
)}

View File

@@ -25,13 +25,9 @@ import {
showInfo,
showSuccess,
loadChannelModels,
copy,
copy
} from '../../helpers';
import {
CHANNEL_OPTIONS,
ITEMS_PER_PAGE,
MODEL_TABLE_PAGE_SIZE,
} from '../../constants';
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE, MODEL_TABLE_PAGE_SIZE } from '../../constants';
import { useIsMobile } from '../common/useIsMobile';
import { useTableCompactMode } from '../common/useTableCompactMode';
import { Modal } from '@douyinfe/semi-ui';
@@ -68,7 +64,7 @@ export const useChannelsData = () => {
// Status filter
const [statusFilter, setStatusFilter] = useState(
localStorage.getItem('channel-status-filter') || 'all',
localStorage.getItem('channel-status-filter') || 'all'
);
// Type tabs states
@@ -83,9 +79,10 @@ export const useChannelsData = () => {
const [testingModels, setTestingModels] = useState(new Set());
const [selectedModelKeys, setSelectedModelKeys] = useState([]);
const [isBatchTesting, setIsBatchTesting] = useState(false);
const [testQueue, setTestQueue] = useState([]);
const [isProcessingQueue, setIsProcessingQueue] = useState(false);
const [modelTablePage, setModelTablePage] = useState(1);
// 使用 ref 来避免闭包问题,类似旧版实现
const shouldStopBatchTestingRef = useRef(false);
// Multi-key management states
const [showMultiKeyManageModal, setShowMultiKeyManageModal] = useState(false);
@@ -119,12 +116,9 @@ export const useChannelsData = () => {
// Initialize from localStorage
useEffect(() => {
const localIdSort = localStorage.getItem('id-sort') === 'true';
const localPageSize =
parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
const localEnableTagMode =
localStorage.getItem('enable-tag-mode') === 'true';
const localEnableBatchDelete =
localStorage.getItem('enable-batch-delete') === 'true';
const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
const localEnableTagMode = localStorage.getItem('enable-tag-mode') === 'true';
const localEnableBatchDelete = localStorage.getItem('enable-batch-delete') === 'true';
setIdSort(localIdSort);
setPageSize(localPageSize);
@@ -182,10 +176,7 @@ export const useChannelsData = () => {
// Save column preferences
useEffect(() => {
if (Object.keys(visibleColumns).length > 0) {
localStorage.setItem(
'channels-table-columns',
JSON.stringify(visibleColumns),
);
localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns));
}
}, [visibleColumns]);
@@ -299,21 +290,14 @@ export const useChannelsData = () => {
const { searchKeyword, searchGroup, searchModel } = getFormValues();
if (searchKeyword !== '' || searchGroup !== '' || searchModel !== '') {
setLoading(true);
await searchChannels(
enableTagMode,
typeKey,
statusF,
page,
pageSize,
idSort,
);
await searchChannels(enableTagMode, typeKey, statusF, page, pageSize, idSort);
setLoading(false);
return;
}
const reqId = ++requestCounter.current;
setLoading(true);
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
const res = await API.get(
`/api/channel/?p=${page}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}${typeParam}${statusParam}`,
@@ -327,10 +311,7 @@ export const useChannelsData = () => {
if (success) {
const { items, total, type_counts } = data;
if (type_counts) {
const sumAll = Object.values(type_counts).reduce(
(acc, v) => acc + v,
0,
);
const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
setTypeCounts({ ...type_counts, all: sumAll });
}
setChannelFormat(items, enableTagMode);
@@ -354,18 +335,11 @@ export const useChannelsData = () => {
setSearching(true);
try {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(
page,
pageSz,
sortFlag,
enableTagMode,
typeKey,
statusF,
);
await loadChannels(page, pageSz, sortFlag, enableTagMode, typeKey, statusF);
return;
}
const typeParam = typeKey !== 'all' ? `&type=${typeKey}` : '';
const typeParam = (typeKey !== 'all') ? `&type=${typeKey}` : '';
const statusParam = statusF !== 'all' ? `&status=${statusF}` : '';
const res = await API.get(
`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${sortFlag}&tag_mode=${enableTagMode}&p=${page}&page_size=${pageSz}${typeParam}${statusParam}`,
@@ -373,10 +347,7 @@ export const useChannelsData = () => {
const { success, message, data } = res.data;
if (success) {
const { items = [], total = 0, type_counts = {} } = data;
const sumAll = Object.values(type_counts).reduce(
(acc, v) => acc + v,
0,
);
const sumAll = Object.values(type_counts).reduce((acc, v) => acc + v, 0);
setTypeCounts({ ...type_counts, all: sumAll });
setChannelFormat(items, enableTagMode);
setChannelCount(total);
@@ -395,14 +366,7 @@ export const useChannelsData = () => {
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
await loadChannels(page, pageSize, idSort, enableTagMode);
} else {
await searchChannels(
enableTagMode,
activeTypeKey,
statusFilter,
page,
pageSize,
idSort,
);
await searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
}
};
@@ -488,16 +452,9 @@ export const useChannelsData = () => {
const { searchKeyword, searchGroup, searchModel } = getFormValues();
setActivePage(page);
if (searchKeyword === '' && searchGroup === '' && searchModel === '') {
loadChannels(page, pageSize, idSort, enableTagMode).then(() => {});
loadChannels(page, pageSize, idSort, enableTagMode).then(() => { });
} else {
searchChannels(
enableTagMode,
activeTypeKey,
statusFilter,
page,
pageSize,
idSort,
);
searchChannels(enableTagMode, activeTypeKey, statusFilter, page, pageSize, idSort);
}
};
@@ -513,14 +470,7 @@ export const useChannelsData = () => {
showError(reason);
});
} else {
searchChannels(
enableTagMode,
activeTypeKey,
statusFilter,
1,
size,
idSort,
);
searchChannels(enableTagMode, activeTypeKey, statusFilter, 1, size, idSort);
}
};
@@ -551,10 +501,7 @@ export const useChannelsData = () => {
showError(res?.data?.message || t('渠道复制失败'));
}
} catch (error) {
showError(
t('渠道复制失败: ') +
(error?.response?.data?.message || error?.message || error),
);
showError(t('渠道复制失败: ') + (error?.response?.data?.message || error?.message || error));
}
};
@@ -593,11 +540,7 @@ export const useChannelsData = () => {
data.priority = parseInt(data.priority);
break;
case 'weight':
if (
data.weight === undefined ||
data.weight < 0 ||
data.weight === ''
) {
if (data.weight === undefined || data.weight < 0 || data.weight === '') {
showInfo('权重必须是非负整数!');
return;
}
@@ -740,136 +683,226 @@ export const useChannelsData = () => {
const res = await API.post(`/api/channel/fix`);
const { success, message, data } = res.data;
if (success) {
showSuccess(
t('已修复 ${success} 个通道,失败 ${fails} 个通道。')
.replace('${success}', data.success)
.replace('${fails}', data.fails),
);
showSuccess(t('已修复 ${success} 个通道,失败 ${fails} 个通道。').replace('${success}', data.success).replace('${fails}', data.fails));
await refresh();
} else {
showError(message);
}
};
// Test channel
// Test channel - 单个模型测试,参考旧版实现
const testChannel = async (record, model) => {
setTestQueue((prev) => [...prev, { channel: record, model }]);
if (!isProcessingQueue) {
setIsProcessingQueue(true);
const testKey = `${record.id}-${model}`;
// 检查是否应该停止批量测试
if (shouldStopBatchTestingRef.current && isBatchTesting) {
return Promise.resolve();
}
};
// Process test queue
const processTestQueue = async () => {
if (!isProcessingQueue || testQueue.length === 0) return;
const { channel, model, indexInFiltered } = testQueue[0];
if (currentTestChannel && currentTestChannel.id === channel.id) {
let pageNo;
if (indexInFiltered !== undefined) {
pageNo = Math.floor(indexInFiltered / MODEL_TABLE_PAGE_SIZE) + 1;
} else {
const filteredModelsList = currentTestChannel.models
.split(',')
.filter((m) =>
m.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
);
const modelIdx = filteredModelsList.indexOf(model);
pageNo =
modelIdx !== -1
? Math.floor(modelIdx / MODEL_TABLE_PAGE_SIZE) + 1
: 1;
}
setModelTablePage(pageNo);
}
// 添加到正在测试的模型集合
setTestingModels(prev => new Set([...prev, model]));
try {
setTestingModels((prev) => new Set([...prev, model]));
const res = await API.get(
`/api/channel/test/${channel.id}?model=${model}`,
);
const res = await API.get(`/api/channel/test/${record.id}?model=${model}`);
// 检查是否在请求期间被停止
if (shouldStopBatchTestingRef.current && isBatchTesting) {
return Promise.resolve();
}
const { success, message, time } = res.data;
setModelTestResults((prev) => ({
// 更新测试结果
setModelTestResults(prev => ({
...prev,
[`${channel.id}-${model}`]: { success, time },
[testKey]: {
success,
message,
time: time || 0,
timestamp: Date.now()
}
}));
if (success) {
updateChannelProperty(channel.id, (ch) => {
ch.response_time = time * 1000;
ch.test_time = Date.now() / 1000;
// 更新渠道响应时间
updateChannelProperty(record.id, (channel) => {
channel.response_time = time * 1000;
channel.test_time = Date.now() / 1000;
});
if (!model) {
if (!model || model === '') {
showInfo(
t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。')
.replace('${name}', channel.name)
.replace('${name}', record.name)
.replace('${time.toFixed(2)}', time.toFixed(2)),
);
} else {
showInfo(
t('通道 ${name} 测试成功,模型 ${model} 耗时 ${time.toFixed(2)} 秒。')
.replace('${name}', record.name)
.replace('${model}', model)
.replace('${time.toFixed(2)}', time.toFixed(2)),
);
}
} else {
showError(message);
showError(`${t('模型')} ${model}: ${message}`);
}
} catch (error) {
showError(error.message);
// 处理网络错误
const testKey = `${record.id}-${model}`;
setModelTestResults(prev => ({
...prev,
[testKey]: {
success: false,
message: error.message || t('网络错误'),
time: 0,
timestamp: Date.now()
}
}));
showError(`${t('模型')} ${model}: ${error.message || t('测试失败')}`);
} finally {
setTestingModels((prev) => {
// 从正在测试的模型集合中移除
setTestingModels(prev => {
const newSet = new Set(prev);
newSet.delete(model);
return newSet;
});
}
setTestQueue((prev) => prev.slice(1));
};
// Monitor queue changes
useEffect(() => {
if (testQueue.length > 0 && isProcessingQueue) {
processTestQueue();
} else if (testQueue.length === 0 && isProcessingQueue) {
setIsProcessingQueue(false);
setIsBatchTesting(false);
}
}, [testQueue, isProcessingQueue]);
// Batch test models
// 批量测试单个渠道的所有模型,参考旧版实现
const batchTestModels = async () => {
if (!currentTestChannel) return;
if (!currentTestChannel || !currentTestChannel.models) {
showError(t('渠道模型信息不完整'));
return;
}
const models = currentTestChannel.models.split(',').filter(model =>
model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
);
if (models.length === 0) {
showError(t('没有找到匹配的模型'));
return;
}
setIsBatchTesting(true);
setModelTablePage(1);
shouldStopBatchTestingRef.current = false; // 重置停止标志
const filteredModels = currentTestChannel.models
.split(',')
.filter((model) =>
model.toLowerCase().includes(modelSearchKeyword.toLowerCase()),
);
// 清空该渠道之前的测试结果
setModelTestResults(prev => {
const newResults = { ...prev };
models.forEach(model => {
const testKey = `${currentTestChannel.id}-${model}`;
delete newResults[testKey];
});
return newResults;
});
setTestQueue(
filteredModels.map((model, idx) => ({
channel: currentTestChannel,
model,
indexInFiltered: idx,
})),
);
setIsProcessingQueue(true);
try {
showInfo(t('开始批量测试 ${count} 个模型,已清空上次结果...').replace('${count}', models.length));
// 提高并发数量以加快测试速度,参考旧版的并发限制
const concurrencyLimit = 5;
const results = [];
for (let i = 0; i < models.length; i += concurrencyLimit) {
// 检查是否应该停止
if (shouldStopBatchTestingRef.current) {
showInfo(t('批量测试已停止'));
break;
}
const batch = models.slice(i, i + concurrencyLimit);
showInfo(t('正在测试第 ${current} - ${end} 个模型 (共 ${total} 个)')
.replace('${current}', i + 1)
.replace('${end}', Math.min(i + concurrencyLimit, models.length))
.replace('${total}', models.length)
);
const batchPromises = batch.map(model => testChannel(currentTestChannel, model));
const batchResults = await Promise.allSettled(batchPromises);
results.push(...batchResults);
// 再次检查是否应该停止
if (shouldStopBatchTestingRef.current) {
showInfo(t('批量测试已停止'));
break;
}
// 短暂延迟避免过于频繁的请求
if (i + concurrencyLimit < models.length) {
await new Promise(resolve => setTimeout(resolve, 100));
}
}
if (!shouldStopBatchTestingRef.current) {
// 等待一小段时间确保所有结果都已更新
await new Promise(resolve => setTimeout(resolve, 300));
// 使用当前状态重新计算结果统计
setModelTestResults(currentResults => {
let successCount = 0;
let failCount = 0;
models.forEach(model => {
const testKey = `${currentTestChannel.id}-${model}`;
const result = currentResults[testKey];
if (result && result.success) {
successCount++;
} else {
failCount++;
}
});
// 显示完成消息
setTimeout(() => {
showSuccess(t('批量测试完成!成功: ${success}, 失败: ${fail}, 总计: ${total}')
.replace('${success}', successCount)
.replace('${fail}', failCount)
.replace('${total}', models.length)
);
}, 100);
return currentResults; // 不修改状态,只是为了获取最新值
});
}
} catch (error) {
showError(t('批量测试过程中发生错误: ') + error.message);
} finally {
setIsBatchTesting(false);
}
};
// 停止批量测试
const stopBatchTesting = () => {
shouldStopBatchTestingRef.current = true;
setIsBatchTesting(false);
setTestingModels(new Set());
showInfo(t('已停止批量测试'));
};
// 清空测试结果
const clearTestResults = () => {
setModelTestResults({});
showInfo(t('已清空测试结果'));
};
// Handle close modal
const handleCloseModal = () => {
// 如果正在批量测试,先停止测试
if (isBatchTesting) {
setTestQueue([]);
setIsProcessingQueue(false);
setIsBatchTesting(false);
showSuccess(t('已停止测试'));
} else {
setShowModelTestModal(false);
setModelSearchKeyword('');
setSelectedModelKeys([]);
setModelTablePage(1);
shouldStopBatchTestingRef.current = true;
showInfo(t('关闭弹窗,已停止批量测试'));
}
setShowModelTestModal(false);
setModelSearchKeyword('');
setIsBatchTesting(false);
setTestingModels(new Set());
setSelectedModelKeys([]);
setModelTablePage(1);
// 可选择性保留测试结果,这里不清空以便用户查看
};
// Type counts
@@ -1012,4 +1045,4 @@ export const useChannelsData = () => {
setCompactMode,
setActivePage,
};
};
};