feat: 支持批量关联配置文件并优化模型列表管理

- 修改批量关联功能,从逐个请求改为批量处理,提高效率
- 扩展快速链接API,支持同时处理多个配置文件
- 优化iFlow模型获取逻辑,支持手动添加多个模型
- 更新提供商模型列表,添加新模型并调整顺序
This commit is contained in:
hex2077 2026-02-05 17:52:22 +08:00
parent 81a3c44771
commit 57e2165fb8
4 changed files with 155 additions and 112 deletions

View file

@ -1054,34 +1054,41 @@ export class IFlowApiService {
await this.initialize();
}
// 需要手动添加的模型列表
const manualModels = ['glm-4.7', 'kimi-k2.5', 'minimax-m2.1'];
try {
const response = await this.axiosInstance.get('/models', {
headers: this._getHeaders(false)
});
// 检查返回数据中是否包含 glm-4.7,如果没有则添加
// 检查返回数据中是否包含手动添加的模型,如果没有则添加
const modelsData = response.data;
if (modelsData && modelsData.data && Array.isArray(modelsData.data)) {
const hasGlm47 = modelsData.data.some(model => model.id === 'glm-4.7');
if (!hasGlm47) {
// 添加 glm-4.7 模型到返回列表
modelsData.data.push({
id: 'glm-4.7',
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: 'iflow'
});
logger.info('[iFlow] Added glm-4.7 to models list');
for (const modelId of manualModels) {
const hasModel = modelsData.data.some(model => model.id === modelId);
if (!hasModel) {
// 添加模型到返回列表
modelsData.data.push({
id: modelId,
object: 'model',
created: Math.floor(Date.now() / 1000),
owned_by: 'iflow'
});
logger.info(`[iFlow] Added ${modelId} to models list`);
}
}
}
return modelsData;
} catch (error) {
logger.warn('[iFlow] Failed to fetch models from API, using default list:', error.message);
// 返回默认模型列表,确保包含 glm-4.7
// 返回默认模型列表,确保包含手动添加的模型
const defaultModels = [...IFLOW_MODELS];
if (!defaultModels.includes('glm-4.7')) {
defaultModels.push('glm-4.7');
for (const modelId of manualModels) {
if (!defaultModels.includes(modelId)) {
defaultModels.push(modelId);
}
}
return {
object: 'list',

View file

@ -56,11 +56,14 @@ export const PROVIDER_MODELS = {
'kimi-k2',
// GLM 模型
'glm-4.6',
'glm-4.7',
// DeepSeek 模型
'deepseek-v3.2',
'deepseek-r1',
'deepseek-v3'
'deepseek-v3',
// 手动定义
'glm-4.7',
'kimi-k2.5',
'minimax-m2.1',
],
'openai-codex-oauth': [
'gpt-5',

View file

@ -810,34 +810,22 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
/**
* 快速链接配置文件到对应的提供商
* 支持单个文件路径或文件路径数组
*/
export async function handleQuickLinkProvider(req, res, currentConfig, providerPoolManager) {
try {
const body = await getRequestBody(req);
const { filePath } = body;
const { filePath, filePaths } = body;
if (!filePath) {
// 支持单个文件路径或文件路径数组
const pathsToLink = filePaths || (filePath ? [filePath] : []);
if (!pathsToLink || pathsToLink.length === 0) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'filePath is required' } }));
res.end(JSON.stringify({ error: { message: 'filePath or filePaths is required' } }));
return true;
}
const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase();
// 根据文件路径自动识别提供商类型
const providerMapping = detectProviderFromPath(normalizedPath);
if (!providerMapping) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: 'Unable to identify provider type for config file, please ensure file is in configs/kiro/, configs/gemini/, configs/qwen/ or configs/antigravity/ directory'
}
}));
return true;
}
const { providerType, credPathKey, defaultCheckModel, displayName } = providerMapping;
const poolsFilePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
// Load existing pools
@ -851,70 +839,116 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
}
}
// Ensure provider type array exists
if (!providerPools[providerType]) {
providerPools[providerType] = [];
const results = [];
const linkedProviders = [];
// 处理每个文件路径
for (const currentFilePath of pathsToLink) {
const normalizedPath = currentFilePath.replace(/\\/g, '/').toLowerCase();
// 根据文件路径自动识别提供商类型
const providerMapping = detectProviderFromPath(normalizedPath);
if (!providerMapping) {
results.push({
filePath: currentFilePath,
success: false,
error: 'Unable to identify provider type for config file'
});
continue;
}
const { providerType, credPathKey, defaultCheckModel, displayName } = providerMapping;
// Ensure provider type array exists
if (!providerPools[providerType]) {
providerPools[providerType] = [];
}
// Check if already linked - 使用标准化路径进行比较
const normalizedForComparison = currentFilePath.replace(/\\/g, '/');
const isAlreadyLinked = providerPools[providerType].some(p => {
const existingPath = p[credPathKey];
if (!existingPath) return false;
const normalizedExistingPath = existingPath.replace(/\\/g, '/');
return normalizedExistingPath === normalizedForComparison ||
normalizedExistingPath === './' + normalizedForComparison ||
'./' + normalizedExistingPath === normalizedForComparison;
});
if (isAlreadyLinked) {
results.push({
filePath: currentFilePath,
success: false,
error: 'This config file is already linked',
providerType: providerType
});
continue;
}
// Create new provider config based on provider type
const newProvider = createProviderConfig({
credPathKey,
credPath: formatSystemPath(currentFilePath),
defaultCheckModel,
needsProjectId: providerMapping.needsProjectId
});
providerPools[providerType].push(newProvider);
linkedProviders.push({ providerType, provider: newProvider });
results.push({
filePath: currentFilePath,
success: true,
providerType: providerType,
displayName: displayName,
provider: newProvider
});
logger.info(`[UI API] Quick linked config: ${currentFilePath} -> ${providerType}`);
}
// Check if already linked - 使用标准化路径进行比较
const normalizedForComparison = filePath.replace(/\\/g, '/');
const isAlreadyLinked = providerPools[providerType].some(p => {
const existingPath = p[credPathKey];
if (!existingPath) return false;
const normalizedExistingPath = existingPath.replace(/\\/g, '/');
return normalizedExistingPath === normalizedForComparison ||
normalizedExistingPath === './' + normalizedForComparison ||
'./' + normalizedExistingPath === normalizedForComparison;
});
// Save to file only if there were successful links
const successCount = results.filter(r => r.success).length;
if (successCount > 0) {
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
if (isAlreadyLinked) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message: 'This config file is already linked' } }));
return true;
// Update provider pool manager if available
if (providerPoolManager) {
providerPoolManager.providerPools = providerPools;
providerPoolManager.initializeProviderStatus();
}
// Broadcast update events
broadcastEvent('config_update', {
action: 'quick_link_batch',
filePath: poolsFilePath,
results: results,
timestamp: new Date().toISOString()
});
for (const { providerType, provider } of linkedProviders) {
broadcastEvent('provider_update', {
action: 'add',
providerType,
providerConfig: provider,
timestamp: new Date().toISOString()
});
}
}
// Create new provider config based on provider type
const newProvider = createProviderConfig({
credPathKey,
credPath: formatSystemPath(filePath),
defaultCheckModel,
needsProjectId: providerMapping.needsProjectId
});
providerPools[providerType].push(newProvider);
// Save to file
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
logger.info(`[UI API] Quick linked config: ${filePath} -> ${providerType}`);
// Update provider pool manager if available
if (providerPoolManager) {
providerPoolManager.providerPools = providerPools;
providerPoolManager.initializeProviderStatus();
}
// Broadcast update event
broadcastEvent('config_update', {
action: 'quick_link',
filePath: poolsFilePath,
providerType,
newProvider,
timestamp: new Date().toISOString()
});
broadcastEvent('provider_update', {
action: 'add',
providerType,
providerConfig: newProvider,
timestamp: new Date().toISOString()
});
const failCount = results.filter(r => !r.success).length;
const message = successCount > 0
? `Successfully linked ${successCount} config file(s)${failCount > 0 ? `, ${failCount} failed` : ''}`
: `Failed to link all ${failCount} config file(s)`;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
message: `Config successfully linked to ${displayName}`,
provider: newProvider,
providerType: providerType
success: successCount > 0,
message: message,
successCount: successCount,
failCount: failCount,
results: results
}));
return true;
} catch (error) {

View file

@ -941,28 +941,27 @@ async function batchLinkProviderConfigs() {
showToast(t('common.info'), t('upload.batchLink.processing', { count: unlinkedConfigs.length }), 'info');
let successCount = 0;
let failCount = 0;
for (const config of unlinkedConfigs) {
try {
await window.apiClient.post('/quick-link-provider', {
filePath: config.path
});
successCount++;
} catch (error) {
console.error(`关联失败: ${config.path}`, error);
failCount++;
try {
// 一次性传递所有文件路径进行批量关联
const filePaths = unlinkedConfigs.map(config => config.path);
const result = await window.apiClient.post('/quick-link-provider', {
filePaths: filePaths
});
// 刷新配置列表
await loadConfigList();
if (result.failCount === 0) {
showToast(t('common.success'), t('upload.batchLink.success', { count: result.successCount }), 'success');
} else {
showToast(t('common.warning'), t('upload.batchLink.partial', { success: result.successCount, fail: result.failCount }), 'warning');
}
}
// 刷新配置列表
await loadConfigList();
if (failCount === 0) {
showToast(t('common.success'), t('upload.batchLink.success', { count: successCount }), 'success');
} else {
showToast(t('common.warning'), t('upload.batchLink.partial', { success: successCount, fail: failCount }), 'warning');
} catch (error) {
console.error('批量关联失败:', error);
showToast(t('common.error'), t('upload.batchLink.failed') + ': ' + error.message, 'error');
// 即使失败也刷新列表,可能部分成功
await loadConfigList();
}
}