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

View file

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

View file

@ -810,34 +810,22 @@ export async function handleHealthCheck(req, res, currentConfig, providerPoolMan
/** /**
* 快速链接配置文件到对应的提供商 * 快速链接配置文件到对应的提供商
* 支持单个文件路径或文件路径数组
*/ */
export async function handleQuickLinkProvider(req, res, currentConfig, providerPoolManager) { export async function handleQuickLinkProvider(req, res, currentConfig, providerPoolManager) {
try { try {
const body = await getRequestBody(req); 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.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; 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'; const poolsFilePath = currentConfig.PROVIDER_POOLS_FILE_PATH || 'configs/provider_pools.json';
// Load existing pools // Load existing pools
@ -851,70 +839,116 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
} }
} }
// Ensure provider type array exists const results = [];
if (!providerPools[providerType]) { const linkedProviders = [];
providerPools[providerType] = [];
// 处理每个文件路径
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 - 使用标准化路径进行比较 // Save to file only if there were successful links
const normalizedForComparison = filePath.replace(/\\/g, '/'); const successCount = results.filter(r => r.success).length;
const isAlreadyLinked = providerPools[providerType].some(p => { if (successCount > 0) {
const existingPath = p[credPathKey]; writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
if (!existingPath) return false;
const normalizedExistingPath = existingPath.replace(/\\/g, '/');
return normalizedExistingPath === normalizedForComparison ||
normalizedExistingPath === './' + normalizedForComparison ||
'./' + normalizedExistingPath === normalizedForComparison;
});
if (isAlreadyLinked) { // Update provider pool manager if available
res.writeHead(400, { 'Content-Type': 'application/json' }); if (providerPoolManager) {
res.end(JSON.stringify({ error: { message: 'This config file is already linked' } })); providerPoolManager.providerPools = providerPools;
return true; 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 failCount = results.filter(r => !r.success).length;
const newProvider = createProviderConfig({ const message = successCount > 0
credPathKey, ? `Successfully linked ${successCount} config file(s)${failCount > 0 ? `, ${failCount} failed` : ''}`
credPath: formatSystemPath(filePath), : `Failed to link all ${failCount} config file(s)`;
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()
});
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ res.end(JSON.stringify({
success: true, success: successCount > 0,
message: `Config successfully linked to ${displayName}`, message: message,
provider: newProvider, successCount: successCount,
providerType: providerType failCount: failCount,
results: results
})); }));
return true; return true;
} catch (error) { } catch (error) {

View file

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