feat: 扩展管理模型列表提供商支持并优化健康检查

- 将 claude-custom 添加到管理模型列表提供商集合中
- 优化模型列表提取逻辑,避免不必要的协议转换
- 重构工具函数导入,提高代码组织性
- 增强暗黑主题下的模型选择器样式支持
- 改进健康检查逻辑,为管理模型列表提供商自动选择测试模型
- 添加文件锁机制防止配置持久化时的并发写入冲突
This commit is contained in:
hex2077 2026-04-06 16:59:09 +08:00
parent fb26659c23
commit 9f270e714d
5 changed files with 68 additions and 46 deletions

View file

@ -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));

View file

@ -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) {

View file

@ -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 [];

View file

@ -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;

View file

@ -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 {