mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
feat: add OAuth excluded models management with UI integration and internationalization support
This commit is contained in:
15
app.js
15
app.js
@@ -97,6 +97,10 @@ class CLIProxyManager {
|
|||||||
this.authFilesPageSizeKey = STORAGE_KEY_AUTH_FILES_PAGE_SIZE;
|
this.authFilesPageSizeKey = STORAGE_KEY_AUTH_FILES_PAGE_SIZE;
|
||||||
this.loadAuthFilePreferences();
|
this.loadAuthFilePreferences();
|
||||||
|
|
||||||
|
// OAuth 模型排除列表状态
|
||||||
|
this.oauthExcludedModels = {};
|
||||||
|
this._oauthExcludedLoading = false;
|
||||||
|
|
||||||
// Vertex AI credential import state
|
// Vertex AI credential import state
|
||||||
this.vertexImportState = {
|
this.vertexImportState = {
|
||||||
file: null,
|
file: null,
|
||||||
@@ -385,6 +389,17 @@ class CLIProxyManager {
|
|||||||
this.bindAuthFilesPageSizeControl();
|
this.bindAuthFilesPageSizeControl();
|
||||||
this.syncAuthFileControls();
|
this.syncAuthFileControls();
|
||||||
|
|
||||||
|
// OAuth 排除列表
|
||||||
|
const oauthExcludedAdd = document.getElementById('oauth-excluded-add');
|
||||||
|
const oauthExcludedRefresh = document.getElementById('oauth-excluded-refresh');
|
||||||
|
|
||||||
|
if (oauthExcludedAdd) {
|
||||||
|
oauthExcludedAdd.addEventListener('click', () => this.openOauthExcludedEditor());
|
||||||
|
}
|
||||||
|
if (oauthExcludedRefresh) {
|
||||||
|
oauthExcludedRefresh.addEventListener('click', () => this.loadOauthExcludedModels(true));
|
||||||
|
}
|
||||||
|
|
||||||
// Vertex AI credential import
|
// Vertex AI credential import
|
||||||
const vertexSelectFile = document.getElementById('vertex-select-file');
|
const vertexSelectFile = document.getElementById('vertex-select-file');
|
||||||
const vertexFileInput = document.getElementById('vertex-file-input');
|
const vertexFileInput = document.getElementById('vertex-file-input');
|
||||||
|
|||||||
68
i18n.js
68
i18n.js
@@ -306,6 +306,40 @@ const i18n = {
|
|||||||
'vertex_import.result_location': '区域',
|
'vertex_import.result_location': '区域',
|
||||||
'vertex_import.result_file': '存储文件',
|
'vertex_import.result_file': '存储文件',
|
||||||
|
|
||||||
|
// OAuth 排除模型
|
||||||
|
'oauth_excluded.title': 'OAuth 排除列表',
|
||||||
|
'oauth_excluded.description': '按提供商分列展示,点击卡片编辑或删除;支持 * 通配符,范围跟随上方的配置文件过滤标签。',
|
||||||
|
'oauth_excluded.add': '新增排除',
|
||||||
|
'oauth_excluded.add_title': '新增提供商排除列表',
|
||||||
|
'oauth_excluded.edit_title': '编辑 {provider} 的排除列表',
|
||||||
|
'oauth_excluded.refresh': '刷新',
|
||||||
|
'oauth_excluded.refreshing': '刷新中...',
|
||||||
|
'oauth_excluded.provider_label': '提供商',
|
||||||
|
'oauth_excluded.provider_auto': '跟随当前过滤',
|
||||||
|
'oauth_excluded.provider_placeholder': '例如 gemini-cli / openai',
|
||||||
|
'oauth_excluded.provider_hint': '默认选中当前筛选的提供商,也可直接输入或选择其他名称。',
|
||||||
|
'oauth_excluded.models_label': '排除的模型',
|
||||||
|
'oauth_excluded.models_placeholder': 'gpt-4.1-mini\n*-preview',
|
||||||
|
'oauth_excluded.models_hint': '逗号或换行分隔;留空保存将删除该提供商记录;支持 * 通配符。',
|
||||||
|
'oauth_excluded.save': '保存/更新',
|
||||||
|
'oauth_excluded.saving': '正在保存...',
|
||||||
|
'oauth_excluded.save_success': '排除列表已更新',
|
||||||
|
'oauth_excluded.save_failed': '更新排除列表失败',
|
||||||
|
'oauth_excluded.delete': '删除提供商',
|
||||||
|
'oauth_excluded.delete_confirm': '确定要删除 {provider} 的排除列表吗?',
|
||||||
|
'oauth_excluded.delete_success': '已删除该提供商的排除列表',
|
||||||
|
'oauth_excluded.delete_failed': '删除排除列表失败',
|
||||||
|
'oauth_excluded.deleting': '正在删除...',
|
||||||
|
'oauth_excluded.no_models': '未配置排除模型',
|
||||||
|
'oauth_excluded.model_count': '排除 {count} 个模型',
|
||||||
|
'oauth_excluded.list_empty_all': '暂无任何提供商的排除列表,点击“新增排除”创建。',
|
||||||
|
'oauth_excluded.list_empty_filtered': '当前筛选下没有排除项,点击“新增排除”添加。',
|
||||||
|
'oauth_excluded.disconnected': '请先连接服务器以查看排除列表',
|
||||||
|
'oauth_excluded.load_failed': '加载排除列表失败',
|
||||||
|
'oauth_excluded.provider_required': '请先填写提供商名称',
|
||||||
|
'oauth_excluded.scope_all': '当前范围:全局(显示所有提供商)',
|
||||||
|
'oauth_excluded.scope_provider': '当前范围:{provider}',
|
||||||
|
|
||||||
|
|
||||||
// Codex OAuth
|
// Codex OAuth
|
||||||
'auth_login.codex_oauth_title': 'Codex OAuth',
|
'auth_login.codex_oauth_title': 'Codex OAuth',
|
||||||
@@ -878,6 +912,40 @@ const i18n = {
|
|||||||
'vertex_import.result_location': 'Region',
|
'vertex_import.result_location': 'Region',
|
||||||
'vertex_import.result_file': 'Persisted file',
|
'vertex_import.result_file': 'Persisted file',
|
||||||
|
|
||||||
|
// OAuth excluded models
|
||||||
|
'oauth_excluded.title': 'OAuth Excluded Models',
|
||||||
|
'oauth_excluded.description': 'Per-provider exclusions are shown as cards; click edit to adjust. Wildcards * are supported and the scope follows the auth file filter.',
|
||||||
|
'oauth_excluded.add': 'Add Exclusion',
|
||||||
|
'oauth_excluded.add_title': 'Add provider exclusion',
|
||||||
|
'oauth_excluded.edit_title': 'Edit exclusions for {provider}',
|
||||||
|
'oauth_excluded.refresh': 'Refresh',
|
||||||
|
'oauth_excluded.refreshing': 'Refreshing...',
|
||||||
|
'oauth_excluded.provider_label': 'Provider',
|
||||||
|
'oauth_excluded.provider_auto': 'Follow current filter',
|
||||||
|
'oauth_excluded.provider_placeholder': 'e.g. gemini-cli',
|
||||||
|
'oauth_excluded.provider_hint': 'Defaults to the current filter; pick an existing provider or type a new name.',
|
||||||
|
'oauth_excluded.models_label': 'Models to exclude',
|
||||||
|
'oauth_excluded.models_placeholder': 'gpt-4.1-mini\n*-preview',
|
||||||
|
'oauth_excluded.models_hint': 'Separate by commas or new lines; saving an empty list removes that provider. * wildcards are supported.',
|
||||||
|
'oauth_excluded.save': 'Save/Update',
|
||||||
|
'oauth_excluded.saving': 'Saving...',
|
||||||
|
'oauth_excluded.save_success': 'Excluded models updated',
|
||||||
|
'oauth_excluded.save_failed': 'Failed to update excluded models',
|
||||||
|
'oauth_excluded.delete': 'Delete Provider',
|
||||||
|
'oauth_excluded.delete_confirm': 'Delete the exclusion list for {provider}?',
|
||||||
|
'oauth_excluded.delete_success': 'Exclusion list removed',
|
||||||
|
'oauth_excluded.delete_failed': 'Failed to delete exclusion list',
|
||||||
|
'oauth_excluded.deleting': 'Deleting...',
|
||||||
|
'oauth_excluded.no_models': 'No excluded models',
|
||||||
|
'oauth_excluded.model_count': '{count} models excluded',
|
||||||
|
'oauth_excluded.list_empty_all': 'No exclusions yet—use “Add Exclusion” to create one.',
|
||||||
|
'oauth_excluded.list_empty_filtered': 'No exclusions in this scope; click “Add Exclusion” to add.',
|
||||||
|
'oauth_excluded.disconnected': 'Connect to the server to view exclusions',
|
||||||
|
'oauth_excluded.load_failed': 'Failed to load exclusion list',
|
||||||
|
'oauth_excluded.provider_required': 'Please enter a provider first',
|
||||||
|
'oauth_excluded.scope_all': 'Scope: All providers',
|
||||||
|
'oauth_excluded.scope_provider': 'Scope: {provider}',
|
||||||
|
|
||||||
// Codex OAuth
|
// Codex OAuth
|
||||||
'auth_login.codex_oauth_title': 'Codex OAuth',
|
'auth_login.codex_oauth_title': 'Codex OAuth',
|
||||||
'auth_login.codex_oauth_button': 'Start Codex Login',
|
'auth_login.codex_oauth_button': 'Start Codex Login',
|
||||||
|
|||||||
25
index.html
25
index.html
@@ -557,6 +557,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OAuth 排除列表 -->
|
||||||
|
<div class="card" id="oauth-excluded-card">
|
||||||
|
<div class="card-header card-header-with-filter">
|
||||||
|
<div class="header-left">
|
||||||
|
<h3><i class="fas fa-ban"></i> <span data-i18n="oauth_excluded.title">OAuth 排除列表</span></h3>
|
||||||
|
<div class="oauth-excluded-scope" id="oauth-excluded-scope"></div>
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button id="oauth-excluded-refresh" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-sync-alt"></i> <span data-i18n="oauth_excluded.refresh">刷新</span>
|
||||||
|
</button>
|
||||||
|
<button id="oauth-excluded-add" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> <span data-i18n="oauth_excluded.add">新增排除</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<p class="form-hint" data-i18n="oauth_excluded.description">为 OAuth/文件凭据配置模型黑名单,支持通配符。</p>
|
||||||
|
<div id="oauth-excluded-status" class="form-hint subtle"></div>
|
||||||
|
<div id="oauth-excluded-list" class="oauth-excluded-list oauth-excluded-grid provider-list">
|
||||||
|
<div class="loading-placeholder" data-i18n="common.loading">正在加载...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Codex OAuth -->
|
<!-- Codex OAuth -->
|
||||||
<div class="card" id="codex-oauth-card">
|
<div class="card" id="codex-oauth-card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
|||||||
@@ -540,6 +540,7 @@ export const authFilesModule = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.refreshFilterButtonTexts();
|
this.refreshFilterButtonTexts();
|
||||||
|
this.renderOauthExcludedModels();
|
||||||
},
|
},
|
||||||
|
|
||||||
generateDynamicTypeLabel(type) {
|
generateDynamicTypeLabel(type) {
|
||||||
@@ -1031,6 +1032,469 @@ export const authFilesModule = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
normalizeOauthExcludedMap(payload = {}) {
|
||||||
|
const raw = (payload && (payload['oauth-excluded-models'] || payload.items)) || payload || {};
|
||||||
|
if (!raw || typeof raw !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const normalized = {};
|
||||||
|
Object.entries(raw).forEach(([provider, models]) => {
|
||||||
|
const key = typeof provider === 'string' ? provider.trim() : '';
|
||||||
|
if (!key) return;
|
||||||
|
const list = Array.isArray(models)
|
||||||
|
? models.map(item => String(item || '').trim()).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
normalized[key.toLowerCase()] = list;
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
},
|
||||||
|
|
||||||
|
getFilteredOauthExcludedMap(filterType = this.currentAuthFileFilter) {
|
||||||
|
const map = this.oauthExcludedModels || {};
|
||||||
|
if (!map || typeof map !== 'object') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const type = (filterType || 'all').toLowerCase();
|
||||||
|
if (type === 'all') {
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
const result = {};
|
||||||
|
Object.entries(map).forEach(([provider, models]) => {
|
||||||
|
if ((provider || '').toLowerCase() === type) {
|
||||||
|
result[provider] = models;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
findOauthExcludedEntry(provider) {
|
||||||
|
if (!provider || provider === 'all') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const normalized = provider.toLowerCase();
|
||||||
|
const map = this.oauthExcludedModels || {};
|
||||||
|
for (const [key, models] of Object.entries(map)) {
|
||||||
|
if ((key || '').toLowerCase() === normalized) {
|
||||||
|
return { provider: key, models: Array.isArray(models) ? models : [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
setOauthExcludedForm(provider = '', models = null) {
|
||||||
|
const providerSelect = document.getElementById('oauth-excluded-provider-select');
|
||||||
|
const modelsInput = document.getElementById('oauth-excluded-models');
|
||||||
|
|
||||||
|
const normalizedProvider = (provider || '').trim();
|
||||||
|
if (providerSelect) {
|
||||||
|
const options = Array.from(providerSelect.options || []);
|
||||||
|
let match = options.find(opt => (opt.value || '').toLowerCase() === normalizedProvider.toLowerCase());
|
||||||
|
if (!match && normalizedProvider) {
|
||||||
|
match = new Option(this.generateDynamicTypeLabel(normalizedProvider) || normalizedProvider, normalizedProvider);
|
||||||
|
providerSelect.appendChild(match);
|
||||||
|
}
|
||||||
|
if (normalizedProvider && match) {
|
||||||
|
providerSelect.value = match.value;
|
||||||
|
} else {
|
||||||
|
providerSelect.value = 'auto';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsInput && models !== null && models !== undefined) {
|
||||||
|
const list = Array.isArray(models) ? models : [];
|
||||||
|
modelsInput.value = list.map(item => item || '').join('\n');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
syncOauthExcludedFormWithFilter(overrideModels = false) {
|
||||||
|
const filterType = (this.currentAuthFileFilter || 'all').toLowerCase();
|
||||||
|
const entry = this.findOauthExcludedEntry(filterType);
|
||||||
|
|
||||||
|
if (filterType === 'all') {
|
||||||
|
if (overrideModels) {
|
||||||
|
if (entry) {
|
||||||
|
this.setOauthExcludedForm(entry.provider, entry.models);
|
||||||
|
} else {
|
||||||
|
this.setOauthExcludedForm('', '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrideModels) {
|
||||||
|
this.setOauthExcludedForm(filterType, entry ? entry.models : []);
|
||||||
|
} else {
|
||||||
|
this.setOauthExcludedForm(filterType);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getOauthExcludedProviderValue() {
|
||||||
|
const providerSelect = document.getElementById('oauth-excluded-provider-select');
|
||||||
|
const filterFallback = (this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all')
|
||||||
|
? this.currentAuthFileFilter
|
||||||
|
: '';
|
||||||
|
|
||||||
|
let selected = (providerSelect && providerSelect.value) ? providerSelect.value.trim() : '';
|
||||||
|
if (!selected || selected === 'auto') {
|
||||||
|
return filterFallback;
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshOauthProviderOptions() {
|
||||||
|
const providerSelect = document.getElementById('oauth-excluded-provider-select');
|
||||||
|
if (!providerSelect) return;
|
||||||
|
|
||||||
|
const allowedProviders = ['gemini-cli', 'vertex', 'aistudio', 'antigravity', 'claude', 'codex', 'qwen', 'iflow'];
|
||||||
|
const mapProviders = Object.keys(this.oauthExcludedModels || {});
|
||||||
|
const filterType = (this.currentAuthFileFilter || '').toLowerCase();
|
||||||
|
const providers = Array.from(new Set([...allowedProviders, ...mapProviders].filter(Boolean)));
|
||||||
|
|
||||||
|
const prevValue = providerSelect.value || 'auto';
|
||||||
|
|
||||||
|
providerSelect.innerHTML = '';
|
||||||
|
const addOption = (value, textKey, fallbackText = null) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = value;
|
||||||
|
if (textKey) {
|
||||||
|
opt.setAttribute('data-i18n-text', textKey);
|
||||||
|
opt.textContent = i18n.t(textKey);
|
||||||
|
} else {
|
||||||
|
opt.textContent = fallbackText || value;
|
||||||
|
}
|
||||||
|
providerSelect.appendChild(opt);
|
||||||
|
};
|
||||||
|
|
||||||
|
addOption('auto', 'oauth_excluded.provider_auto');
|
||||||
|
providers.sort((a, b) => a.localeCompare(b)).forEach(item => addOption(item, null, this.generateDynamicTypeLabel(item) || item));
|
||||||
|
|
||||||
|
const restoreValue = (() => {
|
||||||
|
if (prevValue && Array.from(providerSelect.options).some(opt => opt.value === prevValue)) {
|
||||||
|
return prevValue;
|
||||||
|
}
|
||||||
|
if (filterType && Array.from(providerSelect.options).some(opt => opt.value === filterType)) {
|
||||||
|
return filterType;
|
||||||
|
}
|
||||||
|
return 'auto';
|
||||||
|
})();
|
||||||
|
providerSelect.value = restoreValue;
|
||||||
|
},
|
||||||
|
|
||||||
|
parseOauthExcludedModelsInput(input = '') {
|
||||||
|
const tokens = (input || '').split(/[\n,]/).map(token => token.trim()).filter(Boolean);
|
||||||
|
const unique = [];
|
||||||
|
tokens.forEach(token => {
|
||||||
|
if (!unique.includes(token)) {
|
||||||
|
unique.push(token);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return unique;
|
||||||
|
},
|
||||||
|
|
||||||
|
openOauthExcludedEditor(provider = '', models = null) {
|
||||||
|
const modal = document.getElementById('modal');
|
||||||
|
const modalBody = document.getElementById('modal-body');
|
||||||
|
if (!modal || !modalBody) return;
|
||||||
|
|
||||||
|
const normalizedProvider = (provider || '').trim();
|
||||||
|
const fallbackProvider = normalizedProvider
|
||||||
|
|| ((this.currentAuthFileFilter && this.currentAuthFileFilter !== 'all') ? this.currentAuthFileFilter : '');
|
||||||
|
let targetModels = models;
|
||||||
|
|
||||||
|
if ((targetModels === null || targetModels === undefined) && fallbackProvider) {
|
||||||
|
const existing = this.findOauthExcludedEntry(fallbackProvider);
|
||||||
|
if (existing) {
|
||||||
|
targetModels = existing.models;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<h3>${fallbackProvider
|
||||||
|
? i18n.t('oauth_excluded.edit_title', { provider: this.generateDynamicTypeLabel(fallbackProvider) })
|
||||||
|
: i18n.t('oauth_excluded.add_title')
|
||||||
|
}</h3>
|
||||||
|
<div class="provider-item oauth-excluded-editor-card">
|
||||||
|
<div class="item-content">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oauth-excluded-provider-select" data-i18n="oauth_excluded.provider_label">${i18n.t('oauth_excluded.provider_label')}</label>
|
||||||
|
<select id="oauth-excluded-provider-select"></select>
|
||||||
|
<p class="form-hint" data-i18n="oauth_excluded.provider_hint">${i18n.t('oauth_excluded.provider_hint')}</p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oauth-excluded-models" data-i18n="oauth_excluded.models_label">${i18n.t('oauth_excluded.models_label')}</label>
|
||||||
|
<textarea id="oauth-excluded-models" rows="5" data-i18n-placeholder="oauth_excluded.models_placeholder" placeholder="${i18n.t('oauth_excluded.models_placeholder')}"></textarea>
|
||||||
|
<p class="form-hint" data-i18n="oauth_excluded.models_hint">${i18n.t('oauth_excluded.models_hint')}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn btn-primary" id="oauth-excluded-save">
|
||||||
|
<i class="fas fa-save"></i> ${i18n.t('oauth_excluded.save')}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" id="oauth-excluded-delete">
|
||||||
|
<i class="fas fa-trash"></i> ${i18n.t('oauth_excluded.delete')}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="manager.closeModal()">
|
||||||
|
${i18n.t('common.cancel')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.refreshOauthProviderOptions();
|
||||||
|
this.setOauthExcludedForm(fallbackProvider, targetModels != null ? targetModels : []);
|
||||||
|
this.showModal();
|
||||||
|
|
||||||
|
const saveBtn = document.getElementById('oauth-excluded-save');
|
||||||
|
if (saveBtn) {
|
||||||
|
saveBtn.onclick = () => this.saveOauthExcludedEntry();
|
||||||
|
}
|
||||||
|
const deleteBtn = document.getElementById('oauth-excluded-delete');
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.onclick = () => this.deleteOauthExcludedEntry();
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerSelect = document.getElementById('oauth-excluded-provider-select');
|
||||||
|
const syncDeleteState = () => {
|
||||||
|
if (deleteBtn) {
|
||||||
|
deleteBtn.disabled = !this.getOauthExcludedProviderValue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (providerSelect) {
|
||||||
|
providerSelect.addEventListener('change', syncDeleteState);
|
||||||
|
}
|
||||||
|
syncDeleteState();
|
||||||
|
this.updateOauthExcludedButtonsState(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
buildOauthExcludedItem(provider, models = []) {
|
||||||
|
const providerLabel = this.generateDynamicTypeLabel(provider) || provider;
|
||||||
|
const normalizedModels = Array.isArray(models) ? models.filter(Boolean) : [];
|
||||||
|
const tags = normalizedModels.length
|
||||||
|
? normalizedModels.map(model => `<span class="provider-model-tag"><span class="model-name">${this.escapeHtml(String(model))}</span></span>`).join('')
|
||||||
|
: `<span class="oauth-excluded-empty">${i18n.t('oauth_excluded.no_models')}</span>`;
|
||||||
|
const modelCount = normalizedModels.length;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="provider-item oauth-excluded-card" data-provider="${this.escapeHtml(provider)}">
|
||||||
|
<div class="item-content">
|
||||||
|
<div class="item-title">${this.escapeHtml(providerLabel)}</div>
|
||||||
|
<div class="item-meta">
|
||||||
|
<span class="item-subtitle">
|
||||||
|
${modelCount > 0
|
||||||
|
? i18n.t('oauth_excluded.model_count', { count: modelCount })
|
||||||
|
: i18n.t('oauth_excluded.no_models')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="provider-models oauth-excluded-tags">
|
||||||
|
${tags}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-actions">
|
||||||
|
<button class="btn btn-secondary" data-action="edit" data-provider="${this.escapeHtml(provider)}">
|
||||||
|
<i class="fas fa-pen"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" data-action="delete" data-provider="${this.escapeHtml(provider)}">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderOauthExcludedModels(filterType = this.currentAuthFileFilter) {
|
||||||
|
const container = document.getElementById('oauth-excluded-list');
|
||||||
|
const scopeEl = document.getElementById('oauth-excluded-scope');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const currentType = (filterType || 'all').toLowerCase();
|
||||||
|
const map = this.getFilteredOauthExcludedMap(currentType);
|
||||||
|
const providers = Object.keys(map || {});
|
||||||
|
|
||||||
|
if (scopeEl) {
|
||||||
|
const label = currentType === 'all'
|
||||||
|
? i18n.t('oauth_excluded.scope_all')
|
||||||
|
: i18n.t('oauth_excluded.scope_provider', { provider: this.generateDynamicTypeLabel(currentType) });
|
||||||
|
scopeEl.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isConnected) {
|
||||||
|
container.innerHTML = `<div class="oauth-excluded-empty">${i18n.t('oauth_excluded.disconnected')}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._oauthExcludedLoading) {
|
||||||
|
container.innerHTML = `<div class="loading-placeholder">${i18n.t('common.loading')}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!providers.length) {
|
||||||
|
const emptyKey = currentType === 'all'
|
||||||
|
? 'oauth_excluded.list_empty_all'
|
||||||
|
: 'oauth_excluded.list_empty_filtered';
|
||||||
|
container.innerHTML = `<div class="oauth-excluded-empty">${i18n.t(emptyKey)}</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsHtml = providers
|
||||||
|
.sort((a, b) => a.localeCompare(b))
|
||||||
|
.map(provider => this.buildOauthExcludedItem(provider, map[provider]))
|
||||||
|
.join('');
|
||||||
|
container.innerHTML = itemsHtml;
|
||||||
|
this.refreshOauthProviderOptions();
|
||||||
|
this.bindOauthExcludedActionEvents();
|
||||||
|
},
|
||||||
|
|
||||||
|
bindOauthExcludedActionEvents() {
|
||||||
|
const container = document.getElementById('oauth-excluded-list');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (container._oauthExcludedListener) {
|
||||||
|
container.removeEventListener('click', container._oauthExcludedListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
const listener = (event) => {
|
||||||
|
const button = event.target.closest('button[data-action]');
|
||||||
|
if (!button || !container.contains(button)) return;
|
||||||
|
const provider = button.dataset.provider;
|
||||||
|
if (!provider) return;
|
||||||
|
|
||||||
|
const entry = this.findOauthExcludedEntry(provider);
|
||||||
|
if (button.dataset.action === 'edit') {
|
||||||
|
this.openOauthExcludedEditor(provider, entry ? entry.models : []);
|
||||||
|
} else if (button.dataset.action === 'delete') {
|
||||||
|
this.deleteOauthExcludedEntry(provider);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container._oauthExcludedListener = listener;
|
||||||
|
container.addEventListener('click', listener);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateOauthExcludedButtonsState(isLoading = false) {
|
||||||
|
const refreshBtn = document.getElementById('oauth-excluded-refresh');
|
||||||
|
const saveBtn = document.getElementById('oauth-excluded-save');
|
||||||
|
const deleteBtn = document.getElementById('oauth-excluded-delete');
|
||||||
|
const addBtn = document.getElementById('oauth-excluded-add');
|
||||||
|
const disabled = isLoading || !this.isConnected;
|
||||||
|
[refreshBtn, saveBtn, deleteBtn, addBtn].forEach(btn => {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = disabled;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setOauthExcludedStatus(message = '') {
|
||||||
|
const statusEl = document.getElementById('oauth-excluded-status');
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = message || '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadOauthExcludedModels(forceRefresh = false) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.renderOauthExcludedModels();
|
||||||
|
this.updateOauthExcludedButtonsState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._oauthExcludedLoading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._oauthExcludedLoading = true;
|
||||||
|
this.updateOauthExcludedButtonsState(true);
|
||||||
|
this.setOauthExcludedStatus(i18n.t('oauth_excluded.refreshing'));
|
||||||
|
this.renderOauthExcludedModels();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await this.makeRequest('/oauth-excluded-models');
|
||||||
|
this.oauthExcludedModels = this.normalizeOauthExcludedMap(data);
|
||||||
|
this.refreshOauthProviderOptions();
|
||||||
|
this.setOauthExcludedStatus('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 OAuth 排除列表失败:', error);
|
||||||
|
const message = `${i18n.t('oauth_excluded.load_failed')}: ${error.message}`;
|
||||||
|
this.setOauthExcludedStatus(message);
|
||||||
|
this.showNotification(message, 'error');
|
||||||
|
} finally {
|
||||||
|
this._oauthExcludedLoading = false;
|
||||||
|
this.updateOauthExcludedButtonsState(false);
|
||||||
|
this.renderOauthExcludedModels();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveOauthExcludedEntry() {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.showNotification(i18n.t('notification.connection_required'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modelsInput = document.getElementById('oauth-excluded-models');
|
||||||
|
if (!modelsInput) return;
|
||||||
|
|
||||||
|
const providerValue = this.getOauthExcludedProviderValue();
|
||||||
|
if (!providerValue) {
|
||||||
|
this.showNotification(i18n.t('oauth_excluded.provider_required'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = this.parseOauthExcludedModelsInput(modelsInput.value);
|
||||||
|
this.updateOauthExcludedButtonsState(true);
|
||||||
|
this.setOauthExcludedStatus(i18n.t('oauth_excluded.saving'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.makeRequest('/oauth-excluded-models', {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({
|
||||||
|
provider: providerValue,
|
||||||
|
models
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const successKey = models.length === 0 ? 'oauth_excluded.delete_success' : 'oauth_excluded.save_success';
|
||||||
|
this.showNotification(i18n.t(successKey), 'success');
|
||||||
|
this.clearCache('oauth-excluded-models');
|
||||||
|
await this.loadOauthExcludedModels(true);
|
||||||
|
this.closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification(`${i18n.t('oauth_excluded.save_failed')}: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this.setOauthExcludedStatus('');
|
||||||
|
this.updateOauthExcludedButtonsState(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteOauthExcludedEntry(providerOverride = null) {
|
||||||
|
if (!this.isConnected) {
|
||||||
|
this.showNotification(i18n.t('notification.connection_required'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const providerValue = (providerOverride || this.getOauthExcludedProviderValue() || '').trim();
|
||||||
|
|
||||||
|
if (!providerValue) {
|
||||||
|
this.showNotification(i18n.t('oauth_excluded.provider_required'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm(i18n.t('oauth_excluded.delete_confirm', { provider: providerValue }))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateOauthExcludedButtonsState(true);
|
||||||
|
this.setOauthExcludedStatus(i18n.t('oauth_excluded.deleting'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.makeRequest(`/oauth-excluded-models?provider=${encodeURIComponent(providerValue)}`, { method: 'DELETE' });
|
||||||
|
this.showNotification(i18n.t('oauth_excluded.delete_success'), 'success');
|
||||||
|
this.clearCache('oauth-excluded-models');
|
||||||
|
await this.loadOauthExcludedModels(true);
|
||||||
|
this.closeModal();
|
||||||
|
} catch (error) {
|
||||||
|
this.showNotification(`${i18n.t('oauth_excluded.delete_failed')}: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
this.setOauthExcludedStatus('');
|
||||||
|
this.updateOauthExcludedButtonsState(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
registerAuthFilesListeners() {
|
registerAuthFilesListeners() {
|
||||||
if (!this.events || typeof this.events.on !== 'function') {
|
if (!this.events || typeof this.events.on !== 'function') {
|
||||||
return;
|
return;
|
||||||
@@ -1043,6 +1507,21 @@ export const authFilesModule = {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载认证文件失败:', error);
|
console.error('加载认证文件失败:', error);
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
await this.loadOauthExcludedModels(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载 OAuth 排除列表失败:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.events.on('connection:status-changed', (event) => {
|
||||||
|
const detail = event?.detail || {};
|
||||||
|
this.updateOauthExcludedButtonsState(false);
|
||||||
|
if (detail.isConnected) {
|
||||||
|
this.loadOauthExcludedModels(true);
|
||||||
|
} else {
|
||||||
|
this.renderOauthExcludedModels();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ export const loginModule = {
|
|||||||
this.stopStatusUpdateTimer();
|
this.stopStatusUpdateTimer();
|
||||||
this.resetVersionInfo();
|
this.resetVersionInfo();
|
||||||
this.setManagementKey('', { persist: false });
|
this.setManagementKey('', { persist: false });
|
||||||
|
this.oauthExcludedModels = {};
|
||||||
|
this._oauthExcludedLoading = false;
|
||||||
|
if (typeof this.renderOauthExcludedModels === 'function') {
|
||||||
|
this.renderOauthExcludedModels('all');
|
||||||
|
}
|
||||||
|
|
||||||
localStorage.removeItem('isLoggedIn');
|
localStorage.removeItem('isLoggedIn');
|
||||||
secureStorage.removeItem('managementKey');
|
secureStorage.removeItem('managementKey');
|
||||||
|
|||||||
65
styles.css
65
styles.css
@@ -1748,6 +1748,71 @@ input:checked+.slider:before {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-scope {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-list {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-card .provider-models {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-card .item-actions {
|
||||||
|
top: 14px;
|
||||||
|
right: 14px;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-editor-card .item-content {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-editor-card textarea {
|
||||||
|
min-height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oauth-excluded-empty {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
/* 认证文件工具栏 */
|
/* 认证文件工具栏 */
|
||||||
.auth-file-toolbar {
|
.auth-file-toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Reference in New Issue
Block a user