From 9f270e714d1567d31551c2bcb207b8b11f3f52f6 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Mon, 6 Apr 2026 16:59:09 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=A9=E5=B1=95=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8=E6=8F=90=E4=BE=9B=E5=95=86?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B9=B6=E4=BC=98=E5=8C=96=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 claude-custom 添加到管理模型列表提供商集合中 - 优化模型列表提取逻辑,避免不必要的协议转换 - 重构工具函数导入,提高代码组织性 - 增强暗黑主题下的模型选择器样式支持 - 改进健康检查逻辑,为管理模型列表提供商自动选择测试模型 - 添加文件锁机制防止配置持久化时的并发写入冲突 --- src/providers/provider-models.js | 16 +++++---- src/ui-modules/provider-api.js | 44 +++++++++++++++++-------- src/utils/common.js | 33 ++++++------------- static/app/modal.js | 2 +- static/components/section-providers.css | 19 +++++++++-- 5 files changed, 68 insertions(+), 46 deletions(-) diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js index a007db4..d5abbab 100644 --- a/src/providers/provider-models.js +++ b/src/providers/provider-models.js @@ -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)); diff --git a/src/ui-modules/provider-api.js b/src/ui-modules/provider-api.js index 7d680ca..063419f 100644 --- a/src/ui-modules/provider-api.js +++ b/src/ui-modules/provider-api.js @@ -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) { diff --git a/src/utils/common.js b/src/utils/common.js index f6b801f..6c85584 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -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 []; diff --git a/static/app/modal.js b/static/app/modal.js index 404df5f..709d1db 100644 --- a/static/app/modal.js +++ b/static/app/modal.js @@ -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; diff --git a/static/components/section-providers.css b/static/components/section-providers.css index 8349743..4953a59 100644 --- a/static/components/section-providers.css +++ b/static/components/section-providers.css @@ -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 {