feat: 扩展管理模型列表提供商支持并优化健康检查
- 将 claude-custom 添加到管理模型列表提供商集合中 - 优化模型列表提取逻辑,避免不必要的协议转换 - 重构工具函数导入,提高代码组织性 - 增强暗黑主题下的模型选择器样式支持 - 改进健康检查逻辑,为管理模型列表提供商自动选择测试模型 - 添加文件锁机制防止配置持久化时的并发写入冲突
This commit is contained in:
parent
fb26659c23
commit
9f270e714d
5 changed files with 68 additions and 46 deletions
|
|
@ -113,8 +113,9 @@ export const PROVIDER_MODELS = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MANAGED_MODEL_LIST_PROVIDERS = [
|
export const MANAGED_MODEL_LIST_PROVIDERS = [
|
||||||
MODEL_PROVIDER.OPENAI_CUSTOM,
|
'openai-custom',
|
||||||
MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES
|
'openaiResponses-custom',
|
||||||
|
'claude-custom'
|
||||||
];
|
];
|
||||||
|
|
||||||
export function getManagedModelListProviderType(providerType) {
|
export function getManagedModelListProviderType(providerType) {
|
||||||
|
|
@ -165,10 +166,13 @@ function extractModelIdsFromListShape(modelList) {
|
||||||
export function extractModelIdsFromNativeList(modelList, providerType) {
|
export function extractModelIdsFromNativeList(modelList, providerType) {
|
||||||
let convertedModelList = modelList;
|
let convertedModelList = modelList;
|
||||||
|
|
||||||
try {
|
// 只有在提供商类型与目标类型协议不同时才尝试转换
|
||||||
convertedModelList = convertData(modelList, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
|
if (providerType !== MODEL_PROVIDER.OPENAI_CUSTOM && !providerType.startsWith(MODEL_PROVIDER.OPENAI_CUSTOM + '-')) {
|
||||||
} catch {
|
try {
|
||||||
convertedModelList = modelList;
|
convertedModelList = convertData(modelList, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
|
||||||
|
} catch {
|
||||||
|
convertedModelList = modelList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertedIds = normalizeModelIds(extractModelIdsFromListShape(convertedModelList));
|
const convertedIds = normalizeModelIds(extractModelIdsFromListShape(convertedModelList));
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,23 @@ async function runProviderHealthCheck(providerPoolManager, providerType, provide
|
||||||
const providerConfig = providerStatus.config;
|
const providerConfig = providerStatus.config;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const healthResult = await providerPoolManager._checkProviderHealth(providerType, providerConfig);
|
// 对于管理模型列表的提供商,如果配置了支持的模型,从中挑选一个用于健康检查
|
||||||
|
let checkModelName = providerConfig.checkModelName;
|
||||||
|
if (!checkModelName && usesManagedModelList(providerType)) {
|
||||||
|
const supportedModels = getConfiguredSupportedModels(providerType, providerConfig);
|
||||||
|
if (supportedModels.length > 0) {
|
||||||
|
// 优先挑选常见的/轻量级的模型,或者直接取第一个
|
||||||
|
checkModelName = supportedModels.find(m =>
|
||||||
|
m.includes('flash') || m.includes('mini') || m.includes('3.5') || m.includes('small')
|
||||||
|
) || supportedModels[0];
|
||||||
|
logger.info(`[UI API] Selected model ${checkModelName} for health check of managed provider ${providerConfig.uuid}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const healthResult = await providerPoolManager._checkProviderHealth(providerType, {
|
||||||
|
...providerConfig,
|
||||||
|
checkModelName
|
||||||
|
});
|
||||||
|
|
||||||
if (healthResult.success) {
|
if (healthResult.success) {
|
||||||
providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName);
|
providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName);
|
||||||
|
|
@ -404,18 +420,13 @@ export async function handleDetectProviderModels(req, res, currentConfig, provid
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerPools = loadProviderPools(currentConfig, providerPoolManager);
|
|
||||||
const providers = providerPools[providerType] || [];
|
|
||||||
const existingProvider = providers.find(provider => provider.uuid === providerUuid);
|
|
||||||
|
|
||||||
if (!existingProvider) {
|
|
||||||
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
||||||
res.end(JSON.stringify({ error: { message: 'Provider not found' } }));
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const body = await getRequestBody(req);
|
const body = await getRequestBody(req);
|
||||||
const draftConfig = filterMaskedData(body?.providerConfig || {});
|
const draftConfig = filterMaskedData(body?.providerConfig || {});
|
||||||
|
|
||||||
|
const providerPools = loadProviderPools(currentConfig, providerPoolManager);
|
||||||
|
const providers = providerPools[providerType] || [];
|
||||||
|
const existingProvider = providers.find(provider => provider.uuid === providerUuid) || {};
|
||||||
|
|
||||||
const detectionUuid = `${providerUuid}-detect-models`;
|
const detectionUuid = `${providerUuid}-detect-models`;
|
||||||
const instanceKey = `${providerType}${detectionUuid}`;
|
const instanceKey = `${providerType}${detectionUuid}`;
|
||||||
const tempConfig = {
|
const tempConfig = {
|
||||||
|
|
@ -1244,7 +1255,11 @@ export async function handleSingleProviderHealthCheck(req, res, currentConfig, p
|
||||||
logger.info(`[UI API] Starting single health check for provider ${providerUuid} in ${providerType}`);
|
logger.info(`[UI API] Starting single health check for provider ${providerUuid} in ${providerType}`);
|
||||||
|
|
||||||
const result = await runProviderHealthCheck(providerPoolManager, providerType, providerStatus);
|
const result = await runProviderHealthCheck(providerPoolManager, providerType, providerStatus);
|
||||||
const filePath = persistProviderStatusToFile(currentConfig, providerPoolManager);
|
|
||||||
|
// 使用文件锁进行持久化,防止并发写入冲突
|
||||||
|
const filePath = await withFileLock(async () => {
|
||||||
|
return persistProviderStatusToFile(currentConfig, providerPoolManager);
|
||||||
|
});
|
||||||
|
|
||||||
broadcastEvent('config_update', {
|
broadcastEvent('config_update', {
|
||||||
action: 'health_check_single',
|
action: 'health_check_single',
|
||||||
|
|
@ -1376,7 +1391,10 @@ export async function handleQuickLinkProvider(req, res, currentConfig, providerP
|
||||||
// Save to file only if there were successful links
|
// Save to file only if there were successful links
|
||||||
const successCount = results.filter(r => r.success).length;
|
const successCount = results.filter(r => r.success).length;
|
||||||
if (successCount > 0) {
|
if (successCount > 0) {
|
||||||
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
|
await withFileLock(async () => {
|
||||||
|
writeFileSync(poolsFilePath, JSON.stringify(providerPools, null, 2), 'utf-8');
|
||||||
|
return poolsFilePath;
|
||||||
|
});
|
||||||
|
|
||||||
// Update provider pool manager if available
|
// Update provider pool manager if available
|
||||||
if (providerPoolManager) {
|
if (providerPoolManager) {
|
||||||
|
|
|
||||||
|
|
@ -76,30 +76,17 @@ export const MODEL_PROVIDER = {
|
||||||
AUTO: 'auto',
|
AUTO: 'auto',
|
||||||
}
|
}
|
||||||
|
|
||||||
const MANAGED_MODEL_LIST_PROVIDER_TYPES = new Set([
|
import {
|
||||||
'openai-custom',
|
usesManagedModelList,
|
||||||
'openaiResponses-custom'
|
getConfiguredSupportedModels
|
||||||
]);
|
} from '../providers/provider-models.js';
|
||||||
|
|
||||||
function usesManagedModelList(providerType = '') {
|
|
||||||
return [...MANAGED_MODEL_LIST_PROVIDER_TYPES].some(baseType =>
|
|
||||||
providerType === baseType || providerType.startsWith(baseType + '-')
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConfiguredSupportedModels(providerType, providerConfig = {}) {
|
|
||||||
if (!usesManagedModelList(providerType)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...new Set(
|
|
||||||
(Array.isArray(providerConfig?.supportedModels) ? providerConfig.supportedModels : [])
|
|
||||||
.filter(model => typeof model === 'string')
|
|
||||||
.map(model => model.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
)].sort((a, b) => a.localeCompare(b));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定提供商类型下,所有节点配置的已选模型列表(去重聚合)
|
||||||
|
* @param {object} providerPoolManager - 提供商池管理器
|
||||||
|
* @param {string} providerType - 提供商类型
|
||||||
|
* @returns {string[]} 聚合后的模型 ID 列表
|
||||||
|
*/
|
||||||
function getConfiguredSupportedModelsFromPool(providerPoolManager, providerType) {
|
function getConfiguredSupportedModelsFromPool(providerPoolManager, providerType) {
|
||||||
if (!providerPoolManager?.providerStatus?.[providerType]) {
|
if (!providerPoolManager?.providerStatus?.[providerType]) {
|
||||||
return [];
|
return [];
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { escapeHtml, showToast, getFieldLabel, getProviderTypeFields } from './u
|
||||||
import { handleProviderPasswordToggle } from './event-handlers.js';
|
import { handleProviderPasswordToggle } from './event-handlers.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
|
|
||||||
const MANAGED_MODEL_LIST_PROVIDERS = new Set(['openai-custom', 'openaiResponses-custom']);
|
const MANAGED_MODEL_LIST_PROVIDERS = new Set(['openai-custom', 'openaiResponses-custom', 'claude-custom']);
|
||||||
|
|
||||||
// 分页配置
|
// 分页配置
|
||||||
const PROVIDERS_PER_PAGE = 5;
|
const PROVIDERS_PER_PAGE = 5;
|
||||||
|
|
|
||||||
|
|
@ -1269,12 +1269,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-model-picker-empty {
|
.provider-model-picker-empty {
|
||||||
padding: 24px 12px;
|
grid-column: 1 / -1;
|
||||||
|
padding: 40px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--neutral-500);
|
color: var(--neutral-500);
|
||||||
border: 1px dashed var(--neutral-300);
|
border: 2px dashed var(--neutral-300);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
background: var(--neutral-100);
|
background: var(--neutral-100);
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
|
|
@ -1638,6 +1642,15 @@
|
||||||
[data-theme="dark"] .auth-url-input { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-primary); }
|
[data-theme="dark"] .auth-url-input { background: var(--bg-tertiary); border-color: var(--border-color); color: var(--text-primary); }
|
||||||
[data-theme="dark"] .model-checkbox-label { background: var(--bg-primary); border-color: var(--border-color); }
|
[data-theme="dark"] .model-checkbox-label { background: var(--bg-primary); border-color: var(--border-color); }
|
||||||
[data-theme="dark"] .not-supported-models-container { background: var(--bg-secondary); border-color: var(--border-color); }
|
[data-theme="dark"] .not-supported-models-container { background: var(--bg-secondary); border-color: var(--border-color); }
|
||||||
|
[data-theme="dark"] .supported-models-container { background: var(--bg-secondary); border-color: var(--border-color); }
|
||||||
|
[data-theme="dark"] .supported-model-tag { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .supported-models-empty { color: var(--text-secondary); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-empty { background: var(--bg-secondary); border-color: var(--border-color); color: var(--text-secondary); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-search { background: var(--bg-secondary); border-color: var(--border-color); color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-item { background: var(--bg-primary); border-color: var(--border-color); color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-item:hover { background: var(--bg-tertiary); border-color: var(--primary-color); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-item span { color: var(--text-primary); }
|
||||||
|
[data-theme="dark"] .provider-model-picker-summary { color: var(--text-secondary); }
|
||||||
[data-theme="dark"] .stat-card { transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; }
|
[data-theme="dark"] .stat-card { transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease; }
|
||||||
/* 高亮说明样式 - 暗黑主题 */
|
/* 高亮说明样式 - 暗黑主题 */
|
||||||
[data-theme="dark"] .highlight-note {
|
[data-theme="dark"] .highlight-note {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue