支持白名单chat设置修改
This commit is contained in:
1 parent
603c07f95b
commit
06a7ef7e35
5 files changed
+238
No files matched your search
@@ -31,6 +31,7 @@ class ProviderController:
|
||||
self._app.route("/api/providers", methods=["GET"])(auth(self.get_providers))
|
||||
self._app.route("/api/providers", methods=["POST"])(auth(self.create_provider))
|
||||
self._app.route("/api/providers/fetch-models", methods=["GET"])(auth(self.fetch_models))
|
||||
self._app.route("/api/providers/chat-whitelist", methods=["PUT"])(auth(self.update_chat_whitelist))
|
||||
self._app.route("/api/providers/<string:name>", methods=["GET"])(auth(self.get_provider))
|
||||
self._app.route("/api/providers/<string:name>", methods=["PUT"])(auth(self.update_provider))
|
||||
self._app.route("/api/providers/<string:name>", methods=["DELETE"])(auth(self.delete_provider))
|
||||
@@ -113,3 +114,15 @@ class ProviderController:
|
||||
except Exception as exc:
|
||||
self._logger.error(f"Error fetching provider models: {exc}")
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
|
||||
def update_chat_whitelist(self) -> Response:
|
||||
try:
|
||||
payload = request.get_json(silent=True) or {}
|
||||
enabled = self._provider_service.update_chat_whitelist_enabled(payload.get("enabled"))
|
||||
self._logger.info("Chat whitelist updated: enabled=%s", enabled)
|
||||
return jsonify({"enabled": enabled})
|
||||
except ValueError as exc:
|
||||
return jsonify({"error": str(exc)}), 400
|
||||
except Exception as exc:
|
||||
self._logger.error(f"Error updating chat whitelist: {exc}")
|
||||
return jsonify({"error": str(exc)}), 500
|
||||
@@ -26,6 +26,67 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-md);
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card + .toolbar-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-copy {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-label {
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-status {
|
||||
color: var(--text-secondary);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card[data-enabled="true"] .toolbar-setting-status {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card[data-enabled="true"] {
|
||||
border-color: rgba(28, 164, 113, 0.22);
|
||||
background: rgba(28, 164, 113, 0.06);
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card[data-saving="true"] .toolbar-setting-status {
|
||||
color: var(--primary-active);
|
||||
}
|
||||
|
||||
.providers-page .toolbar-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -128,6 +189,20 @@
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .toolbar-setting-card {
|
||||
border-color: #313b47;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .toolbar-setting-card[data-enabled="true"] {
|
||||
border-color: rgba(46, 160, 104, 0.42);
|
||||
background: rgba(46, 160, 104, 0.1);
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .toolbar-setting-card[data-saving="true"] .toolbar-setting-status {
|
||||
color: #9bc7ff;
|
||||
}
|
||||
|
||||
.providers-page .providers-table tbody tr:nth-child(even) td {
|
||||
background: #fbfdff;
|
||||
}
|
||||
@@ -226,6 +301,58 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.providers-page .switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.providers-page .switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.providers-page .slider {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 999px;
|
||||
border: 1px solid #c8d7e6;
|
||||
background: #e7eef6;
|
||||
cursor: pointer;
|
||||
transition: background-color var(--transition), border-color var(--transition), opacity var(--transition);
|
||||
}
|
||||
|
||||
.providers-page .slider::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.16);
|
||||
transition: transform var(--transition), background-color var(--transition);
|
||||
}
|
||||
|
||||
.providers-page .switch input:checked + .slider {
|
||||
background: #e7f5ef;
|
||||
border-color: #b7dccf;
|
||||
}
|
||||
|
||||
.providers-page .switch input:checked + .slider::before {
|
||||
transform: translateX(20px);
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.providers-page .switch input:disabled + .slider {
|
||||
opacity: 0.72;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.providers-page .model-summary-bar {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
@@ -317,6 +444,15 @@
|
||||
background: #0d1117;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .slider {
|
||||
background: #222b36;
|
||||
border-color: #30363d;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .slider::before {
|
||||
background: #e8eef8;
|
||||
}
|
||||
|
||||
:root[data-theme="dark"] .providers-page .btn-edit {
|
||||
background: #1b2230;
|
||||
border-color: #30363d;
|
||||
@@ -621,6 +757,16 @@
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.providers-page .toolbar-setting-action {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.providers-page .provider-form-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,19 @@
|
||||
|
||||
{% block content %}
|
||||
<section class="providers-toolbar-section">
|
||||
<div class="toolbar-setting-card" id="chatWhitelistCard">
|
||||
<div class="toolbar-setting-copy">
|
||||
<span class="toolbar-setting-label">Chat 白名单</span>
|
||||
<span class="toolbar-setting-hint">控制 Provider Chat 接口是否启用白名单限制,启用后仅允许白名单访问的用户可访问Chat</span>
|
||||
</div>
|
||||
<div class="toolbar-setting-action">
|
||||
<span class="toolbar-setting-status" id="chatWhitelistStatus">已禁用</span>
|
||||
<label class="switch toolbar-switch" aria-label="切换 Chat 白名单">
|
||||
<input type="checkbox" id="chatWhitelistToggle" onchange="toggleChatWhitelist(this.checked)">
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolbar-card">
|
||||
<div class="toolbar-summary">
|
||||
<span class="toolbar-pill toolbar-pill-primary">共 <strong id="providerCount">0</strong> 个 Provider</span>
|
||||
@@ -124,11 +137,14 @@
|
||||
{% block page_scripts %}
|
||||
<script src="/static/js/ui-message.js"></script>
|
||||
<script>
|
||||
const initialChatWhitelistEnabled = {{ 'true' if chat_whitelist_enabled else 'false' }};
|
||||
let providers = [];
|
||||
let editingProviderName = null;
|
||||
let providerModalInstance = null;
|
||||
let modelDetailModalInstance = null;
|
||||
let activeModelProvider = null;
|
||||
let chatWhitelistEnabled = initialChatWhitelistEnabled;
|
||||
let chatWhitelistSaving = false;
|
||||
|
||||
function applyTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
@@ -163,6 +179,49 @@
|
||||
return decodeURIComponent(encodedName);
|
||||
}
|
||||
|
||||
function updateChatWhitelistUi() {
|
||||
const card = document.getElementById('chatWhitelistCard');
|
||||
const status = document.getElementById('chatWhitelistStatus');
|
||||
const toggle = document.getElementById('chatWhitelistToggle');
|
||||
if (!card || !status || !toggle) return;
|
||||
|
||||
card.dataset.enabled = chatWhitelistEnabled ? 'true' : 'false';
|
||||
card.dataset.saving = chatWhitelistSaving ? 'true' : 'false';
|
||||
toggle.checked = !!chatWhitelistEnabled;
|
||||
toggle.disabled = !!chatWhitelistSaving;
|
||||
status.textContent = chatWhitelistSaving
|
||||
? '保存中...'
|
||||
: (chatWhitelistEnabled ? '已启用' : '已禁用');
|
||||
}
|
||||
|
||||
async function toggleChatWhitelist(enabled) {
|
||||
const previous = chatWhitelistEnabled;
|
||||
chatWhitelistEnabled = !!enabled;
|
||||
chatWhitelistSaving = true;
|
||||
updateChatWhitelistUi();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/providers/chat-whitelist', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled: !!enabled })
|
||||
});
|
||||
const result = await response.json();
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || '更新 Chat 白名单失败');
|
||||
}
|
||||
chatWhitelistEnabled = !!result.enabled;
|
||||
showMessage(`Chat 白名单已${chatWhitelistEnabled ? '启用' : '禁用'}`, 'success');
|
||||
} catch (error) {
|
||||
console.error('更新 Chat 白名单失败:', error);
|
||||
chatWhitelistEnabled = previous;
|
||||
showMessage(error.message || '更新 Chat 白名单失败', 'error');
|
||||
} finally {
|
||||
chatWhitelistSaving = false;
|
||||
updateChatWhitelistUi();
|
||||
}
|
||||
}
|
||||
|
||||
function getModelPreview(models, limit = 6) {
|
||||
const items = Array.isArray(models) ? models : [];
|
||||
const visible = items.slice(0, limit);
|
||||
@@ -457,6 +516,7 @@
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initThemeToggle();
|
||||
initTooltips();
|
||||
updateChatWhitelistUi();
|
||||
const modalElement = document.getElementById('providerModal');
|
||||
providerModalInstance = new bootstrap.Modal(modalElement);
|
||||
modalElement.addEventListener('hidden.bs.modal', function() {
|
||||
|
||||
@@ -54,6 +54,7 @@ class WebController:
|
||||
return render_template(
|
||||
"providers.html",
|
||||
active_page="providers",
|
||||
chat_whitelist_enabled=self._config_manager.is_chat_whitelist_enabled(),
|
||||
current_username=self._get_current_username(),
|
||||
auth_enabled=self._auth_service.is_auth_enabled(),
|
||||
)
|
||||
|
||||
@@ -151,6 +151,24 @@ class ProviderService:
|
||||
"fetched_models": fetched_models,
|
||||
}
|
||||
|
||||
def update_chat_whitelist_enabled(self, enabled: Any) -> bool:
|
||||
parsed_enabled = self._parse_optional_bool(enabled)
|
||||
if parsed_enabled is None:
|
||||
raise ValueError("Whitelist enabled flag is required")
|
||||
|
||||
config = self._load_config()
|
||||
chat_config = config.get("chat")
|
||||
if chat_config is None:
|
||||
chat_config = {}
|
||||
config["chat"] = chat_config
|
||||
if not isinstance(chat_config, dict):
|
||||
raise ValueError("Config field 'chat' must be an object")
|
||||
|
||||
chat_config["whitelist_enabled"] = parsed_enabled
|
||||
self._write_config(config)
|
||||
self._reload_callback()
|
||||
return parsed_enabled
|
||||
|
||||
def _load_config(self) -> Dict[str, Any]:
|
||||
with open(self._config_path, "r", encoding="utf-8") as file:
|
||||
data = yaml.safe_load(file)
|
||||
|
||||
Reference in new issue
Block a user