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();
|
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',
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue