@@ -784,19 +793,10 @@ function editProvider(uuid, event) {
// 添加编辑状态类
providerDetail.classList.add('editing');
- // 替换编辑按钮为保存和取消按钮,但保留禁用/启用按钮
+ // 替换编辑按钮为保存和取消按钮,不显示禁用/启用按钮
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
- const toggleButton = actionsGroup.querySelector('[onclick*="toggleProviderStatus"]');
- const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
- const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
- const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用';
- const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban';
- const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
actionsGroup.innerHTML = `
-
@@ -852,11 +852,11 @@ function cancelEdit(uuid, event) {
select.value = originalValue || '';
});
- // 恢复原来的编辑和删除按钮,但保留禁用/启用按钮
+ // 恢复原来的按钮布局
const actionsGroup = providerDetail.querySelector('.provider-actions-group');
const currentProvider = providerDetail.closest('.provider-modal').querySelector(`[data-uuid="${uuid}"]`);
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
- const toggleButtonText = isCurrentlyDisabled ? '启用' : '禁用';
+ const toggleButtonText = isCurrentlyDisabled ? t('modal.provider.enabled') : t('modal.provider.disabled');
const toggleButtonIcon = isCurrentlyDisabled ? 'fas fa-play' : 'fas fa-ban';
const toggleButtonClass = isCurrentlyDisabled ? 'btn-success' : 'btn-warning';
@@ -865,10 +865,13 @@ function cancelEdit(uuid, event) {
${toggleButtonText}
+
`;
}
@@ -1379,6 +1382,134 @@ async function performHealthCheck(providerType) {
}
}
+/**
+ * 刷新提供商UUID
+ * @param {string} uuid - 提供商UUID
+ * @param {Event} event - 事件对象
+ */
+async function refreshProviderUuid(uuid, event) {
+ event.stopPropagation();
+
+ if (!confirm(t('modal.provider.refreshUuidConfirm', { oldUuid: uuid }))) {
+ return;
+ }
+
+ const providerDetail = event.target.closest('.provider-item-detail');
+ const providerType = providerDetail.closest('.provider-modal').getAttribute('data-provider-type');
+
+ try {
+ const response = await window.apiClient.post(
+ `/providers/${encodeURIComponent(providerType)}/${uuid}/refresh-uuid`,
+ {}
+ );
+
+ if (response.success) {
+ showToast(t('common.success'), t('modal.provider.refreshUuid.success', { oldUuid: response.oldUuid, newUuid: response.newUuid }), 'success');
+
+ // 重新加载配置
+ await window.apiClient.post('/reload-config');
+
+ // 刷新提供商配置显示
+ await refreshProviderConfig(providerType);
+ } else {
+ showToast(t('common.error'), t('modal.provider.refreshUuid.failed'), 'error');
+ }
+ } catch (error) {
+ console.error('刷新uuid失败:', error);
+ showToast(t('common.error'), t('modal.provider.refreshUuid.failed') + ': ' + error.message, 'error');
+ }
+}
+
+/**
+ * 删除所有不健康的提供商节点
+ * @param {string} providerType - 提供商类型
+ */
+async function deleteUnhealthyProviders(providerType) {
+ // 先获取不健康节点数量
+ const unhealthyCount = currentProviders.filter(p => !p.isHealthy).length;
+
+ if (unhealthyCount === 0) {
+ showToast(t('common.info'), t('modal.provider.deleteUnhealthy.noUnhealthy'), 'info');
+ return;
+ }
+
+ if (!confirm(t('modal.provider.deleteUnhealthyConfirm', { type: providerType, count: unhealthyCount }))) {
+ return;
+ }
+
+ try {
+ showToast(t('common.info'), t('modal.provider.deleteUnhealthy.deleting'), 'info');
+
+ const response = await window.apiClient.delete(
+ `/providers/${encodeURIComponent(providerType)}/delete-unhealthy`
+ );
+
+ if (response.success) {
+ showToast(
+ t('common.success'),
+ t('modal.provider.deleteUnhealthy.success', { count: response.deletedCount }),
+ 'success'
+ );
+
+ // 重新加载配置
+ await window.apiClient.post('/reload-config');
+
+ // 刷新提供商配置显示
+ await refreshProviderConfig(providerType);
+ } else {
+ showToast(t('common.error'), t('modal.provider.deleteUnhealthy.failed'), 'error');
+ }
+ } catch (error) {
+ console.error('删除不健康节点失败:', error);
+ showToast(t('common.error'), t('modal.provider.deleteUnhealthy.failed') + ': ' + error.message, 'error');
+ }
+}
+
+/**
+ * 批量刷新不健康节点的UUID
+ * @param {string} providerType - 提供商类型
+ */
+async function refreshUnhealthyUuids(providerType) {
+ // 先获取不健康节点数量
+ const unhealthyCount = currentProviders.filter(p => !p.isHealthy).length;
+
+ if (unhealthyCount === 0) {
+ showToast(t('common.info'), t('modal.provider.refreshUnhealthyUuids.noUnhealthy'), 'info');
+ return;
+ }
+
+ if (!confirm(t('modal.provider.refreshUnhealthyUuidsConfirm', { type: providerType, count: unhealthyCount }))) {
+ return;
+ }
+
+ try {
+ showToast(t('common.info'), t('modal.provider.refreshUnhealthyUuids.refreshing'), 'info');
+
+ const response = await window.apiClient.post(
+ `/providers/${encodeURIComponent(providerType)}/refresh-unhealthy-uuids`
+ );
+
+ if (response.success) {
+ showToast(
+ t('common.success'),
+ t('modal.provider.refreshUnhealthyUuids.success', { count: response.refreshedCount }),
+ 'success'
+ );
+
+ // 重新加载配置
+ await window.apiClient.post('/reload-config');
+
+ // 刷新提供商配置显示
+ await refreshProviderConfig(providerType);
+ } else {
+ showToast(t('common.error'), t('modal.provider.refreshUnhealthyUuids.failed'), 'error');
+ }
+ } catch (error) {
+ console.error('刷新不健康节点UUID失败:', error);
+ showToast(t('common.error'), t('modal.provider.refreshUnhealthyUuids.failed') + ': ' + error.message, 'error');
+ }
+}
+
/**
* 渲染不支持的模型选择器(不调用API,直接使用传入的模型列表)
* @param {string} uuid - 提供商UUID
@@ -1430,9 +1561,12 @@ export {
toggleProviderStatus,
resetAllProvidersHealth,
performHealthCheck,
+ deleteUnhealthyProviders,
+ refreshUnhealthyUuids,
loadModelsForProviderType,
renderNotSupportedModelsSelector,
- goToProviderPage
+ goToProviderPage,
+ refreshProviderUuid
};
// 将函数挂载到window对象
@@ -1447,4 +1581,7 @@ window.addProvider = addProvider;
window.toggleProviderStatus = toggleProviderStatus;
window.resetAllProvidersHealth = resetAllProvidersHealth;
window.performHealthCheck = performHealthCheck;
-window.goToProviderPage = goToProviderPage;
\ No newline at end of file
+window.deleteUnhealthyProviders = deleteUnhealthyProviders;
+window.refreshUnhealthyUuids = refreshUnhealthyUuids;
+window.goToProviderPage = goToProviderPage;
+window.refreshProviderUuid = refreshProviderUuid;
\ No newline at end of file
diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js
index f802112..3fcf96d 100644
--- a/static/app/upload-config-manager.js
+++ b/static/app/upload-config-manager.js
@@ -12,7 +12,7 @@ let isLoadingConfigs = false; // 防止重复加载配置
* @param {string} searchTerm - 搜索关键词
* @param {string} statusFilter - 状态过滤
*/
-function searchConfigs(searchTerm = '', statusFilter = '') {
+function searchConfigs(searchTerm = '', statusFilter = '', providerFilter = '') {
if (!allConfigs.length) {
console.log('没有配置数据可搜索');
return;
@@ -29,7 +29,20 @@ function searchConfigs(searchTerm = '', statusFilter = '') {
const configStatus = config.isUsed ? 'used' : 'unused';
const matchesStatus = !statusFilter || configStatus === statusFilter;
- return matchesSearch && matchesStatus;
+ // 提供商类型过滤
+ let matchesProvider = true;
+ if (providerFilter) {
+ const providerInfo = detectProviderFromPath(config.path);
+ if (providerFilter === 'other') {
+ // "其他/未识别" 选项:匹配没有识别到提供商的配置
+ matchesProvider = providerInfo === null;
+ } else {
+ // 匹配特定提供商类型
+ matchesProvider = providerInfo !== null && providerInfo.providerType === providerFilter;
+ }
+ }
+
+ return matchesSearch && matchesStatus && matchesProvider;
});
renderConfigList();
@@ -705,6 +718,7 @@ function initUploadConfigManager() {
const searchInput = document.getElementById('configSearch');
const searchBtn = document.getElementById('searchConfigBtn');
const statusFilter = document.getElementById('configStatusFilter');
+ const providerFilter = document.getElementById('configProviderFilter');
const refreshBtn = document.getElementById('refreshConfigList');
const downloadAllBtn = document.getElementById('downloadAllConfigs');
@@ -712,7 +726,8 @@ function initUploadConfigManager() {
searchInput.addEventListener('input', debounce(() => {
const searchTerm = searchInput.value.trim();
const currentStatusFilter = statusFilter?.value || '';
- searchConfigs(searchTerm, currentStatusFilter);
+ const currentProviderFilter = providerFilter?.value || '';
+ searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
}, 300));
}
@@ -720,7 +735,8 @@ function initUploadConfigManager() {
searchBtn.addEventListener('click', () => {
const searchTerm = searchInput?.value.trim() || '';
const currentStatusFilter = statusFilter?.value || '';
- searchConfigs(searchTerm, currentStatusFilter);
+ const currentProviderFilter = providerFilter?.value || '';
+ searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
});
}
@@ -728,7 +744,17 @@ function initUploadConfigManager() {
statusFilter.addEventListener('change', () => {
const searchTerm = searchInput?.value.trim() || '';
const currentStatusFilter = statusFilter.value;
- searchConfigs(searchTerm, currentStatusFilter);
+ const currentProviderFilter = providerFilter?.value || '';
+ searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
+ });
+ }
+
+ if (providerFilter) {
+ providerFilter.addEventListener('change', () => {
+ const searchTerm = searchInput?.value.trim() || '';
+ const currentStatusFilter = statusFilter?.value || '';
+ const currentProviderFilter = providerFilter.value;
+ searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter);
});
}
@@ -746,6 +772,12 @@ function initUploadConfigManager() {
batchLinkBtn.addEventListener('click', batchLinkProviderConfigs);
}
+ // 删除未绑定配置按钮
+ const deleteUnboundBtn = document.getElementById('deleteUnboundBtn');
+ if (deleteUnboundBtn) {
+ deleteUnboundBtn.addEventListener('click', deleteUnboundConfigs);
+ }
+
// 初始加载配置列表
loadConfigList();
}
@@ -928,6 +960,67 @@ async function batchLinkProviderConfigs() {
}
}
+/**
+ * 删除所有未绑定的配置文件
+ * 只删除 configs/xxx/ 子目录下的未绑定配置文件
+ */
+async function deleteUnboundConfigs() {
+ // 统计未绑定的配置数量,并且必须在 configs/xxx/ 子目录下
+ const unboundConfigs = allConfigs.filter(config => {
+ if (config.isUsed) return false;
+
+ // 检查路径是否在 configs/xxx/ 子目录下
+ const normalizedPath = config.path.replace(/\\/g, '/');
+ const pathParts = normalizedPath.split('/');
+
+ // 路径至少需要3部分:configs/子目录/文件名
+ // 例如:configs/kiro/xxx.json 或 configs/gemini/xxx.json
+ if (pathParts.length >= 3 && pathParts[0] === 'configs') {
+ return true;
+ }
+
+ return false;
+ });
+
+ if (unboundConfigs.length === 0) {
+ showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info');
+ return;
+ }
+
+ // 显示确认对话框
+ const confirmMsg = t('upload.deleteUnbound.confirm', { count: unboundConfigs.length });
+ if (!confirm(confirmMsg)) {
+ return;
+ }
+
+ try {
+ showToast(t('common.info'), t('upload.deleteUnbound.processing'), 'info');
+
+ const result = await window.apiClient.delete('/upload-configs/delete-unbound');
+
+ if (result.deletedCount > 0) {
+ showToast(t('common.success'), t('upload.deleteUnbound.success', { count: result.deletedCount }), 'success');
+
+ // 刷新配置列表
+ await loadConfigList();
+ } else {
+ showToast(t('common.info'), t('upload.deleteUnbound.none'), 'info');
+ }
+
+ // 如果有失败的文件,显示警告
+ if (result.failedCount > 0) {
+ console.warn('部分文件删除失败:', result.failedFiles);
+ showToast(t('common.warning'), t('upload.deleteUnbound.partial', {
+ success: result.deletedCount,
+ fail: result.failedCount
+ }), 'warning');
+ }
+ } catch (error) {
+ console.error('删除未绑定配置失败:', error);
+ showToast(t('common.error'), t('upload.deleteUnbound.failed') + ': ' + error.message, 'error');
+ }
+}
+
/**
* 防抖函数
* @param {Function} func - 要防抖的函数
@@ -1002,5 +1095,6 @@ export {
deleteConfig,
closeConfigModal,
copyConfigContent,
- reloadConfig
+ reloadConfig,
+ deleteUnboundConfigs
};
\ No newline at end of file
diff --git a/static/components/section-providers.css b/static/components/section-providers.css
index 2e133ae..3375bbb 100644
--- a/static/components/section-providers.css
+++ b/static/components/section-providers.css
@@ -1004,6 +1004,34 @@
box-shadow: 0 4px 12px var(--info-hover);
}
+/* 删除不健康节点按钮样式 */
+.provider-summary-actions .btn-danger {
+ background: linear-gradient(135deg, var(--danger-alt) 0%, var(--danger-secondary) 100%);
+ color: var(--white);
+ border: none;
+ box-shadow: 0 2px 8px var(--danger-30);
+}
+
+.provider-summary-actions .btn-danger:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px var(--danger-40);
+ background: linear-gradient(135deg, var(--danger-color) 0%, var(--danger-alt) 100%);
+}
+
+/* 刷新不健康UUID按钮样式 */
+.provider-summary-actions .btn-secondary {
+ background: linear-gradient(135deg, var(--neutral-500) 0%, var(--neutral-600) 100%);
+ color: var(--white);
+ border: none;
+ box-shadow: 0 2px 8px var(--neutral-shadow-lg);
+}
+
+.provider-summary-actions .btn-secondary:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px var(--neutral-shadow-lg);
+ background: linear-gradient(135deg, var(--neutral-600) 0%, var(--neutral-700) 100%);
+}
+
/* 不支持的模型选择器样式 */
.not-supported-models-section {
grid-column: 1 / -1;
diff --git a/static/components/section-upload-config.css b/static/components/section-upload-config.css
index 86da830..ee0c877 100644
--- a/static/components/section-upload-config.css
+++ b/static/components/section-upload-config.css
@@ -93,6 +93,40 @@
overflow-y: auto;
}
+/* 无配置文件提示样式 */
+.no-configs {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 4rem 2rem;
+ text-align: center;
+ background: linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-primary) 100%);
+ border-radius: 0.5rem;
+ margin: 1rem;
+}
+
+.no-configs p {
+ font-size: 1rem;
+ color: var(--text-secondary);
+ margin: 0;
+ padding: 1rem 2rem;
+ background: var(--bg-tertiary);
+ border: 1px dashed var(--border-color);
+ border-radius: 0.5rem;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.no-configs p::before {
+ content: '\f07c';
+ font-family: 'Font Awesome 6 Free';
+ font-weight: 400;
+ font-size: 1.25rem;
+ color: var(--text-tertiary);
+}
+
.config-item-manager {
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
@@ -396,6 +430,96 @@
.delete-modal-footer { flex-direction: column; }
}
+/* 删除未绑定按钮样式 */
+.btn-delete-unbound {
+ background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ margin-left: 8px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px var(--danger-30);
+ font-weight: 500;
+}
+
+.btn-delete-unbound:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+ background: linear-gradient(135deg, #dc2626 0%, #ef4444 100%);
+}
+
+.btn-delete-unbound:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* 配置列表头部的刷新按钮样式 */
+.btn-refresh {
+ background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ margin-left: 8px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
+ font-weight: 500;
+}
+
+.btn-refresh:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+ background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
+}
+
+.btn-refresh:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* 配置列表头部的打包下载按钮样式 */
+.btn-download {
+ background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ padding: 6px 12px;
+ font-size: 12px;
+ cursor: pointer;
+ margin-left: 8px;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
+ font-weight: 500;
+}
+
+.btn-download:hover {
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+ background: linear-gradient(135deg, #059669 0%, #10b981 100%);
+}
+
+.btn-download:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ transform: none;
+}
+
/* 暗黑主题适配 */
[data-theme="dark"] .status-used { background: var(--success-bg); color: var(--success-text); }
[data-theme="dark"] .status-unused { background: var(--warning-bg); color: var(--warning-text); }
diff --git a/static/components/section-upload-config.html b/static/components/section-upload-config.html
index faea4b3..29e7ebb 100644
--- a/static/components/section-upload-config.html
+++ b/static/components/section-upload-config.html
@@ -1,7 +1,7 @@
- 配置管理
+ 凭据文件管理
+
+
+
+
-
@@ -48,6 +49,15 @@