feat: 支持批量关联配置文件并优化模型列表管理
- 修改批量关联功能,从逐个请求改为批量处理,提高效率 - 扩展快速链接API,支持同时处理多个配置文件 - 优化iFlow模型获取逻辑,支持手动添加多个模型 - 更新提供商模型列表,添加新模型并调整顺序
This commit is contained in:
parent
81a3c44771
commit
57e2165fb8
4 changed files with 155 additions and 112 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue