Files
Cli-Proxy-API-Management-Ce…/src/modules/auth-files.js

1045 lines
40 KiB
JavaScript

export const authFilesModule = {
// 加载认证文件
async loadAuthFiles(keyStats = null) {
try {
const data = await this.makeRequest('/auth-files');
if (!keyStats) {
keyStats = await this.getKeyStats();
}
await this.renderAuthFiles(data.files || [], keyStats);
} catch (error) {
console.error('加载认证文件失败:', error);
}
},
// 渲染认证文件列表
async renderAuthFiles(files, keyStats = null) {
const container = document.getElementById('auth-files-list');
if (!container) {
return;
}
const allFiles = Array.isArray(files) ? files : [];
const visibleFiles = allFiles.filter(file => {
if (!file) return false;
return this.shouldDisplayDisabledGeminiCli(file) || file.disabled !== true;
});
const stats = keyStats || await this.getKeyStats();
this.cachedAuthFiles = visibleFiles.map(file => ({ ...file }));
this.authFileStatsCache = stats || { bySource: {}, byAuthIndex: {} };
this.syncAuthFileControls();
if (this.cachedAuthFiles.length === 0) {
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3>${i18n.t('auth_files.empty_title')}</h3>
<p>${i18n.t('auth_files.empty_desc')}</p>
</div>
`;
this.updateFilterButtons(new Set(['all']));
this.bindAuthFileFilterEvents();
this.applyAuthFileFilterState(false);
this.authFilesPagination.currentPage = 1;
this.authFilesPagination.totalPages = 1;
this.updatePaginationControls(0);
return;
}
const existingTypes = new Set(['all']);
this.cachedAuthFiles.forEach(file => {
if (file.type) {
existingTypes.add(file.type);
}
});
this.updateFilterButtons(existingTypes);
this.bindAuthFileFilterEvents();
this.applyAuthFileFilterState(false);
this.renderAuthFilesPage(this.authFilesPagination.currentPage);
this.bindAuthFileActionEvents();
},
isRuntimeOnlyAuthFile(file) {
if (!file) return false;
const runtimeValue = file.runtime_only;
return runtimeValue === true || runtimeValue === 'true';
},
shouldDisplayDisabledGeminiCli(file) {
if (!file) return false;
const provider = typeof file.provider === 'string' ? file.provider.toLowerCase() : '';
const type = typeof file.type === 'string' ? file.type.toLowerCase() : '';
const isGeminiCli = provider === 'gemini-cli' || type === 'gemini-cli';
return isGeminiCli && !this.isRuntimeOnlyAuthFile(file);
},
resolveAuthFileStats(file, stats = {}) {
const statsBySource = (stats && stats.bySource) || stats || {};
const statsByAuthIndex = (stats && stats.byAuthIndex) || {};
const rawFileName = typeof file?.name === 'string' ? file.name : '';
const defaultStats = { success: 0, failure: 0 };
const authIndexKey = this.normalizeAuthIndexValue(file?.auth_index);
if (authIndexKey && statsByAuthIndex[authIndexKey]) {
return statsByAuthIndex[authIndexKey];
}
if (!rawFileName) {
return defaultStats;
}
const fromName = statsBySource[rawFileName];
if (fromName && (fromName.success > 0 || fromName.failure > 0)) {
return fromName;
}
let fileStats = fromName || defaultStats;
if (fileStats.success === 0 && fileStats.failure === 0) {
const nameWithoutExt = rawFileName.replace(/\.[^/.]+$/, "");
if (nameWithoutExt && nameWithoutExt !== rawFileName) {
const candidateNames = new Set([nameWithoutExt]);
const normalizedName = nameWithoutExt.toLowerCase();
const typePrefix = typeof file?.type === 'string' ? file.type.trim().toLowerCase() : '';
const providerPrefix = typeof file?.provider === 'string' ? file.provider.trim().toLowerCase() : '';
const prefixList = [];
if (typePrefix) {
prefixList.push(`${typePrefix}-`);
}
if (providerPrefix && providerPrefix !== typePrefix) {
prefixList.push(`${providerPrefix}-`);
}
prefixList.forEach(prefix => {
if (prefix && normalizedName.startsWith(prefix)) {
const trimmed = nameWithoutExt.substring(prefix.length);
if (trimmed) {
candidateNames.add(trimmed);
}
}
});
for (const candidate of candidateNames) {
const candidateStats = statsBySource[candidate];
if (candidateStats && (candidateStats.success > 0 || candidateStats.failure > 0)) {
fileStats = candidateStats;
break;
}
}
}
}
return fileStats || defaultStats;
},
normalizeAuthIndexValue(value) {
if (typeof value === 'number' && Number.isFinite(value)) {
return value.toString();
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? trimmed : null;
}
return null;
},
buildAuthFileItemHtml(file) {
const rawFileName = typeof file?.name === 'string' ? file.name : '';
const safeFileName = this.escapeHtml(rawFileName);
const stats = this.authFileStatsCache || {};
const fileStats = this.resolveAuthFileStats(file, stats);
const fileType = file.type || 'unknown';
let typeDisplayKey;
switch (fileType) {
case 'qwen':
typeDisplayKey = 'auth_files.type_qwen';
break;
case 'gemini':
typeDisplayKey = 'auth_files.type_gemini';
break;
case 'gemini-cli':
typeDisplayKey = 'auth_files.type_gemini-cli';
break;
case 'aistudio':
typeDisplayKey = 'auth_files.type_aistudio';
break;
case 'claude':
typeDisplayKey = 'auth_files.type_claude';
break;
case 'codex':
typeDisplayKey = 'auth_files.type_codex';
break;
case 'iflow':
typeDisplayKey = 'auth_files.type_iflow';
break;
case 'vertex':
typeDisplayKey = 'auth_files.type_vertex';
break;
case 'empty':
typeDisplayKey = 'auth_files.type_empty';
break;
default:
typeDisplayKey = 'auth_files.type_unknown';
break;
}
const typeBadge = `<span class="file-type-badge ${fileType}">${i18n.t(typeDisplayKey)}</span>`;
const isRuntimeOnly = this.isRuntimeOnlyAuthFile(file);
const shouldShowMainFlag = this.shouldDisplayDisabledGeminiCli(file);
const mainFlagButton = shouldShowMainFlag ? `
<button class="btn-small btn-warning main-flag-btn" title="主" disabled>主</button>` : '';
const actionsHtml = isRuntimeOnly ? `
<div class="item-actions">
<span class="virtual-auth-badge">虚拟认证文件</span>
</div>` : `
<div class="item-actions" data-filename="${safeFileName}">
${mainFlagButton}
<button class="btn-small btn-info" data-action="showDetails" title="详细信息">
<i class="fas fa-info-circle"></i>
</button>
<button class="btn-small btn-primary" data-action="download" title="下载">
<i class="fas fa-download"></i>
</button>
<button class="btn-small btn-danger" data-action="delete" title="删除">
<i class="fas fa-trash"></i>
</button>
</div>`;
return `
<div class="file-item" data-file-type="${fileType}" data-file-name="${safeFileName}" ${isRuntimeOnly ? 'data-runtime-only="true"' : ''}>
<div class="item-content">
<div class="item-title">${typeBadge}${safeFileName}</div>
<div class="item-meta">
<span class="item-subtitle">${i18n.t('auth_files.file_modified')}: ${new Date(file.modtime).toLocaleString(i18n.currentLanguage === 'zh-CN' ? 'zh-CN' : 'en-US')}</span>
<span class="item-subtitle">${i18n.t('auth_files.file_size')}: ${this.formatFileSize(file.size)}</span>
</div>
<div class="item-footer">
<div class="item-stats">
<span class="stat-badge stat-success">
<i class="fas fa-check-circle"></i> ${i18n.t('stats.success')}: ${fileStats.success}
</span>
<span class="stat-badge stat-failure">
<i class="fas fa-times-circle"></i> ${i18n.t('stats.failure')}: ${fileStats.failure}
</span>
</div>
${actionsHtml}
</div>
</div>
</div>
`;
},
getFilteredAuthFiles(filterType = this.currentAuthFileFilter) {
const files = Array.isArray(this.cachedAuthFiles) ? this.cachedAuthFiles : [];
if (!files.length) {
return [];
}
const typeFilter = (filterType || 'all').toLowerCase();
const keyword = (this.authFileSearchQuery || '').trim().toLowerCase();
return files.filter(file => {
const name = String(file?.name || '').toLowerCase();
const type = String(file?.type || '').toLowerCase();
const provider = String(file?.provider || '').toLowerCase();
if (typeFilter !== 'all') {
if (!type) return false;
if (type !== typeFilter) {
if (!provider || provider !== typeFilter) {
return false;
}
}
}
if (!keyword) return true;
return name.includes(keyword) || type.includes(keyword) || provider.includes(keyword);
});
},
updateAuthFileSearchQuery(value = '') {
const normalized = (value || '').trim();
if (this.authFileSearchQuery === normalized) {
return;
}
this.authFileSearchQuery = normalized;
this.authFilesPagination.currentPage = 1;
this.renderAuthFilesPage(1);
},
updateAuthFilesPageSize(value) {
const normalized = this.normalizeAuthFilesPageSize(value);
if (this.authFilesPagination?.pageSize === normalized) {
this.syncAuthFileControls();
return;
}
this.authFilesPagination.pageSize = normalized;
this.authFilesPagination.currentPage = 1;
try {
localStorage.setItem(this.authFilesPageSizeKey, `${normalized}`);
} catch (error) {
console.warn('Failed to persist auth files page size:', error);
}
this.syncAuthFileControls();
this.renderAuthFilesPage(1);
},
syncAuthFileControls() {
const searchInput = document.getElementById('auth-files-search-input');
if (searchInput && searchInput.value !== this.authFileSearchQuery) {
searchInput.value = this.authFileSearchQuery;
}
const pageSizeInput = document.getElementById('auth-files-page-size-input');
const targetSize = this.authFilesPagination?.pageSize || 9;
if (pageSizeInput && parseInt(pageSizeInput.value, 10) !== targetSize) {
pageSizeInput.value = targetSize;
}
},
bindAuthFilesSearchControl() {
const searchInput = document.getElementById('auth-files-search-input');
if (!searchInput) return;
if (searchInput._authFileSearchListener) {
searchInput.removeEventListener('input', searchInput._authFileSearchListener);
}
const debounced = this.debounce((value) => {
this.updateAuthFileSearchQuery(value);
}, 250);
const listener = (event) => {
const value = event?.target?.value ?? '';
debounced(value);
};
searchInput._authFileSearchListener = listener;
searchInput.addEventListener('input', listener);
},
bindAuthFilesPageSizeControl() {
const pageSizeInput = document.getElementById('auth-files-page-size-input');
if (!pageSizeInput) return;
if (pageSizeInput._authFilePageSizeListener) {
pageSizeInput.removeEventListener('change', pageSizeInput._authFilePageSizeListener);
}
const listener = (event) => {
const value = parseInt(event?.target?.value, 10);
if (!Number.isFinite(value)) {
return;
}
this.updateAuthFilesPageSize(value);
};
pageSizeInput._authFilePageSizeListener = listener;
pageSizeInput.addEventListener('change', listener);
if (pageSizeInput._authFilePageSizeBlur) {
pageSizeInput.removeEventListener('blur', pageSizeInput._authFilePageSizeBlur);
}
const blurListener = () => {
if (!pageSizeInput.value) {
this.syncAuthFileControls();
}
};
pageSizeInput._authFilePageSizeBlur = blurListener;
pageSizeInput.addEventListener('blur', blurListener);
},
renderAuthFilesPage(page = null) {
const container = document.getElementById('auth-files-list');
if (!container) return;
const pageSize = this.authFilesPagination?.pageSize || 9;
const filteredFiles = this.getFilteredAuthFiles();
const totalItems = filteredFiles.length;
const hasCachedFiles = Array.isArray(this.cachedAuthFiles) && this.cachedAuthFiles.length > 0;
const filterApplied = (this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all');
const searchApplied = Boolean((this.authFileSearchQuery || '').trim());
if (totalItems === 0) {
const titleKey = hasCachedFiles && (filterApplied || searchApplied)
? 'auth_files.search_empty_title'
: 'auth_files.empty_title';
const descKey = hasCachedFiles && (filterApplied || searchApplied)
? 'auth_files.search_empty_desc'
: 'auth_files.empty_desc';
container.innerHTML = `
<div class="empty-state">
<i class="fas fa-file-alt"></i>
<h3>${i18n.t(titleKey)}</h3>
<p>${i18n.t(descKey)}</p>
</div>
`;
this.authFilesPagination.currentPage = 1;
this.updatePaginationControls(0);
return;
}
const maxPages = Math.max(1, Math.ceil(totalItems / pageSize));
let currentPage = page == null ? (this.authFilesPagination.currentPage || 1) : page;
if (currentPage > maxPages) {
currentPage = maxPages;
}
if (currentPage < 1) {
currentPage = 1;
}
this.authFilesPagination.currentPage = currentPage;
this.authFilesPagination.totalPages = maxPages;
const startIndex = (currentPage - 1) * pageSize;
const pageFiles = filteredFiles.slice(startIndex, startIndex + pageSize);
container.innerHTML = pageFiles.map(file => this.buildAuthFileItemHtml(file)).join('');
this.updatePaginationControls(totalItems);
this.bindAuthFileActionEvents();
},
bindAuthFilesPaginationEvents() {
const container = document.getElementById('auth-files-pagination');
if (!container) return;
const oldListener = container._paginationListener;
if (oldListener) {
container.removeEventListener('click', oldListener);
}
const listener = (event) => {
const button = event.target.closest('button[data-action]');
if (!button || !container.contains(button)) return;
event.preventDefault();
const action = button.dataset.action;
const currentPage = this.authFilesPagination?.currentPage || 1;
if (action === 'prev') {
this.renderAuthFilesPage(currentPage - 1);
} else if (action === 'next') {
this.renderAuthFilesPage(currentPage + 1);
}
};
container._paginationListener = listener;
container.addEventListener('click', listener);
},
updatePaginationControls(totalItems = 0) {
const paginationContainer = document.getElementById('auth-files-pagination');
const infoEl = document.getElementById('auth-files-pagination-info');
if (!paginationContainer || !infoEl) return;
const prevBtn = paginationContainer.querySelector('button[data-action=\"prev\"]');
const nextBtn = paginationContainer.querySelector('button[data-action=\"next\"]');
const pageSize = this.authFilesPagination?.pageSize || 9;
const totalPages = this.authFilesPagination?.totalPages || 1;
const currentPage = Math.min(this.authFilesPagination?.currentPage || 1, totalPages);
const shouldShow = totalItems > pageSize;
paginationContainer.style.display = shouldShow ? 'flex' : 'none';
const infoParams = totalItems === 0
? { current: 0, total: 0, count: 0 }
: { current: currentPage, total: totalPages, count: totalItems };
infoEl.textContent = i18n.t('auth_files.pagination_info', infoParams);
if (prevBtn) {
prevBtn.disabled = currentPage <= 1;
}
if (nextBtn) {
nextBtn.disabled = currentPage >= totalPages;
}
},
updateFilterButtons(existingTypes) {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
const predefinedTypes = [
{ type: 'all', labelKey: 'auth_files.filter_all' },
{ type: 'qwen', labelKey: 'auth_files.filter_qwen' },
{ type: 'gemini', labelKey: 'auth_files.filter_gemini' },
{ type: 'gemini-cli', labelKey: 'auth_files.filter_gemini-cli' },
{ type: 'aistudio', labelKey: 'auth_files.filter_aistudio' },
{ type: 'claude', labelKey: 'auth_files.filter_claude' },
{ type: 'codex', labelKey: 'auth_files.filter_codex' },
{ type: 'iflow', labelKey: 'auth_files.filter_iflow' },
{ type: 'vertex', labelKey: 'auth_files.filter_vertex' },
{ type: 'empty', labelKey: 'auth_files.filter_empty' }
];
const existingButtons = filterContainer.querySelectorAll('.filter-btn');
const existingButtonTypes = new Set();
existingButtons.forEach(btn => {
existingButtonTypes.add(btn.dataset.type);
});
existingButtons.forEach(btn => {
const btnType = btn.dataset.type;
if (existingTypes.has(btnType)) {
btn.style.display = 'inline-block';
const match = predefinedTypes.find(item => item.type === btnType);
if (match) {
btn.textContent = i18n.t(match.labelKey);
btn.setAttribute('data-i18n-text', match.labelKey);
}
} else {
btn.style.display = 'none';
}
});
const predefinedTypeSet = new Set(predefinedTypes.map(t => t.type));
existingTypes.forEach(type => {
if (type !== 'all' && !predefinedTypeSet.has(type) && !existingButtonTypes.has(type)) {
const btn = document.createElement('button');
btn.className = 'filter-btn';
btn.dataset.type = type;
const match = predefinedTypes.find(item => item.type === type);
if (match) {
btn.setAttribute('data-i18n-text', match.labelKey);
btn.textContent = i18n.t(match.labelKey);
} else {
const dynamicKey = `auth_files.filter_${type}`;
btn.setAttribute('data-i18n-text', dynamicKey);
btn.textContent = this.generateDynamicTypeLabel(type);
}
const emptyBtn = filterContainer.querySelector('[data-type=\"empty\"]');
if (emptyBtn) {
filterContainer.insertBefore(btn, emptyBtn);
} else {
filterContainer.appendChild(btn);
}
}
});
},
handleFilterClick(clickedBtn, options = {}) {
if (!clickedBtn) return;
const { skipRender = false } = options;
const filterBtns = document.querySelectorAll('.auth-file-filter .filter-btn');
filterBtns.forEach(b => b.classList.remove('active'));
clickedBtn.classList.add('active');
const filterType = clickedBtn.dataset.type;
this.currentAuthFileFilter = filterType || 'all';
if (!skipRender) {
this.authFilesPagination.currentPage = 1;
this.renderAuthFilesPage(1);
}
this.refreshFilterButtonTexts();
},
generateDynamicTypeLabel(type) {
if (!type) return '';
const key = `auth_files.type_${type}`;
const translated = i18n.t(key);
if (translated && translated !== key) {
return translated;
}
if (type.toLowerCase() === 'iflow') return 'iFlow';
return type.charAt(0).toUpperCase() + type.slice(1);
},
bindAuthFileFilterEvents() {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
if (filterContainer._filterListener) {
filterContainer.removeEventListener('click', filterContainer._filterListener);
}
const listener = (event) => {
const button = event.target.closest('.filter-btn');
if (!button || !filterContainer.contains(button)) return;
event.preventDefault();
this.handleFilterClick(button);
};
filterContainer._filterListener = listener;
filterContainer.addEventListener('click', listener);
this.refreshFilterButtonTexts();
},
applyAuthFileFilterState(shouldRender = false) {
const filterContainer = document.querySelector('.auth-file-filter');
if (!filterContainer) return;
const currentType = this.currentAuthFileFilter || 'all';
const buttons = filterContainer.querySelectorAll('.filter-btn');
if (buttons.length === 0) return;
let targetButton = null;
buttons.forEach(btn => {
if (btn.dataset.type === currentType) {
targetButton = btn;
}
});
if (!targetButton) {
targetButton = filterContainer.querySelector('.filter-btn[data-type=\"all\"]') || buttons[0];
if (targetButton) {
this.currentAuthFileFilter = targetButton.dataset.type || 'all';
}
}
if (targetButton) {
this.handleFilterClick(targetButton, { skipRender: !shouldRender });
}
},
removeAuthFileElements(filenames = []) {
if (!Array.isArray(filenames) || filenames.length === 0) {
return;
}
const removalSet = new Set(filenames);
this.cachedAuthFiles = (this.cachedAuthFiles || []).filter(file => file && !removalSet.has(file.name));
if (!this.cachedAuthFiles.length) {
this.authFilesPagination.currentPage = 1;
}
this.renderAuthFilesPage(this.authFilesPagination.currentPage);
},
refreshFilterButtonTexts() {
document.querySelectorAll('.auth-file-filter .filter-btn[data-i18n-text]').forEach(btn => {
const key = btn.getAttribute('data-i18n-text');
if (key) {
btn.textContent = i18n.t(key);
}
});
},
bindAuthFileActionEvents() {
const container = document.getElementById('auth-files-list');
if (!container) return;
const oldListener = container._authFileActionListener;
if (oldListener) {
container.removeEventListener('click', oldListener);
}
const listener = (e) => {
const button = e.target.closest('button[data-action]');
if (!button) return;
const action = button.dataset.action;
const actionsContainer = button.closest('.item-actions');
if (!actionsContainer) return;
const filename = actionsContainer.dataset.filename;
if (!filename) return;
switch (action) {
case 'showDetails':
this.showAuthFileDetails(filename);
break;
case 'download':
this.downloadAuthFile(filename);
break;
case 'delete':
this.deleteAuthFile(filename);
break;
}
};
container._authFileActionListener = listener;
container.addEventListener('click', listener);
},
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
},
openVertexFilePicker() {
const fileInput = document.getElementById('vertex-file-input');
if (fileInput) {
fileInput.click();
}
},
handleVertexFileSelection(event) {
const fileInput = event?.target;
const file = fileInput?.files?.[0] || null;
if (fileInput) {
fileInput.value = '';
}
if (file && !file.name.toLowerCase().endsWith('.json')) {
this.showNotification(i18n.t('vertex_import.file_required'), 'error');
this.vertexImportState.file = null;
this.updateVertexFileDisplay();
this.updateVertexImportButtonState();
return;
}
this.vertexImportState.file = file;
this.updateVertexFileDisplay(file ? file.name : '');
this.updateVertexImportButtonState();
},
updateVertexFileDisplay(filename = '') {
const displayInput = document.getElementById('vertex-file-display');
if (!displayInput) return;
displayInput.value = filename || '';
},
updateVertexImportButtonState() {
const importBtn = document.getElementById('vertex-import-btn');
if (!importBtn) return;
const disabled = !this.vertexImportState.file || this.vertexImportState.loading;
importBtn.disabled = disabled;
},
async importVertexCredential() {
if (!this.vertexImportState.file) {
this.showNotification(i18n.t('vertex_import.file_required'), 'error');
return;
}
const locationInput = document.getElementById('vertex-location');
const location = locationInput ? locationInput.value.trim() : '';
const formData = new FormData();
formData.append('file', this.vertexImportState.file, this.vertexImportState.file.name);
if (location) {
formData.append('location', location);
}
try {
this.vertexImportState.loading = true;
this.updateVertexImportButtonState();
const response = await this.apiClient.requestRaw('/vertex/import', {
method: 'POST',
body: formData
});
if (!response.ok) {
let errorMessage = `HTTP ${response.status}`;
try {
const errorData = await response.json();
errorMessage = errorData?.message || errorData?.error || errorMessage;
} catch (parseError) {
const text = await response.text();
if (text) {
errorMessage = text;
}
}
throw new Error(errorMessage);
}
const result = await response.json();
this.vertexImportState.result = result;
this.renderVertexImportResult(result);
this.showNotification(i18n.t('vertex_import.success'), 'success');
this.vertexImportState.file = null;
this.updateVertexFileDisplay('');
} catch (error) {
console.error('Vertex credential import failed:', error);
this.showNotification(`${i18n.t('notification.import_failed')}: ${error.message}`, 'error');
} finally {
this.vertexImportState.loading = false;
this.updateVertexImportButtonState();
const fileInput = document.getElementById('vertex-file-input');
if (fileInput) {
fileInput.value = '';
}
}
},
renderVertexImportResult(result = null) {
const container = document.getElementById('vertex-import-result');
const projectEl = document.getElementById('vertex-result-project');
const emailEl = document.getElementById('vertex-result-email');
const locationEl = document.getElementById('vertex-result-location');
const fileEl = document.getElementById('vertex-result-file');
if (!container || !projectEl || !emailEl || !locationEl || !fileEl) return;
if (!result) {
container.style.display = 'none';
this.vertexImportState.result = null;
return;
}
this.vertexImportState.result = result;
projectEl.textContent = result.project || '-';
emailEl.textContent = result.email || '-';
locationEl.textContent = result.location || '-';
fileEl.textContent = result.file || '-';
container.style.display = 'block';
},
showAuthFileDetails(filename, content) {
const file = (this.cachedAuthFiles || []).find(f => f && f.name === filename);
if (!file) return;
const stats = this.resolveAuthFileStats(file, this.authFileStatsCache || {});
const size = typeof file.size === 'number' ? this.formatFileSize(file.size) : '-';
const provider = file.provider || '-';
const type = file.type || '-';
const createdAt = file.created_at ? new Date(file.created_at).toLocaleString('zh-CN') : '-';
const jsonContent = content || JSON.stringify(file, null, 2);
// 使用独立的 JSON 弹窗样式,避免被通用 .modal 的 display:none 覆盖
const modalHtml = `
<div class="json-modal" id="json-modal">
<div class="json-modal-content">
<div class="json-modal-header">
<h3>${this.escapeHtml(filename)}</h3>
</div>
<div class="json-modal-body">
<pre class="json-content">${this.escapeHtml(jsonContent)}</pre>
</div>
<div class="json-modal-footer">
<button class="btn btn-secondary" data-action="copy">
<i class="fas fa-copy"></i>
${i18n.t('common.copy')}
</button>
<button class="btn btn-secondary" data-action="close">
${i18n.t('common.close')}
</button>
</div>
</div>
</div>
`;
const oldModal = document.getElementById('json-modal');
if (oldModal) {
oldModal.remove();
}
document.body.insertAdjacentHTML('beforeend', modalHtml);
const modal = document.getElementById('json-modal');
modal.addEventListener('click', (e) => {
if (e.target === modal) {
this.closeJsonModal();
}
});
this.bindJsonModalEvents(modal);
},
bindJsonModalEvents(modal) {
modal.addEventListener('click', (e) => {
const button = e.target.closest('button[data-action]');
if (!button) return;
const action = button.dataset.action;
switch (action) {
case 'copy':
this.copyJsonContent();
break;
case 'close':
this.closeJsonModal();
break;
}
});
},
closeJsonModal() {
const modal = document.getElementById('json-modal');
if (modal) {
modal.remove();
}
},
copyJsonContent() {
const jsonContent = document.querySelector('.json-content');
if (jsonContent) {
const text = jsonContent.textContent;
navigator.clipboard.writeText(text).then(() => {
this.showNotification('内容已复制到剪贴板', 'success');
}).catch(() => {
this.showNotification('复制失败', 'error');
});
}
},
async downloadAuthFile(filename) {
try {
const response = await this.apiClient.requestRaw(`/auth-files/download?name=${encodeURIComponent(filename)}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
window.URL.revokeObjectURL(url);
this.showNotification(i18n.t('auth_files.download_success'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.download_failed')}: ${error.message}`, 'error');
}
},
async deleteAuthFile(filename) {
if (!confirm(`${i18n.t('auth_files.delete_confirm')} "${filename}" 吗?`)) return;
try {
await this.makeRequest(`/auth-files?name=${encodeURIComponent(filename)}`, { method: 'DELETE' });
this.removeAuthFileElements([filename]);
this.clearCache();
await this.loadAuthFiles();
this.showNotification(i18n.t('auth_files.delete_success'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
}
},
async deleteAllAuthFiles() {
const filterType = (this.currentAuthFileFilter || 'all').toLowerCase();
const isFiltered = filterType !== 'all';
const typeLabel = this.generateDynamicTypeLabel(filterType);
const confirmMessage = isFiltered
? i18n.t('auth_files.delete_filtered_confirm').replace('{type}', typeLabel)
: i18n.t('auth_files.delete_all_confirm');
if (!confirm(confirmMessage)) return;
try {
if (!isFiltered) {
const response = await this.makeRequest('/auth-files?all=true', { method: 'DELETE' });
const currentNames = (this.cachedAuthFiles || []).map(file => file.name).filter(Boolean);
if (currentNames.length > 0) {
this.removeAuthFileElements(currentNames);
}
this.clearCache();
this.currentAuthFileFilter = 'all';
await this.loadAuthFiles();
this.showNotification(`${i18n.t('auth_files.delete_all_success')} ${response.deleted} ${i18n.t('auth_files.files_count')}`, 'success');
return;
}
const deletableFiles = (this.cachedAuthFiles || []).filter(file => {
if (!file || file.runtime_only) return false;
const fileType = (file.type || 'unknown').toLowerCase();
return fileType === filterType;
});
if (deletableFiles.length === 0) {
this.showNotification(i18n.t('auth_files.delete_filtered_none').replace('{type}', typeLabel), 'info');
return;
}
let success = 0;
let failed = 0;
const deletedNames = [];
for (const file of deletableFiles) {
try {
await this.makeRequest(`/auth-files?name=${encodeURIComponent(file.name)}`, { method: 'DELETE' });
success++;
deletedNames.push(file.name);
} catch (error) {
console.error('删除认证文件失败:', file?.name, error);
failed++;
}
}
if (deletedNames.length > 0) {
this.removeAuthFileElements(deletedNames);
}
this.clearCache();
this.currentAuthFileFilter = 'all';
await this.loadAuthFiles();
if (failed === 0) {
const successMsg = i18n.t('auth_files.delete_filtered_success')
.replace('{count}', success)
.replace('{type}', typeLabel);
this.showNotification(successMsg, 'success');
} else {
const warningMsg = i18n.t('auth_files.delete_filtered_partial')
.replace('{success}', success)
.replace('{failed}', failed)
.replace('{type}', typeLabel);
this.showNotification(warningMsg, 'warning');
}
} catch (error) {
this.showNotification(`${i18n.t('notification.delete_failed')}: ${error.message}`, 'error');
}
},
// 触发文件上传选择器
uploadAuthFile() {
const authFileInput = document.getElementById('auth-file-input');
if (authFileInput) {
authFileInput.click();
}
},
// 处理文件上传
async handleFileUpload(event) {
const file = event.target.files[0];
if (!file) return;
if (!file.name.endsWith('.json')) {
this.showNotification(i18n.t('auth_files.upload_error_json'), 'error');
event.target.value = '';
return;
}
try {
const formData = new FormData();
formData.append('file', file, file.name);
const response = await this.apiClient.requestRaw('/auth-files', {
method: 'POST',
body: formData
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || `HTTP ${response.status}`);
}
this.clearCache(); // 清除缓存
await this.loadAuthFiles();
this.showNotification(i18n.t('auth_files.upload_success'), 'success');
} catch (error) {
this.showNotification(`${i18n.t('notification.upload_failed')}: ${error.message}`, 'error');
} finally {
// 清空文件输入框,允许重复上传同一文件
event.target.value = '';
}
},
registerAuthFilesListeners() {
if (!this.events || typeof this.events.on !== 'function') {
return;
}
this.events.on('data:config-loaded', async (event) => {
const detail = event?.detail || {};
const keyStats = detail.keyStats || null;
try {
await this.loadAuthFiles(keyStats);
} catch (error) {
console.error('加载认证文件失败:', error);
}
});
}
};