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 = [
|
||||
MODEL_PROVIDER.OPENAI_CUSTOM,
|
||||
MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES
|
||||
'openai-custom',
|
||||
'openaiResponses-custom',
|
||||
'claude-custom'
|
||||
];
|
||||
|
||||
export function getManagedModelListProviderType(providerType) {
|
||||
|
|
@ -165,10 +166,13 @@ function extractModelIdsFromListShape(modelList) {
|
|||
export function extractModelIdsFromNativeList(modelList, providerType) {
|
||||
let convertedModelList = modelList;
|
||||
|
||||
try {
|
||||
convertedModelList = convertData(modelList, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
|
||||
} catch {
|
||||
convertedModelList = modelList;
|
||||
// 只有在提供商类型与目标类型协议不同时才尝试转换
|
||||
if (providerType !== MODEL_PROVIDER.OPENAI_CUSTOM && !providerType.startsWith(MODEL_PROVIDER.OPENAI_CUSTOM + '-')) {
|
||||
try {
|
||||
convertedModelList = convertData(modelList, 'modelList', providerType, MODEL_PROVIDER.OPENAI_CUSTOM);
|
||||
} catch {
|
||||
convertedModelList = modelList;
|
||||
}
|
||||
}
|
||||
|
||||
const convertedIds = normalizeModelIds(extractModelIdsFromListShape(convertedModelList));
|
||||
|
|
|
|||
|
|
@ -138,7 +138,23 @@ async function runProviderHealthCheck(providerPoolManager, providerType, provide
|
|||
const providerConfig = providerStatus.config;
|
||||
|
||||
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) {
|
||||
providerPoolManager.markProviderHealthy(providerType, providerConfig, false, healthResult.modelName);
|
||||
|
|
@ -404,18 +420,13 @@ export async function handleDetectProviderModels(req, res, currentConfig, provid
|
|||
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 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 instanceKey = `${providerType}${detectionUuid}`;
|
||||
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}`);
|
||||
|
||||
const result = await runProviderHealthCheck(providerPoolManager, providerType, providerStatus);
|
||||
const filePath = persistProviderStatusToFile(currentConfig, providerPoolManager);
|
||||
|
||||
// 使用文件锁进行持久化,防止并发写入冲突
|
||||
const filePath = await withFileLock(async () => {
|
||||
return persistProviderStatusToFile(currentConfig, providerPoolManager);
|
||||
});
|
||||
|
||||
broadcastEvent('config_update', {
|
||||
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
|
||||
const successCount = results.filter(r => r.success).length;
|
||||
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
|
||||
if (providerPoolManager) {
|
||||
|
|
|
|||
|
|
@ -76,30 +76,17 @@ export const MODEL_PROVIDER = {
|
|||
AUTO: 'auto',
|
||||
}
|
||||
|
||||
const MANAGED_MODEL_LIST_PROVIDER_TYPES = new Set([
|
||||
'openai-custom',
|
||||
'openaiResponses-custom'
|
||||
]);
|
||||
|
||||
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));
|
||||
}
|
||||
import {
|
||||
usesManagedModelList,
|
||||
getConfiguredSupportedModels
|
||||
} from '../providers/provider-models.js';
|
||||
|
||||
/**
|
||||
* 获取指定提供商类型下,所有节点配置的已选模型列表(去重聚合)
|
||||
* @param {object} providerPoolManager - 提供商池管理器
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {string[]} 聚合后的模型 ID 列表
|
||||
*/
|
||||
function getConfiguredSupportedModelsFromPool(providerPoolManager, providerType) {
|
||||
if (!providerPoolManager?.providerStatus?.[providerType]) {
|
||||
return [];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { escapeHtml, showToast, getFieldLabel, getProviderTypeFields } from './u
|
|||
import { handleProviderPasswordToggle } from './event-handlers.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;
|
||||
|
|
|
|||
|
|
@ -1269,12 +1269,16 @@
|
|||
}
|
||||
|
||||
.provider-model-picker-empty {
|
||||
padding: 24px 12px;
|
||||
grid-column: 1 / -1;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
color: var(--neutral-500);
|
||||
border: 1px dashed var(--neutral-300);
|
||||
border-radius: 8px;
|
||||
border: 2px dashed var(--neutral-300);
|
||||
border-radius: 12px;
|
||||
background: var(--neutral-100);
|
||||
margin: 10px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@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"] .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"] .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"] .highlight-note {
|
||||
|
|
|
|||
Loading…
Reference in a new issue