feat(provider): 实现基于模型选择的提供商池管理功能
添加 provider-models.js 集中管理各提供商支持的模型列表 修改 provider-pool-manager.js 支持根据请求模型过滤提供商 更新服务管理器和请求处理逻辑以支持模型感知的提供商选择 添加前端UI支持配置不支持的模型列表 更新示例配置文件展示新功能
This commit is contained in:
parent
9c114e2a7d
commit
fec0b19bd4
13 changed files with 364 additions and 21 deletions
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"REQUIRED_API_KEY": "123456",
|
||||
"SERVER_PORT": 3000,
|
||||
"HOST": "127.0.0.1",
|
||||
"HOST": "0.0.0.0",
|
||||
"MODEL_PROVIDER": "gemini-cli-oauth",
|
||||
"OPENAI_API_KEY": "xxx",
|
||||
"OPENAI_BASE_URL": "https://openai/v1",
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||
"checkModelName": null,
|
||||
"checkHealth": true,
|
||||
"notSupportedModels": ["gpt-4-turbo"],
|
||||
"uuid": "2f579c65-d3c5-41b1-9985-9f6e3d7bf39c",
|
||||
"isHealthy": true,
|
||||
"isDisabled": false,
|
||||
|
|
@ -18,6 +19,7 @@
|
|||
"OPENAI_BASE_URL": "https://api.openai.com/v1",
|
||||
"checkModelName": null,
|
||||
"checkHealth": true,
|
||||
"notSupportedModels": ["gpt-4-turbo", "gpt-4"],
|
||||
"uuid": "e284628d-302f-456d-91f3-6095386fb3b8",
|
||||
"isHealthy": true,
|
||||
"isDisabled": true,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { promises as fs } from 'fs';
|
|||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
|
||||
const KIRO_CONSTANTS = {
|
||||
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
|
||||
|
|
@ -20,7 +21,11 @@ const KIRO_CONSTANTS = {
|
|||
ORIGIN_AI_EDITOR: 'AI_EDITOR',
|
||||
};
|
||||
|
||||
const MODEL_MAPPING = {
|
||||
// 从 provider-models.js 获取支持的模型列表
|
||||
const KIRO_MODELS = getProviderModels('claude-kiro-oauth');
|
||||
|
||||
// 完整的模型映射表
|
||||
const FULL_MODEL_MAPPING = {
|
||||
"claude-opus-4-5":"claude-opus-4.5",
|
||||
"claude-sonnet-4-5": "CLAUDE_SONNET_4_5_20250929_V1_0",
|
||||
"claude-sonnet-4-5-20250929": "CLAUDE_SONNET_4_5_20250929_V1_0",
|
||||
|
|
@ -30,6 +35,11 @@ const MODEL_MAPPING = {
|
|||
"amazonq-claude-3-7-sonnet-20250219": "CLAUDE_3_7_SONNET_20250219_V1_0"
|
||||
};
|
||||
|
||||
// 只保留 KIRO_MODELS 中存在的模型映射
|
||||
const MODEL_MAPPING = Object.fromEntries(
|
||||
Object.entries(FULL_MODEL_MAPPING).filter(([key]) => KIRO_MODELS.includes(key))
|
||||
);
|
||||
|
||||
const KIRO_AUTH_TOKEN_FILE = "kiro-auth-token.json";
|
||||
|
||||
/**
|
||||
|
|
@ -1115,7 +1125,7 @@ async initializeAuth(forceRefresh = false) {
|
|||
* List available models
|
||||
*/
|
||||
async listModels() {
|
||||
const models = Object.keys(MODEL_MAPPING).map(id => ({
|
||||
const models = KIRO_MODELS.map(id => ({
|
||||
name: id
|
||||
}));
|
||||
|
||||
|
|
|
|||
|
|
@ -399,6 +399,13 @@ export async function handleContentGenerationRequest(req, res, service, endpoint
|
|||
}
|
||||
console.log(`[Content Generation] Model: ${model}, Stream: ${isStream}`);
|
||||
|
||||
// 2.5. 如果使用了提供商池,根据模型重新选择提供商
|
||||
if (providerPoolManager && CONFIG.providerPools && CONFIG.providerPools[CONFIG.MODEL_PROVIDER]) {
|
||||
const { getApiService } = await import('./service-manager.js');
|
||||
service = await getApiService(CONFIG, model);
|
||||
console.log(`[Content Generation] Re-selected service adapter based on model: ${model}`);
|
||||
}
|
||||
|
||||
// 3. Apply system prompt from file if configured.
|
||||
processedRequestBody = await _applySystemPromptFromFile(CONFIG, processedRequestBody, toProvider);
|
||||
await _manageSystemPrompt(processedRequestBody, toProvider);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as path from 'path';
|
|||
import * as os from 'os';
|
||||
import * as readline from 'readline';
|
||||
import { API_ACTIONS, formatExpiryTime } from '../common.js';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
|
||||
// --- Constants ---
|
||||
const AUTH_REDIRECT_PORT = 8085;
|
||||
|
|
@ -14,7 +15,7 @@ const CODE_ASSIST_ENDPOINT = 'https://cloudcode-pa.googleapis.com';
|
|||
const CODE_ASSIST_API_VERSION = 'v1internal';
|
||||
const OAUTH_CLIENT_ID = '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
|
||||
const OAUTH_CLIENT_SECRET = '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';
|
||||
const GEMINI_MODELS = ['gemini-2.5-flash', 'gemini-2.5-flash-lite', 'gemini-2.5-pro' , 'gemini-2.5-pro-preview-06-05', 'gemini-2.5-flash-preview-09-2025', 'gemini-3-pro-preview'];
|
||||
const GEMINI_MODELS = getProviderModels('gemini-cli-oauth');
|
||||
const ANTI_TRUNCATION_MODELS = GEMINI_MODELS.map(model => `anti-${model}`);
|
||||
|
||||
function is_anti_truncation_model(model) {
|
||||
|
|
|
|||
|
|
@ -505,7 +505,7 @@ export async function handleOllamaChat(req, res, apiService, currentConfig, prov
|
|||
|
||||
if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) {
|
||||
// Select provider from pool
|
||||
const providerConfig = providerPoolManager.selectProvider(detectedProvider);
|
||||
const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName);
|
||||
if (providerConfig) {
|
||||
actualConfig = {
|
||||
...currentConfig,
|
||||
|
|
@ -601,7 +601,7 @@ export async function handleOllamaGenerate(req, res, apiService, currentConfig,
|
|||
|
||||
if (detectedProvider !== currentConfig.MODEL_PROVIDER && providerPoolManager) {
|
||||
// Select provider from pool
|
||||
const providerConfig = providerPoolManager.selectProvider(detectedProvider);
|
||||
const providerConfig = providerPoolManager.selectProvider(detectedProvider, modelName);
|
||||
if (providerConfig) {
|
||||
actualConfig = {
|
||||
...currentConfig,
|
||||
|
|
|
|||
|
|
@ -6,14 +6,17 @@ import * as os from 'os';
|
|||
import open from 'open';
|
||||
import { EventEmitter } from 'events';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { getProviderModels } from '../provider-models.js';
|
||||
|
||||
// --- Constants ---
|
||||
const QWEN_DIR = '.qwen';
|
||||
const QWEN_CREDENTIAL_FILENAME = 'oauth_creds.json';
|
||||
const QWEN_MODEL_LIST = [
|
||||
{ id: 'qwen3-coder-plus', name: 'Qwen3 Coder Plus' },
|
||||
{ id: 'qwen3-coder-flash', name: 'Qwen3 Coder Flash' },
|
||||
];
|
||||
// 从 provider-models.js 获取支持的模型列表
|
||||
const QWEN_MODELS = getProviderModels('openai-qwen-oauth');
|
||||
const QWEN_MODEL_LIST = QWEN_MODELS.map(id => ({
|
||||
id: id,
|
||||
name: id.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
|
||||
}));
|
||||
|
||||
const TOKEN_REFRESH_BUFFER_MS = 30 * 1000;
|
||||
const LOCK_TIMEOUT_MS = 10000;
|
||||
|
|
|
|||
48
src/provider-models.js
Normal file
48
src/provider-models.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* 各提供商支持的模型列表
|
||||
* 用于前端UI选择不支持的模型
|
||||
*/
|
||||
|
||||
export const PROVIDER_MODELS = {
|
||||
'gemini-cli-oauth': [
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-flash-lite',
|
||||
'gemini-2.5-pro',
|
||||
'gemini-2.5-pro-preview-06-05',
|
||||
'gemini-2.5-flash-preview-09-2025',
|
||||
'gemini-3-pro-preview'
|
||||
],
|
||||
'claude-custom': [],
|
||||
'claude-kiro-oauth': [
|
||||
'claude-opus-4-5',
|
||||
'claude-sonnet-4-5',
|
||||
'claude-sonnet-4-5-20250929',
|
||||
'claude-sonnet-4-20250514',
|
||||
'claude-3-7-sonnet-20250219',
|
||||
'amazonq-claude-sonnet-4-20250514',
|
||||
'amazonq-claude-3-7-sonnet-20250219'
|
||||
],
|
||||
'openai-custom': [],
|
||||
'openaiResponses-custom': [],
|
||||
'openai-qwen-oauth': [
|
||||
'qwen3-coder-plus',
|
||||
'qwen3-coder-flash'
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取指定提供商类型支持的模型列表
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @returns {Array<string>} 模型列表
|
||||
*/
|
||||
export function getProviderModels(providerType) {
|
||||
return PROVIDER_MODELS[providerType] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有提供商的模型列表
|
||||
* @returns {Object} 所有提供商的模型映射
|
||||
*/
|
||||
export function getAllProviderModels() {
|
||||
return PROVIDER_MODELS;
|
||||
}
|
||||
|
|
@ -93,10 +93,12 @@ export class ProviderPoolManager {
|
|||
/**
|
||||
* Selects a provider from the pool for a given provider type.
|
||||
* Currently uses a simple round-robin for healthy providers.
|
||||
* If requestedModel is provided, providers that don't support the model will be excluded.
|
||||
* @param {string} providerType - The type of provider to select (e.g., 'gemini-cli', 'openai-custom').
|
||||
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
||||
* @returns {object|null} The selected provider's configuration, or null if no healthy provider is found.
|
||||
*/
|
||||
selectProvider(providerType) {
|
||||
selectProvider(providerType, requestedModel = null) {
|
||||
// 参数校验
|
||||
if (!providerType || typeof providerType !== 'string') {
|
||||
this._log('error', `Invalid providerType: ${providerType}`);
|
||||
|
|
@ -104,28 +106,52 @@ export class ProviderPoolManager {
|
|||
}
|
||||
|
||||
const availableProviders = this.providerStatus[providerType] || [];
|
||||
const availableAndHealthyProviders = availableProviders.filter(p =>
|
||||
let availableAndHealthyProviders = availableProviders.filter(p =>
|
||||
p.config.isHealthy && !p.config.isDisabled
|
||||
);
|
||||
|
||||
// 如果指定了模型,则排除不支持该模型的提供商
|
||||
if (requestedModel) {
|
||||
const modelFilteredProviders = availableAndHealthyProviders.filter(p => {
|
||||
// 如果提供商没有配置 notSupportedModels,则认为它支持所有模型
|
||||
if (!p.config.notSupportedModels || !Array.isArray(p.config.notSupportedModels)) {
|
||||
return true;
|
||||
}
|
||||
// 检查 notSupportedModels 数组中是否包含请求的模型,如果包含则排除
|
||||
return !p.config.notSupportedModels.includes(requestedModel);
|
||||
});
|
||||
|
||||
if (modelFilteredProviders.length === 0) {
|
||||
this._log('warn', `No available providers for type: ${providerType} that support model: ${requestedModel}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
availableAndHealthyProviders = modelFilteredProviders;
|
||||
this._log('debug', `Filtered ${modelFilteredProviders.length} providers supporting model: ${requestedModel}`);
|
||||
}
|
||||
|
||||
if (availableAndHealthyProviders.length === 0) {
|
||||
this._log('warn', `No available and healthy providers for type: ${providerType}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 简化轮询逻辑
|
||||
const currentIndex = this.roundRobinIndex[providerType] || 0;
|
||||
// 为每个提供商类型和模型组合维护独立的轮询索引
|
||||
// 使用组合键:providerType 或 providerType:model
|
||||
const indexKey = requestedModel ? `${providerType}:${requestedModel}` : providerType;
|
||||
const currentIndex = this.roundRobinIndex[indexKey] || 0;
|
||||
|
||||
// 使用取模确保索引始终在有效范围内,即使列表长度变化
|
||||
const providerIndex = currentIndex % availableAndHealthyProviders.length;
|
||||
const selected = availableAndHealthyProviders[providerIndex];
|
||||
|
||||
// 更新下次轮询的索引
|
||||
this.roundRobinIndex[providerType] = (providerIndex + 1) % availableAndHealthyProviders.length;
|
||||
this.roundRobinIndex[indexKey] = (currentIndex + 1) % availableAndHealthyProviders.length;
|
||||
|
||||
// 更新使用信息
|
||||
selected.config.lastUsed = new Date().toISOString();
|
||||
selected.config.usageCount++;
|
||||
|
||||
this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}`);
|
||||
this._log('debug', `Selected provider for ${providerType} (round-robin): ${selected.config.uuid}${requestedModel ? ` for model: ${requestedModel}` : ''}`);
|
||||
|
||||
// 使用防抖保存
|
||||
this._debouncedSave(providerType);
|
||||
|
|
|
|||
|
|
@ -59,21 +59,22 @@ export async function initApiService(config) {
|
|||
/**
|
||||
* Get API service adapter, considering provider pools
|
||||
* @param {Object} config - The current request configuration
|
||||
* @param {string} [requestedModel] - Optional. The model name to filter providers by.
|
||||
* @returns {Promise<Object>} The API service adapter
|
||||
*/
|
||||
export async function getApiService(config) {
|
||||
export async function getApiService(config, requestedModel = null) {
|
||||
let serviceConfig = config;
|
||||
if (providerPoolManager && config.providerPools && config.providerPools[config.MODEL_PROVIDER]) {
|
||||
// 如果有号池管理器,并且当前模型提供者类型有对应的号池,则从号池中选择一个提供者配置
|
||||
const selectedProviderConfig = providerPoolManager.selectProvider(config.MODEL_PROVIDER);
|
||||
const selectedProviderConfig = providerPoolManager.selectProvider(config.MODEL_PROVIDER, requestedModel);
|
||||
if (selectedProviderConfig) {
|
||||
// 合并选中的提供者配置到当前请求的 config 中
|
||||
serviceConfig = deepmerge(config, selectedProviderConfig);
|
||||
delete serviceConfig.providerPools; // 移除 providerPools 属性
|
||||
config.uuid = serviceConfig.uuid;
|
||||
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}`);
|
||||
console.log(`[API Service] Using pooled configuration for ${config.MODEL_PROVIDER}: ${serviceConfig.uuid}${requestedModel ? ` (model: ${requestedModel})` : ''}`);
|
||||
} else {
|
||||
console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}. Falling back to main config.`);
|
||||
console.warn(`[API Service] No healthy provider found in pool for ${config.MODEL_PROVIDER}${requestedModel ? ` supporting model: ${requestedModel}` : ''}. Falling back to main config.`);
|
||||
}
|
||||
}
|
||||
return getServiceAdapter(serviceConfig);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import path from 'path';
|
|||
import multer from 'multer';
|
||||
import crypto from 'crypto';
|
||||
import { getRequestBody } from './common.js';
|
||||
import { getAllProviderModels, getProviderModels } from './provider-models.js';
|
||||
import { CONFIG } from './config-manager.js';
|
||||
import { serviceInstances } from './adapter.js';
|
||||
import { initApiService } from './service-manager.js';
|
||||
|
|
@ -669,6 +670,27 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
return true;
|
||||
}
|
||||
|
||||
// Get available models for all providers or specific provider type
|
||||
if (method === 'GET' && pathParam === '/api/provider-models') {
|
||||
const allModels = getAllProviderModels();
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(allModels));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get available models for a specific provider type
|
||||
const providerModelsMatch = pathParam.match(/^\/api\/provider-models\/([^\/]+)$/);
|
||||
if (method === 'GET' && providerModelsMatch) {
|
||||
const providerType = decodeURIComponent(providerModelsMatch[1]);
|
||||
const models = getProviderModels(providerType);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
providerType,
|
||||
models
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add new provider configuration
|
||||
if (method === 'POST' && pathParam === '/api/providers') {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -61,6 +61,36 @@ function showProviderManagerModal(data) {
|
|||
|
||||
// 添加模态框事件监听
|
||||
addModalEventListeners(modal);
|
||||
|
||||
// 先获取该提供商类型的模型列表(只调用一次API)
|
||||
loadModelsForProviderType(providerType, providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为提供商类型加载模型列表(优化:只调用一次API)
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @param {Array} providers - 提供商列表
|
||||
*/
|
||||
async function loadModelsForProviderType(providerType, providers) {
|
||||
try {
|
||||
// 只调用一次API获取模型列表
|
||||
const response = await window.apiClient.get(`/provider-models/${encodeURIComponent(providerType)}`);
|
||||
const models = response.models || [];
|
||||
|
||||
// 为每个提供商渲染模型选择器
|
||||
providers.forEach(provider => {
|
||||
renderNotSupportedModelsSelector(provider.uuid, models, provider.notSupportedModels || []);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load models for provider type:', error);
|
||||
// 如果加载失败,为每个提供商显示错误信息
|
||||
providers.forEach(provider => {
|
||||
const container = document.querySelector(`.not-supported-models-container[data-uuid="${provider.uuid}"]`);
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="error-message">加载模型列表失败</div>';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -381,6 +411,23 @@ function renderProviderConfig(provider) {
|
|||
html += '</div>';
|
||||
}
|
||||
|
||||
// 添加 notSupportedModels 配置区域
|
||||
html += '<div class="form-grid full-width">';
|
||||
html += `
|
||||
<div class="config-item not-supported-models-section">
|
||||
<label>
|
||||
<i class="fas fa-ban"></i> 不支持的模型
|
||||
<span class="help-text">选择此提供商不支持的模型,系统会自动排除这些模型</span>
|
||||
</label>
|
||||
<div class="not-supported-models-container" data-uuid="${provider.uuid}">
|
||||
<div class="models-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载模型列表...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
|
|
@ -456,6 +503,15 @@ function editProvider(uuid, event) {
|
|||
select.disabled = false;
|
||||
});
|
||||
|
||||
// 启用模型复选框
|
||||
const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox');
|
||||
modelCheckboxes.forEach(checkbox => {
|
||||
checkbox.disabled = false;
|
||||
});
|
||||
|
||||
// 添加编辑状态类
|
||||
providerDetail.classList.add('editing');
|
||||
|
||||
// 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮
|
||||
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
|
||||
const toggleButton = actionsGroup.querySelector('[onclick*="toggleProviderStatus"]');
|
||||
|
|
@ -501,6 +557,15 @@ function cancelEdit(uuid, event) {
|
|||
}
|
||||
});
|
||||
|
||||
// 禁用模型复选框
|
||||
const modelCheckboxes = providerDetail.querySelectorAll('.model-checkbox');
|
||||
modelCheckboxes.forEach(checkbox => {
|
||||
checkbox.disabled = true;
|
||||
});
|
||||
|
||||
// 移除编辑状态类
|
||||
providerDetail.classList.remove('editing');
|
||||
|
||||
// 禁用文件上传按钮
|
||||
const uploadButtons = providerDetail.querySelectorAll('.upload-btn');
|
||||
uploadButtons.forEach(button => {
|
||||
|
|
@ -563,6 +628,11 @@ async function saveProvider(uuid, event) {
|
|||
providerConfig[key] = value;
|
||||
});
|
||||
|
||||
// 收集不支持的模型列表
|
||||
const modelCheckboxes = providerDetail.querySelectorAll(`.model-checkbox[data-uuid="${uuid}"]:checked`);
|
||||
const notSupportedModels = Array.from(modelCheckboxes).map(checkbox => checkbox.value);
|
||||
providerConfig.notSupportedModels = notSupportedModels;
|
||||
|
||||
try {
|
||||
await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig });
|
||||
await window.apiClient.post('/reload-config');
|
||||
|
|
@ -630,6 +700,9 @@ async function refreshProviderConfig(providerType) {
|
|||
if (providerList) {
|
||||
providerList.innerHTML = renderProviderList(data.providers);
|
||||
}
|
||||
|
||||
// 重新加载模型列表
|
||||
loadModelsForProviderType(providerType, data.providers);
|
||||
}
|
||||
|
||||
// 同时更新主界面的提供商统计数据
|
||||
|
|
@ -923,6 +996,42 @@ async function toggleProviderStatus(uuid, event) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
|
||||
* @param {string} uuid - 提供商UUID
|
||||
* @param {Array} models - 模型列表
|
||||
* @param {Array} notSupportedModels - 当前不支持的模型列表
|
||||
*/
|
||||
function renderNotSupportedModelsSelector(uuid, models, notSupportedModels = []) {
|
||||
const container = document.querySelector(`.not-supported-models-container[data-uuid="${uuid}"]`);
|
||||
if (!container) return;
|
||||
|
||||
if (models.length === 0) {
|
||||
container.innerHTML = '<div class="no-models">该提供商类型暂无可用模型列表</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// 渲染模型复选框列表
|
||||
let html = '<div class="models-checkbox-grid">';
|
||||
models.forEach(model => {
|
||||
const isChecked = notSupportedModels.includes(model);
|
||||
html += `
|
||||
<label class="model-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="model-checkbox"
|
||||
value="${model}"
|
||||
data-uuid="${uuid}"
|
||||
${isChecked ? 'checked' : ''}
|
||||
disabled>
|
||||
<span class="model-name">${model}</span>
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 导出所有函数,并挂载到window对象供HTML调用
|
||||
export {
|
||||
showProviderManagerModal,
|
||||
|
|
@ -935,7 +1044,9 @@ export {
|
|||
refreshProviderConfig,
|
||||
showAddProviderForm,
|
||||
addProvider,
|
||||
toggleProviderStatus
|
||||
toggleProviderStatus,
|
||||
loadModelsForProviderType,
|
||||
renderNotSupportedModelsSelector
|
||||
};
|
||||
|
||||
// 将函数挂载到window对象
|
||||
|
|
|
|||
|
|
@ -2985,3 +2985,115 @@ input:checked + .toggle-slider:before {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 不支持的模型选择器样式 */
|
||||
.not-supported-models-section {
|
||||
grid-column: 1 / -1;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.not-supported-models-section label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.not-supported-models-section .help-text {
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: #6c757d;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.not-supported-models-container {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.models-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
color: #6c757d;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.models-checkbox-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.model-checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: white;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.model-checkbox-label:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.model-checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.model-checkbox-label input[type="checkbox"]:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.model-checkbox-label .model-name {
|
||||
font-size: 13px;
|
||||
color: #495057;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.model-checkbox-label input[type="checkbox"]:checked + .model-name {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-models,
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #6c757d;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* 编辑模式下的样式 */
|
||||
.provider-item-detail.editing .model-checkbox-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.provider-item-detail.editing .model-checkbox-label input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.provider-item-detail.editing .model-checkbox-label input[type="checkbox"]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 全宽配置项 */
|
||||
.form-grid.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue