mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat: implement OpenAI provider connection testing with UI integration, status updates, and internationalization support
This commit is contained in:
24
i18n.js
24
i18n.js
@@ -238,6 +238,15 @@ const i18n = {
|
|||||||
'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?',
|
'ai_providers.openai_delete_confirm': '确定要删除这个OpenAI提供商吗?',
|
||||||
'ai_providers.openai_keys_count': '密钥数量',
|
'ai_providers.openai_keys_count': '密钥数量',
|
||||||
'ai_providers.openai_models_count': '模型数量',
|
'ai_providers.openai_models_count': '模型数量',
|
||||||
|
'ai_providers.openai_test_title': '连通性测试',
|
||||||
|
'ai_providers.openai_test_hint': '使用当前配置向 /v1/chat/completions 请求,验证是否可用。',
|
||||||
|
'ai_providers.openai_test_model_placeholder': '选择或输入要测试的模型',
|
||||||
|
'ai_providers.openai_test_action': '发送测试',
|
||||||
|
'ai_providers.openai_test_running': '正在发送测试请求...',
|
||||||
|
'ai_providers.openai_test_success': '测试成功,模型可用。',
|
||||||
|
'ai_providers.openai_test_failed': '测试失败',
|
||||||
|
'ai_providers.openai_test_select_placeholder': '从当前模型列表选择',
|
||||||
|
'ai_providers.openai_test_select_empty': '当前未配置模型,可直接输入',
|
||||||
|
|
||||||
|
|
||||||
// 认证文件管理
|
// 认证文件管理
|
||||||
@@ -619,6 +628,9 @@ const i18n = {
|
|||||||
'notification.openai_provider_updated': 'OpenAI提供商更新成功',
|
'notification.openai_provider_updated': 'OpenAI提供商更新成功',
|
||||||
'notification.openai_provider_deleted': 'OpenAI提供商删除成功',
|
'notification.openai_provider_deleted': 'OpenAI提供商删除成功',
|
||||||
'notification.openai_model_name_required': '请填写模型名称',
|
'notification.openai_model_name_required': '请填写模型名称',
|
||||||
|
'notification.openai_test_url_required': '请先填写有效的 Base URL 以进行测试',
|
||||||
|
'notification.openai_test_key_required': '请至少填写一个 API 密钥以进行测试',
|
||||||
|
'notification.openai_test_model_required': '请选择或输入要测试的模型',
|
||||||
'notification.data_refreshed': '数据刷新成功',
|
'notification.data_refreshed': '数据刷新成功',
|
||||||
'notification.connection_required': '请先建立连接',
|
'notification.connection_required': '请先建立连接',
|
||||||
'notification.refresh_failed': '刷新失败',
|
'notification.refresh_failed': '刷新失败',
|
||||||
@@ -893,6 +905,15 @@ const i18n = {
|
|||||||
'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?',
|
'ai_providers.openai_delete_confirm': 'Are you sure you want to delete this OpenAI provider?',
|
||||||
'ai_providers.openai_keys_count': 'Keys Count',
|
'ai_providers.openai_keys_count': 'Keys Count',
|
||||||
'ai_providers.openai_models_count': 'Models Count',
|
'ai_providers.openai_models_count': 'Models Count',
|
||||||
|
'ai_providers.openai_test_title': 'Connection Test',
|
||||||
|
'ai_providers.openai_test_hint': 'Send a /v1/chat/completions request with the current settings to verify availability.',
|
||||||
|
'ai_providers.openai_test_model_placeholder': 'Model to test',
|
||||||
|
'ai_providers.openai_test_action': 'Run Test',
|
||||||
|
'ai_providers.openai_test_running': 'Sending test request...',
|
||||||
|
'ai_providers.openai_test_success': 'Test succeeded. The model responded.',
|
||||||
|
'ai_providers.openai_test_failed': 'Test failed',
|
||||||
|
'ai_providers.openai_test_select_placeholder': 'Choose from current models',
|
||||||
|
'ai_providers.openai_test_select_empty': 'No models configured, enter manually',
|
||||||
|
|
||||||
|
|
||||||
// Auth files management
|
// Auth files management
|
||||||
@@ -1273,6 +1294,9 @@ const i18n = {
|
|||||||
'notification.openai_provider_updated': 'OpenAI provider updated successfully',
|
'notification.openai_provider_updated': 'OpenAI provider updated successfully',
|
||||||
'notification.openai_provider_deleted': 'OpenAI provider deleted successfully',
|
'notification.openai_provider_deleted': 'OpenAI provider deleted successfully',
|
||||||
'notification.openai_model_name_required': 'Model name is required',
|
'notification.openai_model_name_required': 'Model name is required',
|
||||||
|
'notification.openai_test_url_required': 'Please provide a valid Base URL before testing',
|
||||||
|
'notification.openai_test_key_required': 'Please add at least one API key before testing',
|
||||||
|
'notification.openai_test_model_required': 'Please select or enter a model to test',
|
||||||
'notification.data_refreshed': 'Data refreshed successfully',
|
'notification.data_refreshed': 'Data refreshed successfully',
|
||||||
'notification.connection_required': 'Please establish connection first',
|
'notification.connection_required': 'Please establish connection first',
|
||||||
'notification.refresh_failed': 'Refresh failed',
|
'notification.refresh_failed': 'Refresh failed',
|
||||||
|
|||||||
@@ -23,6 +23,19 @@ const buildModelEndpoint = (baseUrl) => {
|
|||||||
return `${trimmed}/v1/models`;
|
return `${trimmed}/v1/models`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildChatCompletionsEndpoint = (baseUrl) => {
|
||||||
|
if (!baseUrl) return '';
|
||||||
|
const trimmed = String(baseUrl).trim().replace(/\/+$/g, '');
|
||||||
|
if (!trimmed) return '';
|
||||||
|
if (trimmed.endsWith('/chat/completions')) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
if (trimmed.endsWith('/v1')) {
|
||||||
|
return `${trimmed}/chat/completions`;
|
||||||
|
}
|
||||||
|
return `${trimmed}/v1/chat/completions`;
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeExcludedModels = (input) => {
|
const normalizeExcludedModels = (input) => {
|
||||||
const rawList = Array.isArray(input)
|
const rawList = Array.isArray(input)
|
||||||
? input
|
? input
|
||||||
@@ -1336,6 +1349,9 @@ export function applyOpenAIModelDiscoverySelection() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.populateModelFields(context.modelWrapperId, Array.from(mergedMap.values()));
|
this.populateModelFields(context.modelWrapperId, Array.from(mergedMap.values()));
|
||||||
|
if (context.mode === 'edit' && typeof this.populateOpenAITestModelOptions === 'function') {
|
||||||
|
this.populateOpenAITestModelOptions(Array.from(mergedMap.values()), { preserveInput: true });
|
||||||
|
}
|
||||||
this.closeOpenAIModelDiscovery();
|
this.closeOpenAIModelDiscovery();
|
||||||
|
|
||||||
if (addedCount > 0) {
|
if (addedCount > 0) {
|
||||||
@@ -1353,6 +1369,180 @@ export function closeOpenAIModelDiscovery() {
|
|||||||
this.openAIModelDiscoveryContext = null;
|
this.openAIModelDiscoveryContext = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function populateOpenAITestModelOptions(models = [], { preserveInput = true } = {}) {
|
||||||
|
const select = document.getElementById('openai-test-model-select');
|
||||||
|
const input = document.getElementById('openai-test-model-input');
|
||||||
|
if (!select) return;
|
||||||
|
|
||||||
|
const names = [];
|
||||||
|
const seen = new Set();
|
||||||
|
(Array.isArray(models) ? models : []).forEach(model => {
|
||||||
|
const name = model?.name ? String(model.name).trim() : '';
|
||||||
|
if (!name || seen.has(name)) return;
|
||||||
|
seen.add(name);
|
||||||
|
names.push(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!names.length) {
|
||||||
|
select.disabled = true;
|
||||||
|
select.innerHTML = `<option value="">${i18n.t('ai_providers.openai_test_select_empty')}</option>`;
|
||||||
|
if (input && !preserveInput) {
|
||||||
|
input.value = '';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
select.disabled = false;
|
||||||
|
const placeholder = `<option value="">${i18n.t('ai_providers.openai_test_select_placeholder')}</option>`;
|
||||||
|
const options = names.map(name => `<option value="${this.escapeHtml(name)}">${this.escapeHtml(name)}</option>`).join('');
|
||||||
|
select.innerHTML = `${placeholder}${options}`;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
if (!preserveInput || !input.value) {
|
||||||
|
const firstName = names[0];
|
||||||
|
if (firstName) {
|
||||||
|
input.value = firstName;
|
||||||
|
select.value = firstName;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = input.value.trim();
|
||||||
|
if (current && names.includes(current)) {
|
||||||
|
select.value = current;
|
||||||
|
} else {
|
||||||
|
select.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setOpenAITestStatus(message = '', type = 'info') {
|
||||||
|
const statusEl = document.getElementById('openai-test-status');
|
||||||
|
if (!statusEl) return;
|
||||||
|
statusEl.textContent = message || '';
|
||||||
|
statusEl.className = `openai-test-status ${type || ''}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const setOpenAITestButtonState = (state = 'idle') => {
|
||||||
|
const button = document.getElementById('openai-test-button');
|
||||||
|
if (!button) return;
|
||||||
|
button.disabled = state === 'loading';
|
||||||
|
button.classList.remove('openai-test-btn-success', 'openai-test-btn-error');
|
||||||
|
|
||||||
|
switch (state) {
|
||||||
|
case 'loading':
|
||||||
|
button.innerHTML = `<i class="fas fa-spinner fa-spin"></i>`;
|
||||||
|
break;
|
||||||
|
case 'success':
|
||||||
|
button.classList.add('openai-test-btn-success');
|
||||||
|
button.innerHTML = `<i class="fas fa-check"></i>`;
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
button.classList.add('openai-test-btn-error');
|
||||||
|
button.innerHTML = `<i class="fas fa-times"></i>`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
button.innerHTML = `<i class="fas fa-stethoscope"></i> ${i18n.t('ai_providers.openai_test_action')}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function testOpenAIProviderConnection() {
|
||||||
|
const baseUrlInput = document.getElementById('edit-provider-url');
|
||||||
|
const baseUrl = baseUrlInput ? baseUrlInput.value.trim() : '';
|
||||||
|
if (!baseUrl) {
|
||||||
|
const message = i18n.t('notification.openai_test_url_required');
|
||||||
|
this.setOpenAITestStatus(message, 'error');
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const endpoint = buildChatCompletionsEndpoint(baseUrl);
|
||||||
|
if (!endpoint) {
|
||||||
|
const message = i18n.t('notification.openai_test_url_required');
|
||||||
|
this.setOpenAITestStatus(message, 'error');
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiKeyEntries = this.collectApiKeyEntryInputs('edit-openai-keys-wrapper');
|
||||||
|
const firstKeyEntry = Array.isArray(apiKeyEntries) ? apiKeyEntries.find(entry => entry && entry['api-key']) : null;
|
||||||
|
if (!firstKeyEntry) {
|
||||||
|
const message = i18n.t('notification.openai_test_key_required');
|
||||||
|
this.setOpenAITestStatus(message, 'error');
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = this.collectModelInputs('edit-provider-models-wrapper');
|
||||||
|
this.populateOpenAITestModelOptions(models);
|
||||||
|
|
||||||
|
const modelInput = document.getElementById('openai-test-model-input');
|
||||||
|
let modelName = modelInput ? modelInput.value.trim() : '';
|
||||||
|
if (!modelName) {
|
||||||
|
const firstModel = Array.isArray(models) ? models.find(model => model && model.name) : null;
|
||||||
|
if (firstModel && firstModel.name) {
|
||||||
|
modelName = firstModel.name;
|
||||||
|
if (modelInput) {
|
||||||
|
modelInput.value = firstModel.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modelName) {
|
||||||
|
const message = i18n.t('notification.openai_test_model_required');
|
||||||
|
this.setOpenAITestStatus(message, 'error');
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const customHeaders = this.collectHeaderInputs('edit-openai-headers-wrapper') || {};
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...customHeaders
|
||||||
|
};
|
||||||
|
if (!headers.Authorization && !headers.authorization) {
|
||||||
|
headers.Authorization = `Bearer ${firstKeyEntry['api-key']}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setOpenAITestStatus('', 'info');
|
||||||
|
setOpenAITestButtonState('loading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: modelName,
|
||||||
|
messages: [{ role: 'user', content: 'Hi' }],
|
||||||
|
stream: false,
|
||||||
|
max_tokens: 5
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawText = await response.text();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMessage = `${response.status} ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const parsed = rawText ? JSON.parse(rawText) : null;
|
||||||
|
errorMessage = parsed?.error?.message || parsed?.message || errorMessage;
|
||||||
|
} catch (error) {
|
||||||
|
if (rawText) {
|
||||||
|
errorMessage = rawText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setOpenAITestStatus('', 'info');
|
||||||
|
setOpenAITestButtonState('success');
|
||||||
|
} catch (error) {
|
||||||
|
this.setOpenAITestStatus(`${i18n.t('ai_providers.openai_test_failed')}: ${error.message}`, 'error');
|
||||||
|
setOpenAITestButtonState('error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function showAddOpenAIProviderModal() {
|
export function showAddOpenAIProviderModal() {
|
||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
const modalBody = document.getElementById('modal-body');
|
const modalBody = document.getElementById('modal-body');
|
||||||
@@ -1490,6 +1680,18 @@ export function editOpenAIProvider(index, provider) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${i18n.t('ai_providers.openai_test_title')}</label>
|
||||||
|
<p class="form-hint">${i18n.t('ai_providers.openai_test_hint')}</p>
|
||||||
|
<div class="input-group openai-test-group">
|
||||||
|
<select id="openai-test-model-select" aria-label="${i18n.t('ai_providers.openai_test_model_placeholder')}"></select>
|
||||||
|
<input type="text" id="openai-test-model-input" placeholder="${i18n.t('ai_providers.openai_test_model_placeholder')}">
|
||||||
|
<button type="button" class="btn btn-secondary" id="openai-test-button" onclick="manager.testOpenAIProviderConnection()">
|
||||||
|
<i class="fas fa-stethoscope"></i> ${i18n.t('ai_providers.openai_test_action')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="openai-test-status" class="openai-test-status"></div>
|
||||||
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
<button class="btn btn-secondary" onclick="manager.closeModal()">${i18n.t('common.cancel')}</button>
|
||||||
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button>
|
<button class="btn btn-primary" onclick="manager.updateOpenAIProvider(${index})">${i18n.t('common.update')}</button>
|
||||||
@@ -1500,6 +1702,28 @@ export function editOpenAIProvider(index, provider) {
|
|||||||
this.populateModelFields('edit-provider-models-wrapper', models);
|
this.populateModelFields('edit-provider-models-wrapper', models);
|
||||||
this.populateHeaderFields('edit-openai-headers-wrapper', provider?.headers || null);
|
this.populateHeaderFields('edit-openai-headers-wrapper', provider?.headers || null);
|
||||||
this.populateApiKeyEntryFields('edit-openai-keys-wrapper', apiKeyEntries);
|
this.populateApiKeyEntryFields('edit-openai-keys-wrapper', apiKeyEntries);
|
||||||
|
this.populateOpenAITestModelOptions(models);
|
||||||
|
this.setOpenAITestStatus('', 'info');
|
||||||
|
setOpenAITestButtonState('idle');
|
||||||
|
|
||||||
|
const modelWrapper = document.getElementById('edit-provider-models-wrapper');
|
||||||
|
if (modelWrapper) {
|
||||||
|
modelWrapper.addEventListener('input', () => {
|
||||||
|
const currentModels = this.collectModelInputs('edit-provider-models-wrapper');
|
||||||
|
this.populateOpenAITestModelOptions(currentModels, { preserveInput: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelSelect = document.getElementById('openai-test-model-select');
|
||||||
|
if (modelSelect) {
|
||||||
|
modelSelect.addEventListener('change', (event) => {
|
||||||
|
const value = event?.target?.value || '';
|
||||||
|
const input = document.getElementById('openai-test-model-input');
|
||||||
|
if (input && value) {
|
||||||
|
input.value = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateOpenAIProvider(index) {
|
export async function updateOpenAIProvider(index) {
|
||||||
@@ -1683,6 +1907,9 @@ export const aiProvidersModule = {
|
|||||||
setOpenAIModelDiscoverySearch,
|
setOpenAIModelDiscoverySearch,
|
||||||
applyOpenAIModelDiscoverySelection,
|
applyOpenAIModelDiscoverySelection,
|
||||||
closeOpenAIModelDiscovery,
|
closeOpenAIModelDiscovery,
|
||||||
|
populateOpenAITestModelOptions,
|
||||||
|
setOpenAITestStatus,
|
||||||
|
testOpenAIProviderConnection,
|
||||||
addModelField,
|
addModelField,
|
||||||
populateModelFields,
|
populateModelFields,
|
||||||
collectModelInputs,
|
collectModelInputs,
|
||||||
|
|||||||
43
styles.css
43
styles.css
@@ -2725,6 +2725,49 @@ input:checked+.slider:before {
|
|||||||
color: #d97706;
|
color: #d97706;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.openai-test-status {
|
||||||
|
min-height: 22px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-status.success {
|
||||||
|
color: #16a34a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-status.error {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-status.warning {
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-group select {
|
||||||
|
min-width: 200px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--border-primary);
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-group select:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-btn-success {
|
||||||
|
background: #16a34a !important;
|
||||||
|
border-color: #16a34a !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.openai-test-btn-error {
|
||||||
|
background: #dc2626 !important;
|
||||||
|
border-color: #dc2626 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
|
||||||
.model-discovery-list {
|
.model-discovery-list {
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
|
|||||||
Reference in New Issue
Block a user