mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +08:00
feat: Unified Embedding Pool with auto-discovery
Architecture refactoring for multi-provider rotation: Backend: - Add EmbeddingPoolConfig type with autoDiscover support - Implement discoverProvidersForModel() for auto-aggregation - Add GET/PUT /api/litellm-api/embedding-pool endpoints - Add GET /api/litellm-api/embedding-pool/discover/:model preview - Convert ccw-litellm status check to async with 5-min cache - Maintain backward compatibility with legacy rotation config Frontend: - Add "Embedding Pool" tab in API Settings - Auto-discover providers when target model selected - Show provider/key count with include/exclude controls - Increase sidebar width (280px → 320px) - Add sync result feedback on save Other: - Remove worker count limits (was max=32) - Add i18n translations (EN/CN) - Update .gitignore for .mcp.json 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -958,8 +958,8 @@ select.cli-input {
|
||||
|
||||
/* Left Sidebar */
|
||||
.api-settings-sidebar {
|
||||
width: 280px;
|
||||
min-width: 240px;
|
||||
width: 320px;
|
||||
min-width: 280px;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -19,6 +19,7 @@ const i18n = {
|
||||
'common.delete': 'Delete',
|
||||
'common.cancel': 'Cancel',
|
||||
'common.save': 'Save',
|
||||
'common.include': 'Include',
|
||||
'common.close': 'Close',
|
||||
'common.loading': 'Loading...',
|
||||
'common.error': 'Error',
|
||||
@@ -28,6 +29,8 @@ const i18n = {
|
||||
'common.retry': 'Retry',
|
||||
'common.refresh': 'Refresh',
|
||||
'common.minutes': 'minutes',
|
||||
'common.enabled': 'Enabled',
|
||||
'common.disabled': 'Disabled',
|
||||
|
||||
// Header
|
||||
'header.project': 'Project:',
|
||||
@@ -267,7 +270,7 @@ const i18n = {
|
||||
'codexlens.embeddingModel': 'Embedding Model',
|
||||
'codexlens.modelHint': 'Select embedding model for vector search (models with ✓ are installed)',
|
||||
'codexlens.concurrency': 'API Concurrency',
|
||||
'codexlens.concurrencyHint': 'Number of parallel API calls (1-32). Higher values speed up indexing but may hit rate limits.',
|
||||
'codexlens.concurrencyHint': 'Number of parallel API calls. Higher values speed up indexing but may hit rate limits.',
|
||||
'codexlens.concurrencyCustom': 'Custom',
|
||||
'codexlens.rotation': 'Multi-Provider Rotation',
|
||||
'codexlens.rotationDesc': 'Aggregate multiple API providers and keys for parallel embedding generation',
|
||||
@@ -289,6 +292,8 @@ const i18n = {
|
||||
'codexlens.selectKeys': 'Select Keys',
|
||||
'codexlens.configureRotation': 'Configure Rotation',
|
||||
'codexlens.rotationSaved': 'Rotation config saved successfully',
|
||||
'codexlens.endpointsSynced': 'endpoints synced to CodexLens',
|
||||
'codexlens.syncFailed': 'Sync failed',
|
||||
'codexlens.rotationDeleted': 'Rotation config deleted',
|
||||
'codexlens.totalEndpoints': 'Total Endpoints',
|
||||
'codexlens.fullIndex': 'Full',
|
||||
@@ -312,6 +317,9 @@ const i18n = {
|
||||
'codexlens.runSearch': 'Run Search',
|
||||
'codexlens.results': 'Results',
|
||||
'codexlens.resultsCount': 'results',
|
||||
'codexlens.resultLimit': 'Limit',
|
||||
'codexlens.contentLength': 'Content Length',
|
||||
'codexlens.extraFiles': 'Extra Files',
|
||||
'codexlens.saveConfig': 'Save Configuration',
|
||||
'codexlens.searching': 'Searching...',
|
||||
'codexlens.searchCompleted': 'Search completed',
|
||||
@@ -1470,6 +1478,20 @@ const i18n = {
|
||||
'apiSettings.endpointDeleted': 'Endpoint deleted successfully',
|
||||
'apiSettings.cacheCleared': 'Cache cleared successfully',
|
||||
'apiSettings.cacheSettingsUpdated': 'Cache settings updated',
|
||||
'apiSettings.embeddingPool': 'Embedding Pool',
|
||||
'apiSettings.embeddingPoolDesc': 'Auto-rotate between providers with same model',
|
||||
'apiSettings.targetModel': 'Target Model',
|
||||
'apiSettings.discoveredProviders': 'Discovered Providers',
|
||||
'apiSettings.autoDiscover': 'Auto-discover providers',
|
||||
'apiSettings.excludeProvider': 'Exclude',
|
||||
'apiSettings.defaultCooldown': 'Cooldown (seconds)',
|
||||
'apiSettings.defaultConcurrent': 'Concurrent per key',
|
||||
'apiSettings.poolEnabled': 'Enable Embedding Pool',
|
||||
'apiSettings.noProvidersFound': 'No providers found for this model',
|
||||
'apiSettings.poolSaved': 'Embedding pool config saved',
|
||||
'apiSettings.strategy': 'Strategy',
|
||||
'apiSettings.providerKeys': 'keys',
|
||||
'apiSettings.selectTargetModel': 'Select target model',
|
||||
'apiSettings.confirmDeleteProvider': 'Are you sure you want to delete this provider?',
|
||||
'apiSettings.confirmDeleteEndpoint': 'Are you sure you want to delete this endpoint?',
|
||||
'apiSettings.confirmClearCache': 'Are you sure you want to clear the cache?',
|
||||
@@ -1703,6 +1725,7 @@ const i18n = {
|
||||
'common.delete': '删除',
|
||||
'common.cancel': '取消',
|
||||
'common.save': '保存',
|
||||
'common.include': '包含',
|
||||
'common.close': '关闭',
|
||||
'common.loading': '加载中...',
|
||||
'common.error': '错误',
|
||||
@@ -1712,6 +1735,8 @@ const i18n = {
|
||||
'common.retry': '重试',
|
||||
'common.refresh': '刷新',
|
||||
'common.minutes': '分钟',
|
||||
'common.enabled': '已启用',
|
||||
'common.disabled': '已禁用',
|
||||
|
||||
// Header
|
||||
'header.project': '项目:',
|
||||
@@ -1951,7 +1976,7 @@ const i18n = {
|
||||
'codexlens.embeddingModel': '嵌入模型',
|
||||
'codexlens.modelHint': '选择向量搜索的嵌入模型(带 ✓ 的已安装)',
|
||||
'codexlens.concurrency': 'API 并发数',
|
||||
'codexlens.concurrencyHint': '并行 API 调用数量(1-32)。较高的值可加速索引但可能触发速率限制。',
|
||||
'codexlens.concurrencyHint': '并行 API 调用数量。较高的值可加速索引但可能触发速率限制。',
|
||||
'codexlens.concurrencyCustom': '自定义',
|
||||
'codexlens.rotation': '多供应商轮训',
|
||||
'codexlens.rotationDesc': '聚合多个 API 供应商和密钥进行并行嵌入生成',
|
||||
@@ -1973,6 +1998,8 @@ const i18n = {
|
||||
'codexlens.selectKeys': '选择密钥',
|
||||
'codexlens.configureRotation': '配置轮训',
|
||||
'codexlens.rotationSaved': '轮训配置保存成功',
|
||||
'codexlens.endpointsSynced': '个端点已同步到 CodexLens',
|
||||
'codexlens.syncFailed': '同步失败',
|
||||
'codexlens.rotationDeleted': '轮训配置已删除',
|
||||
'codexlens.totalEndpoints': '总端点数',
|
||||
'codexlens.fullIndex': '全部',
|
||||
@@ -1996,6 +2023,9 @@ const i18n = {
|
||||
'codexlens.runSearch': '运行搜索',
|
||||
'codexlens.results': '结果',
|
||||
'codexlens.resultsCount': '个结果',
|
||||
'codexlens.resultLimit': '数量限制',
|
||||
'codexlens.contentLength': '内容长度',
|
||||
'codexlens.extraFiles': '额外文件',
|
||||
'codexlens.saveConfig': '保存配置',
|
||||
'codexlens.searching': '搜索中...',
|
||||
'codexlens.searchCompleted': '搜索完成',
|
||||
@@ -3163,6 +3193,20 @@ const i18n = {
|
||||
'apiSettings.endpointDeleted': '端点删除成功',
|
||||
'apiSettings.cacheCleared': '缓存清除成功',
|
||||
'apiSettings.cacheSettingsUpdated': '缓存设置已更新',
|
||||
'apiSettings.embeddingPool': '高可用嵌入',
|
||||
'apiSettings.embeddingPoolDesc': '自动轮训相同模型的供应商',
|
||||
'apiSettings.targetModel': '目标模型',
|
||||
'apiSettings.discoveredProviders': '发现的供应商',
|
||||
'apiSettings.autoDiscover': '自动发现供应商',
|
||||
'apiSettings.excludeProvider': '排除',
|
||||
'apiSettings.defaultCooldown': '冷却时间(秒)',
|
||||
'apiSettings.defaultConcurrent': '每密钥并发数',
|
||||
'apiSettings.poolEnabled': '启用嵌入池',
|
||||
'apiSettings.noProvidersFound': '未找到提供此模型的供应商',
|
||||
'apiSettings.poolSaved': '嵌入池配置已保存',
|
||||
'apiSettings.strategy': '策略',
|
||||
'apiSettings.providerKeys': '密钥',
|
||||
'apiSettings.selectTargetModel': '选择目标模型',
|
||||
'apiSettings.confirmDeleteProvider': '确定要删除此提供商吗?',
|
||||
'apiSettings.confirmDeleteEndpoint': '确定要删除此端点吗?',
|
||||
'apiSettings.confirmClearCache': '确定要清除缓存吗?',
|
||||
|
||||
@@ -11,7 +11,12 @@ let selectedProviderId = null;
|
||||
let providerSearchQuery = '';
|
||||
let activeModelTab = 'llm';
|
||||
let expandedModelGroups = new Set();
|
||||
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache'
|
||||
let activeSidebarTab = 'providers'; // 'providers' | 'endpoints' | 'cache' | 'embedding-pool'
|
||||
|
||||
// Embedding Pool state
|
||||
let embeddingPoolConfig = null;
|
||||
let embeddingPoolAvailableModels = [];
|
||||
let embeddingPoolDiscoveredProviders = [];
|
||||
|
||||
// ========== Data Loading ==========
|
||||
|
||||
@@ -61,6 +66,112 @@ async function loadCacheStats() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load embedding pool configuration and available models
|
||||
*/
|
||||
async function loadEmbeddingPoolConfig() {
|
||||
try {
|
||||
const response = await fetch('/api/litellm-api/embedding-pool');
|
||||
if (!response.ok) throw new Error('Failed to load embedding pool config');
|
||||
const data = await response.json();
|
||||
embeddingPoolConfig = data.poolConfig;
|
||||
embeddingPoolAvailableModels = data.availableModels || [];
|
||||
|
||||
// If pool is enabled and has a target model, discover providers
|
||||
if (embeddingPoolConfig && embeddingPoolConfig.enabled && embeddingPoolConfig.targetModel) {
|
||||
await discoverProvidersForTargetModel(embeddingPoolConfig.targetModel);
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to load embedding pool config:', err);
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover providers for a specific target model
|
||||
*/
|
||||
async function discoverProvidersForTargetModel(targetModel) {
|
||||
try {
|
||||
const response = await fetch('/api/litellm-api/embedding-pool/discover/' + encodeURIComponent(targetModel));
|
||||
if (!response.ok) throw new Error('Failed to discover providers');
|
||||
const data = await response.json();
|
||||
embeddingPoolDiscoveredProviders = data.discovered || [];
|
||||
return data;
|
||||
} catch (err) {
|
||||
console.error('Failed to discover providers:', err);
|
||||
embeddingPoolDiscoveredProviders = [];
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save embedding pool configuration
|
||||
*/
|
||||
async function saveEmbeddingPoolConfig() {
|
||||
try {
|
||||
const enabled = document.getElementById('embedding-pool-enabled')?.checked || false;
|
||||
const targetModel = document.getElementById('embedding-pool-target-model')?.value || '';
|
||||
const strategy = document.getElementById('embedding-pool-strategy')?.value || 'round_robin';
|
||||
const defaultCooldown = parseInt(document.getElementById('embedding-pool-cooldown')?.value || '60');
|
||||
const defaultMaxConcurrentPerKey = parseInt(document.getElementById('embedding-pool-concurrent')?.value || '4');
|
||||
|
||||
const poolConfig = enabled ? {
|
||||
enabled: true,
|
||||
targetModel: targetModel,
|
||||
strategy: strategy,
|
||||
autoDiscover: true,
|
||||
excludedProviderIds: embeddingPoolConfig?.excludedProviderIds || [],
|
||||
defaultCooldown: defaultCooldown,
|
||||
defaultMaxConcurrentPerKey: defaultMaxConcurrentPerKey
|
||||
} : null;
|
||||
|
||||
const response = await fetch('/api/litellm-api/embedding-pool', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(poolConfig)
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to save embedding pool config');
|
||||
|
||||
const result = await response.json();
|
||||
embeddingPoolConfig = result.poolConfig;
|
||||
|
||||
const syncCount = result.syncResult?.syncedEndpoints?.length || 0;
|
||||
showRefreshToast(t('apiSettings.poolSaved') + (syncCount > 0 ? ' (' + syncCount + ' endpoints synced)' : ''), 'success');
|
||||
|
||||
// Reload the embedding pool section
|
||||
await renderEmbeddingPoolMainPanel();
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to save embedding pool config:', err);
|
||||
showRefreshToast(t('common.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle provider exclusion in embedding pool
|
||||
*/
|
||||
async function toggleProviderExclusion(providerId) {
|
||||
if (!embeddingPoolConfig) return;
|
||||
|
||||
const excludedIds = embeddingPoolConfig.excludedProviderIds || [];
|
||||
const index = excludedIds.indexOf(providerId);
|
||||
|
||||
if (index > -1) {
|
||||
excludedIds.splice(index, 1);
|
||||
} else {
|
||||
excludedIds.push(providerId);
|
||||
}
|
||||
|
||||
embeddingPoolConfig.excludedProviderIds = excludedIds;
|
||||
|
||||
// Re-render the discovered providers section
|
||||
renderDiscoveredProviders();
|
||||
}
|
||||
|
||||
// ========== Provider Management ==========
|
||||
|
||||
/**
|
||||
@@ -825,6 +936,9 @@ async function renderApiSettings() {
|
||||
'<button class="sidebar-tab' + (activeSidebarTab === 'endpoints' ? ' active' : '') + '" onclick="switchSidebarTab(\'endpoints\')">' +
|
||||
'<i data-lucide="link"></i> ' + t('apiSettings.endpoints') +
|
||||
'</button>' +
|
||||
'<button class="sidebar-tab' + (activeSidebarTab === 'embedding-pool' ? ' active' : '') + '" onclick="switchSidebarTab(\'embedding-pool\')">' +
|
||||
'<i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') +
|
||||
'</button>' +
|
||||
'<button class="sidebar-tab' + (activeSidebarTab === 'cache' ? ' active' : '') + '" onclick="switchSidebarTab(\'cache\')">' +
|
||||
'<i data-lucide="database"></i> ' + t('apiSettings.cache') +
|
||||
'</button>' +
|
||||
@@ -833,7 +947,7 @@ async function renderApiSettings() {
|
||||
// Build sidebar content based on active tab
|
||||
var sidebarContentHtml = '';
|
||||
var addButtonHtml = '';
|
||||
|
||||
|
||||
if (activeSidebarTab === 'providers') {
|
||||
sidebarContentHtml = '<div class="provider-search">' +
|
||||
'<i data-lucide="search" class="search-icon"></i>' +
|
||||
@@ -848,6 +962,10 @@ async function renderApiSettings() {
|
||||
addButtonHtml = '<button class="btn btn-primary btn-full" onclick="showAddEndpointModal()">' +
|
||||
'<i data-lucide="plus"></i> ' + t('apiSettings.addEndpoint') +
|
||||
'</button>';
|
||||
} else if (activeSidebarTab === 'embedding-pool') {
|
||||
sidebarContentHtml = '<div class="embedding-pool-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
|
||||
'<p>' + t('apiSettings.embeddingPoolDesc') + '</p>' +
|
||||
'</div>';
|
||||
} else if (activeSidebarTab === 'cache') {
|
||||
sidebarContentHtml = '<div class="cache-sidebar-info" style="padding: 1rem; color: var(--text-secondary); font-size: 0.875rem;">' +
|
||||
'<p>' + t('apiSettings.cacheTabHint') + '</p>' +
|
||||
@@ -887,6 +1005,8 @@ async function renderApiSettings() {
|
||||
} else if (activeSidebarTab === 'endpoints') {
|
||||
renderEndpointsList();
|
||||
renderEndpointsMainPanel();
|
||||
} else if (activeSidebarTab === 'embedding-pool') {
|
||||
renderEmbeddingPoolMainPanel();
|
||||
} else if (activeSidebarTab === 'cache') {
|
||||
renderCacheMainPanel();
|
||||
}
|
||||
@@ -2367,6 +2487,174 @@ function generateKeyId() {
|
||||
return 'key-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// ========== Embedding Pool Management ==========
|
||||
|
||||
/**
|
||||
* Render embedding pool main panel
|
||||
*/
|
||||
async function renderEmbeddingPoolMainPanel() {
|
||||
var container = document.getElementById('provider-detail-panel');
|
||||
if (!container) return;
|
||||
|
||||
// Load embedding pool config if not already loaded
|
||||
if (!embeddingPoolConfig) {
|
||||
await loadEmbeddingPoolConfig();
|
||||
}
|
||||
|
||||
const enabled = embeddingPoolConfig?.enabled || false;
|
||||
const targetModel = embeddingPoolConfig?.targetModel || '';
|
||||
const strategy = embeddingPoolConfig?.strategy || 'round_robin';
|
||||
const defaultCooldown = embeddingPoolConfig?.defaultCooldown || 60;
|
||||
const defaultMaxConcurrentPerKey = embeddingPoolConfig?.defaultMaxConcurrentPerKey || 4;
|
||||
|
||||
// Build model dropdown options
|
||||
let modelOptionsHtml = '<option value="">' + t('apiSettings.selectTargetModel') + '</option>';
|
||||
embeddingPoolAvailableModels.forEach(function(model) {
|
||||
const providerCount = model.providers.length;
|
||||
const selected = model.modelId === targetModel ? ' selected' : '';
|
||||
modelOptionsHtml += '<option value="' + model.modelId + '"' + selected + '>' +
|
||||
model.modelName + ' (' + providerCount + ' providers)' +
|
||||
'</option>';
|
||||
});
|
||||
|
||||
var html = '<div class="embedding-pool-main-panel">' +
|
||||
'<div class="panel-header">' +
|
||||
'<h2><i data-lucide="repeat"></i> ' + t('apiSettings.embeddingPool') + '</h2>' +
|
||||
'<p class="panel-subtitle">' + t('apiSettings.embeddingPoolDesc') + '</p>' +
|
||||
'</div>' +
|
||||
|
||||
// Enable/Disable Toggle
|
||||
'<div class="settings-section">' +
|
||||
'<div class="section-header">' +
|
||||
'<h3>' + t('apiSettings.poolEnabled') + '</h3>' +
|
||||
'<label class="toggle-switch">' +
|
||||
'<input type="checkbox" id="embedding-pool-enabled" ' + (enabled ? 'checked' : '') + ' onchange="onEmbeddingPoolEnabledChange(this.checked)" />' +
|
||||
'<span class="toggle-track"><span class="toggle-thumb"></span></span>' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
// Configuration Form
|
||||
'<div class="settings-section" id="embedding-pool-config" style="' + (enabled ? '' : 'display: none;') + '">' +
|
||||
'<div class="form-group">' +
|
||||
'<label for="embedding-pool-target-model">' + t('apiSettings.targetModel') + '</label>' +
|
||||
'<select id="embedding-pool-target-model" class="cli-input" onchange="onTargetModelChange(this.value)">' +
|
||||
modelOptionsHtml +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label for="embedding-pool-strategy">' + t('apiSettings.strategy') + '</label>' +
|
||||
'<select id="embedding-pool-strategy" class="cli-input">' +
|
||||
'<option value="round_robin"' + (strategy === 'round_robin' ? ' selected' : '') + '>Round Robin</option>' +
|
||||
'<option value="latency_aware"' + (strategy === 'latency_aware' ? ' selected' : '') + '>Latency Aware</option>' +
|
||||
'<option value="weighted_random"' + (strategy === 'weighted_random' ? ' selected' : '') + '>Weighted Random</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label for="embedding-pool-cooldown">' + t('apiSettings.defaultCooldown') + '</label>' +
|
||||
'<input type="number" id="embedding-pool-cooldown" class="cli-input" value="' + defaultCooldown + '" min="1" />' +
|
||||
'</div>' +
|
||||
|
||||
'<div class="form-group">' +
|
||||
'<label for="embedding-pool-concurrent">' + t('apiSettings.defaultConcurrent') + '</label>' +
|
||||
'<input type="number" id="embedding-pool-concurrent" class="cli-input" value="' + defaultMaxConcurrentPerKey + '" min="1" />' +
|
||||
'</div>' +
|
||||
|
||||
// Discovered Providers Section
|
||||
'<div id="discovered-providers-section"></div>' +
|
||||
|
||||
'<div class="form-actions">' +
|
||||
'<button class="btn btn-primary" onclick="saveEmbeddingPoolConfig()">' +
|
||||
'<i data-lucide="save"></i> ' + t('common.save') +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
container.innerHTML = html;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
|
||||
// Render discovered providers if we have a target model
|
||||
if (enabled && targetModel) {
|
||||
renderDiscoveredProviders();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle embedding pool enabled/disabled toggle
|
||||
*/
|
||||
function onEmbeddingPoolEnabledChange(enabled) {
|
||||
const configSection = document.getElementById('embedding-pool-config');
|
||||
if (configSection) {
|
||||
configSection.style.display = enabled ? '' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle target model selection change
|
||||
*/
|
||||
async function onTargetModelChange(modelId) {
|
||||
if (!modelId) {
|
||||
embeddingPoolDiscoveredProviders = [];
|
||||
renderDiscoveredProviders();
|
||||
return;
|
||||
}
|
||||
|
||||
// Discover providers for this model
|
||||
await discoverProvidersForTargetModel(modelId);
|
||||
renderDiscoveredProviders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render discovered providers list
|
||||
*/
|
||||
function renderDiscoveredProviders() {
|
||||
const container = document.getElementById('discovered-providers-section');
|
||||
if (!container) return;
|
||||
|
||||
if (embeddingPoolDiscoveredProviders.length === 0) {
|
||||
container.innerHTML = '<div class="info-message" style="margin-top: 1rem;">' +
|
||||
'<i data-lucide="info"></i> ' + t('apiSettings.noProvidersFound') +
|
||||
'</div>';
|
||||
if (window.lucide) lucide.createIcons();
|
||||
return;
|
||||
}
|
||||
|
||||
const excludedIds = embeddingPoolConfig?.excludedProviderIds || [];
|
||||
let totalProviders = 0;
|
||||
let totalKeys = 0;
|
||||
|
||||
embeddingPoolDiscoveredProviders.forEach(function(p) {
|
||||
totalProviders++;
|
||||
totalKeys += p.keyCount || 1;
|
||||
});
|
||||
|
||||
let providersHtml = '<div class="discovered-providers-box" style="margin-top: 1rem; padding: 1rem; background: var(--bg-secondary); border-radius: 8px;">' +
|
||||
'<h4>' + t('apiSettings.discoveredProviders') + ' (' + totalProviders + ' providers, ' + totalKeys + ' ' + t('apiSettings.providerKeys') + ')</h4>' +
|
||||
'<div class="providers-list" style="margin-top: 0.75rem;">';
|
||||
|
||||
embeddingPoolDiscoveredProviders.forEach(function(provider) {
|
||||
const isExcluded = excludedIds.indexOf(provider.providerId) > -1;
|
||||
const icon = isExcluded ? 'x-circle' : 'check-circle';
|
||||
const statusClass = isExcluded ? 'text-error' : 'text-success';
|
||||
const keyInfo = provider.keyCount > 1 ? ' (' + provider.keyCount + ' ' + t('apiSettings.providerKeys') + ')' : '';
|
||||
|
||||
providersHtml += '<div class="provider-item" style="display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; border-bottom: 1px solid var(--border-color);">' +
|
||||
'<i data-lucide="' + icon + '" class="' + statusClass + '"></i>' +
|
||||
'<span style="flex: 1;">' + provider.providerName + keyInfo + '</span>' +
|
||||
'<button class="btn btn-sm ' + (isExcluded ? 'btn-secondary' : 'btn-outline') + '" onclick="toggleProviderExclusion(\'' + provider.providerId + '\')">' +
|
||||
(isExcluded ? t('common.include') : t('apiSettings.excludeProvider')) +
|
||||
'</button>' +
|
||||
'</div>';
|
||||
});
|
||||
|
||||
providersHtml += '</div></div>';
|
||||
container.innerHTML = providersHtml;
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render API keys section
|
||||
*/
|
||||
|
||||
@@ -271,6 +271,9 @@ function initCodexLensConfigEvents(currentConfig) {
|
||||
var searchType = document.getElementById('searchTypeSelect').value;
|
||||
var searchMode = document.getElementById('searchModeSelect').value;
|
||||
var query = document.getElementById('searchQueryInput').value.trim();
|
||||
var searchLimit = document.getElementById('searchLimitInput')?.value || '5';
|
||||
var contentLength = document.getElementById('contentLengthInput')?.value || '200';
|
||||
var extraFiles = document.getElementById('extraFilesInput')?.value || '10';
|
||||
var resultsDiv = document.getElementById('searchResults');
|
||||
var resultCount = document.getElementById('searchResultCount');
|
||||
var resultContent = document.getElementById('searchResultContent');
|
||||
@@ -286,7 +289,12 @@ function initCodexLensConfigEvents(currentConfig) {
|
||||
|
||||
try {
|
||||
var endpoint = '/api/codexlens/' + searchType;
|
||||
var params = new URLSearchParams({ query: query, limit: '20' });
|
||||
var params = new URLSearchParams({
|
||||
query: query,
|
||||
limit: searchLimit,
|
||||
max_content_length: contentLength,
|
||||
extra_files_count: extraFiles
|
||||
});
|
||||
// Add mode parameter for search and search_files (not for symbol search)
|
||||
if (searchType === 'search' || searchType === 'search_files') {
|
||||
params.append('mode', searchMode);
|
||||
@@ -2001,7 +2009,7 @@ function buildCodexLensManagerPage(config) {
|
||||
'<div id="concurrencySelector" class="hidden">' +
|
||||
'<label class="block text-sm font-medium mb-1.5">' + t('codexlens.concurrency') + '</label>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<input type="number" id="pageConcurrencyInput" min="1" max="32" value="4" ' +
|
||||
'<input type="number" id="pageConcurrencyInput" min="1" value="4" ' +
|
||||
'class="w-24 px-3 py-2 border border-border rounded-lg bg-background text-sm" ' +
|
||||
'onchange="validateConcurrencyInput(this)" />' +
|
||||
'<span class="text-sm text-muted-foreground">workers</span>' +
|
||||
@@ -2173,6 +2181,20 @@ function buildCodexLensManagerPage(config) {
|
||||
'<option value="vector">' + t('codexlens.vectorMode') + '</option>' +
|
||||
'</select>' +
|
||||
'</div>' +
|
||||
'<div class="flex gap-3 items-center">' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.resultLimit') + '</label>' +
|
||||
'<input type="number" id="searchLimitInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="5" min="1" max="50" />' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.contentLength') + '</label>' +
|
||||
'<input type="number" id="contentLengthInput" class="w-20 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="200" min="50" max="2000" />' +
|
||||
'</div>' +
|
||||
'<div class="flex items-center gap-2">' +
|
||||
'<label class="text-xs text-muted-foreground whitespace-nowrap">' + t('codexlens.extraFiles') + '</label>' +
|
||||
'<input type="number" id="extraFilesInput" class="w-16 px-2 py-1.5 border border-border rounded-lg bg-background text-sm text-center" value="10" min="0" max="50" />' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="flex gap-3">' +
|
||||
'<input type="text" id="searchQueryInput" class="flex-1 px-3 py-2 border border-border rounded-lg bg-background text-sm" placeholder="' + t('codexlens.searchPlaceholder') + '" />' +
|
||||
'<button class="btn-sm btn-primary" id="runSearchBtn"><i data-lucide="search" class="w-3.5 h-3.5"></i> ' + t('codexlens.runSearch') + '</button>' +
|
||||
@@ -2228,14 +2250,12 @@ function buildModelSelectOptionsForPage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate concurrency input value (1-32)
|
||||
* Validate concurrency input value (min 1, no max limit)
|
||||
*/
|
||||
function validateConcurrencyInput(input) {
|
||||
var value = parseInt(input.value, 10);
|
||||
if (isNaN(value) || value < 1) {
|
||||
input.value = 1;
|
||||
} else if (value > 32) {
|
||||
input.value = 32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2338,7 +2358,7 @@ function initCodexLensIndexFromPage(indexType) {
|
||||
var concurrencyInput = document.getElementById('pageConcurrencyInput');
|
||||
var selectedBackend = backendSelect ? backendSelect.value : 'fastembed';
|
||||
var selectedModel = modelSelect ? modelSelect.value : 'code';
|
||||
var selectedConcurrency = concurrencyInput ? Math.min(32, Math.max(1, parseInt(concurrencyInput.value, 10) || 4)) : 4;
|
||||
var selectedConcurrency = concurrencyInput ? Math.max(1, parseInt(concurrencyInput.value, 10) || 4) : 4;
|
||||
|
||||
// For FTS-only index, model is not needed
|
||||
if (indexType === 'normal') {
|
||||
@@ -2879,7 +2899,16 @@ async function saveRotationConfig() {
|
||||
var result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showRefreshToast(t('codexlens.rotationSaved'), 'success');
|
||||
// Show sync result in toast
|
||||
var syncMsg = '';
|
||||
if (result.syncResult) {
|
||||
if (result.syncResult.success) {
|
||||
syncMsg = ' (' + result.syncResult.endpointCount + ' ' + t('codexlens.endpointsSynced') + ')';
|
||||
} else {
|
||||
syncMsg = ' (' + t('codexlens.syncFailed') + ': ' + result.syncResult.message + ')';
|
||||
}
|
||||
}
|
||||
showRefreshToast(t('codexlens.rotationSaved') + syncMsg, 'success');
|
||||
window.rotationConfig = rotationConfig;
|
||||
updateRotationStatusDisplay(rotationConfig);
|
||||
closeRotationModal();
|
||||
|
||||
Reference in New Issue
Block a user