From 2225a40bbe5f33a37aea33c3b0388efa7faa471e Mon Sep 17 00:00:00 2001 From: snaily Date: Fri, 2 May 2025 22:49:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20Gemini=20=E5=AE=89?= =?UTF-8?q?=E5=85=A8=E8=AE=BE=E7=BD=AE=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 `SAFETY_SETTINGS` 配置项,允许用户通过环境变量或数据库配置 Gemini 模型的安全过滤级别。 - 更新后端服务 (`config.py`, `constants.py`, `gemini_routes.py`, `openai_routes.py`, `openai_chat_service.py`, `api_client.py`, `model_service.py`) 以支持和传递 `safety_settings` 参数。 - 在配置编辑器前端 (`config_editor.js`, `config_editor.html`) 添加了用于管理安全设置的用户界面。 - 将模型获取逻辑 (`model_service.py`, `api_client.py`) 改为异步。 - 优化 Service Worker (`service-worker.js`) 的缓存策略为 "cache then network"。 Bump version to 2.1.2 --- .env.example | 4 + app/config/config.py | 29 ++++- app/core/constants.py | 18 +++- app/router/gemini_routes.py | 13 +-- app/router/openai_routes.py | 6 +- app/service/chat/openai_chat_service.py | 16 +-- app/service/client/api_client.py | 27 ++++- app/service/model/model_service.py | 61 +++++------ app/static/js/config_editor.js | 137 +++++++++++++++++++++++- app/static/service-worker.js | 28 +++-- app/templates/config_editor.html | 14 +++ 11 files changed, 282 insertions(+), 71 deletions(-) diff --git a/.env.example b/.env.example index add0e86..bf1f74a 100644 --- a/.env.example +++ b/.env.example @@ -47,3 +47,7 @@ STREAM_CHUNK_SIZE=5 # 日志级别 (debug, info, warning, error, critical),默认为 info LOG_LEVEL=info ########################################################################## + +# 安全设置 (JSON 字符串格式) +# 注意:这里的示例值可能需要根据实际模型支持情况调整 +SAFETY_SETTINGS='[{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "BLOCK_NONE"}, {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "BLOCK_NONE"}]' diff --git a/app/config/config.py b/app/config/config.py index c6ae5da..466f2fc 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -9,7 +9,7 @@ from pydantic import ValidationError from pydantic_settings import BaseSettings from sqlalchemy import insert, update, select -from app.core.constants import API_VERSION, DEFAULT_CREATE_IMAGE_MODEL, DEFAULT_FILTER_MODELS, DEFAULT_MODEL, DEFAULT_STREAM_CHUNK_SIZE, DEFAULT_STREAM_LONG_TEXT_THRESHOLD, DEFAULT_STREAM_MAX_DELAY, DEFAULT_STREAM_MIN_DELAY, DEFAULT_STREAM_SHORT_TEXT_THRESHOLD, DEFAULT_TIMEOUT, MAX_RETRIES +from app.core.constants import API_VERSION, DEFAULT_CREATE_IMAGE_MODEL, DEFAULT_FILTER_MODELS, DEFAULT_MODEL, DEFAULT_SAFETY_SETTINGS, DEFAULT_STREAM_CHUNK_SIZE, DEFAULT_STREAM_LONG_TEXT_THRESHOLD, DEFAULT_STREAM_MAX_DELAY, DEFAULT_STREAM_MIN_DELAY, DEFAULT_STREAM_SHORT_TEXT_THRESHOLD, DEFAULT_TIMEOUT, MAX_RETRIES from app.log.logger import Logger @@ -69,6 +69,7 @@ class Settings(BaseSettings): # 日志配置 LOG_LEVEL: str = "INFO" # 默认日志级别 + SAFETY_SETTINGS: List[Dict[str, str]] = DEFAULT_SAFETY_SETTINGS # 新增:安全设置 def __init__(self, **kwargs): super().__init__(**kwargs) @@ -121,6 +122,32 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: # Log other errors (ValueError, TypeError) or JSON errors without single quotes logger.error(f"Could not parse '{db_value}' as Dict[str, float] for key '{key}': {e1}. Returning empty dict.") return parsed_dict # Return the parsed dict or an empty one if all attempts fail + # 处理 List[Dict[str, str]] + elif target_type == List[Dict[str, str]]: + try: + parsed = json.loads(db_value) + if isinstance(parsed, list): + # 验证列表中的每个元素是否为字典,并且键和值都是字符串 + valid = all( + isinstance(item, dict) and + all(isinstance(k, str) for k in item.keys()) and + all(isinstance(v, str) for v in item.values()) + for item in parsed + ) + if valid: + return parsed + else: + logger.warning(f"Invalid structure in List[Dict[str, str]] for key '{key}'. Value: {db_value}") + return [] # 或者返回默认值?这里返回空列表 + else: + logger.warning(f"Parsed DB value for key '{key}' is not a list type. Value: {db_value}") + return [] + except json.JSONDecodeError: + logger.error(f"Could not parse '{db_value}' as JSON for List[Dict[str, str]] for key '{key}'. Returning empty list.") + return [] + except Exception as e: + logger.error(f"Error parsing List[Dict[str, str]] for key '{key}': {e}. Value: {db_value}. Returning empty list.") + return [] # 处理 bool elif target_type == bool: return db_value.lower() in ('true', '1', 'yes', 'on') diff --git a/app/core/constants.py b/app/core/constants.py index 3775cdd..21d72aa 100644 --- a/app/core/constants.py +++ b/app/core/constants.py @@ -60,4 +60,20 @@ VIDEO_FORMAT_TO_MIMETYPE = { "mov": "video/quicktime", "avi": "video/x-msvideo", "webm": "video/webm", -} \ No newline at end of file +} + +GEMINI_2_FLASH_EXP_SAFETY_SETTINGS = [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"}, + ] + +DEFAULT_SAFETY_SETTINGS = [ + {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, + {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}, + ] \ No newline at end of file diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index 66a4daa..5512a7f 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -46,10 +46,6 @@ async def list_models( ): """获取可用的 Gemini 模型列表,并根据配置添加衍生模型(搜索、图像、非思考)。""" operation_name = "list_gemini_models" - # 注意:此路由的错误处理相对复杂,涉及模型查找和修改, - # 使用通用错误处理可能隐藏部分逻辑错误。暂时保留原有结构, - # 但如果需要更统一的处理,可以将内部逻辑封装并应用 handle_route_errors。 - # 这里仅添加日志分隔符。 logger.info("-" * 50 + operation_name + "-" * 50) logger.info("Handling Gemini models list request") @@ -59,8 +55,7 @@ async def list_models( raise HTTPException(status_code=503, detail="No valid API keys available to fetch models.") logger.info(f"Using API key: {api_key}") - # 假设 get_gemini_models 是同步的,如果不是需要 await - models_data = model_service.get_gemini_models(api_key) + models_data =await model_service.get_gemini_models(api_key) if not models_data or "models" not in models_data: raise HTTPException(status_code=500, detail="Failed to fetch base models list.") @@ -76,7 +71,7 @@ async def list_models( item["name"] = f"models/{base_name}{suffix}" display_name = f'{item.get("displayName", base_name)}{display_suffix}' item["displayName"] = display_name - item["description"] = display_name # 使用 display_name 作为描述 + item["description"] = display_name models_json["models"].append(item) # 添加衍生模型 @@ -120,7 +115,7 @@ async def generate_content( logger.debug(f"Request: \n{request.model_dump_json(indent=2)}") logger.info(f"Using API key: {api_key}") - if not model_service.check_model_support(model_name): + if not await model_service.check_model_support(model_name): raise HTTPException(status_code=400, detail=f"Model {model_name} is not supported") response = await chat_service.generate_content( @@ -150,7 +145,7 @@ async def stream_generate_content( logger.debug(f"Request: \n{request.model_dump_json(indent=2)}") logger.info(f"Using API key: {api_key}") - if not model_service.check_model_support(model_name): + if not await model_service.check_model_support(model_name): raise HTTPException(status_code=400, detail=f"Model {model_name} is not supported") response_stream = chat_service.stream_generate_content( diff --git a/app/router/openai_routes.py b/app/router/openai_routes.py index 7f81cf2..39c4dbc 100644 --- a/app/router/openai_routes.py +++ b/app/router/openai_routes.py @@ -54,9 +54,7 @@ async def list_models( logger.info("Handling models list request") api_key = await key_manager.get_first_valid_key() logger.info(f"Using API key: {api_key}") - # 注意:这里假设 model_service.get_gemini_openai_models 是同步函数 - # 如果它是异步的,需要 await - return model_service.get_gemini_openai_models(api_key) + return await model_service.get_gemini_openai_models(api_key) @router.post("/v1/chat/completions") @@ -83,7 +81,7 @@ async def chat_completion( logger.info(f"Using API key: {current_api_key}") # 检查模型支持性应在错误处理块内,以便捕获并记录错误 - if not model_service.check_model_support(request.model): + if not await model_service.check_model_support(request.model): # 使用 HTTPException,会被 handle_route_errors 捕获并记录 raise HTTPException( status_code=400, detail=f"Model {request.model} is not supported" diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index 5cf07c3..6fbf0e7 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -102,20 +102,8 @@ def _get_safety_settings(model: str) -> List[Dict[str, str]]: # and "gemini-2.0-pro-exp" not in model # ): if model == "gemini-2.0-flash-exp": - return [ - {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "OFF"}, - ] - return [ - {"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold": "OFF"}, - {"category": "HARM_CATEGORY_CIVIC_INTEGRITY", "threshold": "BLOCK_NONE"}, - ] + return settings.GEMINI_2_FLASH_EXP_SAFETY_SETTINGS + return settings.SAFETY_SETTINGS def _build_payload( diff --git a/app/service/client/api_client.py b/app/service/client/api_client.py index d5ac785..662fd3f 100644 --- a/app/service/client/api_client.py +++ b/app/service/client/api_client.py @@ -1,6 +1,6 @@ # app/services/chat/api_client.py -from typing import Dict, Any, AsyncGenerator +from typing import Dict, Any, AsyncGenerator, Optional import httpx import random from abc import ABC, abstractmethod @@ -40,6 +40,31 @@ class GeminiApiClient(ApiClient): model = model[:-20] return model + async def get_models(self, api_key: str) -> Optional[Dict[str, Any]]: + """获取可用的 Gemini 模型列表""" + timeout = httpx.Timeout(timeout=5) + + proxy_to_use = None + if settings.PROXIES: + proxy_to_use = random.choice(settings.PROXIES) + logger.info(f"Using proxy for getting models: {proxy_to_use}") + + async with httpx.AsyncClient(timeout=timeout, proxy=proxy_to_use) as client: + url = f"{self.base_url}/models?key={api_key}" + try: + response = await client.get(url) + response.raise_for_status() # 如果状态码不是 2xx,则引发 HTTPStatusError + return response.json() + except httpx.HTTPStatusError as e: + logger.error(f"获取模型列表失败: {e.response.status_code}") + logger.error(e.response.text) + # 返回 None 而不是抛出异常,以便上层处理 + return None + except httpx.RequestError as e: + logger.error(f"请求模型列表失败: {e}") + # 返回 None 而不是抛出异常 + return None + async def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]: timeout = httpx.Timeout(self.timeout, read=self.timeout) model = self._get_real_model(model) diff --git a/app/service/model/model_service.py b/app/service/model/model_service.py index 873a4da..16755d4 100644 --- a/app/service/model/model_service.py +++ b/app/service/model/model_service.py @@ -1,50 +1,47 @@ from datetime import datetime, timezone from typing import Any, Dict, Optional -import requests - from app.config.config import settings from app.log.logger import get_model_logger +from app.service.client.api_client import GeminiApiClient logger = get_model_logger() class ModelService: - def get_gemini_models(self, api_key: str) -> Optional[Dict[str, Any]]: - url = f"{settings.BASE_URL}/models?key={api_key}" + async def get_gemini_models(self, api_key: str) -> Optional[Dict[str, Any]]: + """使用 GeminiApiClient 获取并过滤模型列表""" + api_client = GeminiApiClient(base_url=settings.BASE_URL) # 实例化客户端 + gemini_models = await api_client.get_models(api_key) - try: - response = requests.get(url) - if response.status_code == 200: - gemini_models = response.json() - - filtered_models_list = [] - for model in gemini_models.get("models", []): - model_id = model["name"].split("/")[-1] - if model_id not in settings.FILTERED_MODELS: - filtered_models_list.append(model) - else: - logger.debug(f"Filtered out model: {model_id}") - - gemini_models["models"] = filtered_models_list - return gemini_models - else: - logger.error(f"Error: {response.status_code}") - logger.error(response.text) - return None - except requests.RequestException as e: - logger.error(f"Request failed: {e}") + if gemini_models is None: + logger.error("从 API 客户端获取模型列表失败。") return None - def get_gemini_openai_models(self, api_key: str) -> Optional[Dict[str, Any]]: try: - gemini_models = self.get_gemini_models(api_key) - return self.convert_to_openai_models_format(gemini_models) - except requests.RequestException as e: - logger.error(f"Request failed: {e}") + filtered_models_list = [] + for model in gemini_models.get("models", []): + model_id = model["name"].split("/")[-1] + if model_id not in settings.FILTERED_MODELS: + filtered_models_list.append(model) + else: + logger.debug(f"Filtered out model: {model_id}") + + gemini_models["models"] = filtered_models_list + return gemini_models + except Exception as e: + logger.error(f"处理模型列表时出错: {e}") return None - def convert_to_openai_models_format( + async def get_gemini_openai_models(self, api_key: str) -> Optional[Dict[str, Any]]: + """获取 Gemini 模型并转换为 OpenAI 格式""" + gemini_models = await self.get_gemini_models(api_key) + if gemini_models is None: + return None + + return await self.convert_to_openai_models_format(gemini_models) + + async def convert_to_openai_models_format( self, gemini_models: Dict[str, Any] ) -> Dict[str, Any]: openai_format = {"object": "list", "data": [], "success": True} @@ -81,7 +78,7 @@ class ModelService: openai_format["data"].append(image_model) return openai_format - def check_model_support(self, model: str) -> bool: + async def check_model_support(self, model: str) -> bool: if not model or not isinstance(model, str): return False diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 0d45dca..136eb5f 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -1,3 +1,7 @@ +// 将需要在外部函数访问的 DOM 元素移到外部 +const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container'); +const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); + document.addEventListener('DOMContentLoaded', function() { // 初始化配置 initConfig(); @@ -85,6 +89,7 @@ document.addEventListener('DOMContentLoaded', function() { const cancelResetBtn = document.getElementById('cancelResetBtn'); const confirmResetBtn = document.getElementById('confirmResetBtn'); // --- 结束:新增 --- + // const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container'); // Moved outside // 打开模态框 @@ -297,7 +302,7 @@ document.addEventListener('DOMContentLoaded', function() { // --- 结束:思考模型预算映射相关 --- // 添加事件委托,处理动态添加的 THINKING_MODELS 输入框的 input 事件 - const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); + // const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); // Moved outside if (thinkingModelsContainer) { thinkingModelsContainer.addEventListener('input', function(event) { if (event.target && event.target.classList.contains('array-input') && event.target.closest('.array-item[data-model-id]')) { @@ -311,6 +316,12 @@ document.addEventListener('DOMContentLoaded', function() { }); } + // --- 新增:安全设置相关 --- + const addSafetySettingBtn = document.getElementById('addSafetySettingBtn'); + if (addSafetySettingBtn) { + addSafetySettingBtn.addEventListener('click', () => addSafetySettingItem()); + } + // --- 结束:安全设置相关 --- }); // <-- DOMContentLoaded 结束括号 @@ -367,7 +378,11 @@ async function initConfig() { if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) { config.THINKING_BUDGET_MAP = {}; // 默认为空对象 } - // --- 结束:处理新字段的默认值 --- + // --- 新增:处理 SAFETY_SETTINGS 默认值 --- + if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) { + config.SAFETY_SETTINGS = []; // 默认为空数组 + } + // --- 结束:处理 SAFETY_SETTINGS 默认值 --- populateForm(config); @@ -506,6 +521,24 @@ function populateForm(config) { if (uploadProvider) { toggleProviderConfig(uploadProvider.value); } + + // --- 新增:填充 SAFETY_SETTINGS --- + let safetyItemsAdded = false; + if (safetySettingsContainer && Array.isArray(config.SAFETY_SETTINGS)) { + config.SAFETY_SETTINGS.forEach(setting => { + if (setting && typeof setting === 'object' && setting.category && setting.threshold) { + addSafetySettingItem(setting.category, setting.threshold); + safetyItemsAdded = true; + } else { + console.warn("Invalid safety setting item found:", setting); + } + }); + } + // 如果没有添加任何安全设置项,则显示占位符 + if (safetySettingsContainer && !safetyItemsAdded) { + safetySettingsContainer.innerHTML = '
定义模型的安全过滤阈值。
'; + } + // --- 结束:填充 SAFETY_SETTINGS --- } // --- 新增:处理批量添加 API Key 的逻辑 --- @@ -963,6 +996,23 @@ function collectFormData() { } // --- 结束:处理 THINKING_BUDGET_MAP --- + // --- 新增:处理 SAFETY_SETTINGS --- + if (safetySettingsContainer) { + formData['SAFETY_SETTINGS'] = []; + const settingItems = safetySettingsContainer.querySelectorAll('.safety-setting-item'); + settingItems.forEach(item => { + const categorySelect = item.querySelector('.safety-category-select'); + const thresholdSelect = item.querySelector('.safety-threshold-select'); + if (categorySelect && thresholdSelect && categorySelect.value && thresholdSelect.value) { + formData['SAFETY_SETTINGS'].push({ + category: categorySelect.value, + threshold: thresholdSelect.value + }); + } + }); + } + // --- 结束:处理 SAFETY_SETTINGS --- + return formData; } @@ -1166,3 +1216,86 @@ function addBudgetMapItemWithValue(mapKey, mapValue, modelId) { createAndAppendBudgetMapItem(mapKey, mapValue, modelId); } /* --- 结束:(addBudgetMapItemWithValue 已弃用) --- */ + + +// --- 新增:添加安全设置项的函数 --- +function addSafetySettingItem(category = '', threshold = '') { + const container = document.getElementById('SAFETY_SETTINGS_container'); + if (!container) { + console.error("Cannot add safety setting: SAFETY_SETTINGS_container not found!"); + return; + } + + // 如果容器当前只有占位符,则清除它 + const placeholder = container.querySelector('.text-gray-500.italic'); + if (placeholder && container.children.length === 1 && container.firstChild === placeholder) { + container.innerHTML = ''; + } + + const harmCategories = [ + "HARM_CATEGORY_HARASSMENT", + "HARM_CATEGORY_HATE_SPEECH", + "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "HARM_CATEGORY_DANGEROUS_CONTENT", + "HARM_CATEGORY_CIVIC_INTEGRITY" // 根据需要添加或移除 + ]; + const harmThresholds = [ + "BLOCK_NONE", + "BLOCK_LOW_AND_ABOVE", + "BLOCK_MEDIUM_AND_ABOVE", + "BLOCK_ONLY_HIGH", + "OFF" // 根据 Google API 文档添加或移除 + ]; + + const settingItem = document.createElement('div'); + settingItem.className = 'safety-setting-item flex items-center mb-2 gap-2'; + + // Category Select + const categorySelect = document.createElement('select'); + categorySelect.className = 'safety-category-select flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white'; + harmCategories.forEach(cat => { + const option = document.createElement('option'); + option.value = cat; + option.textContent = cat.replace('HARM_CATEGORY_', ''); // 显示更友好的名称 + if (cat === category) { + option.selected = true; + } + categorySelect.appendChild(option); + }); + + // Threshold Select + const thresholdSelect = document.createElement('select'); + thresholdSelect.className = 'safety-threshold-select w-48 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:border-primary-500 focus:ring focus:ring-primary-200 focus:ring-opacity-50 bg-white'; + harmThresholds.forEach(thr => { + const option = document.createElement('option'); + option.value = thr; + option.textContent = thr.replace('BLOCK_', '').replace('_AND_ABOVE', '+'); // 简化显示 + if (thr === threshold) { + option.selected = true; + } + thresholdSelect.appendChild(option); + }); + + // Remove Button + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'remove-btn text-gray-400 hover:text-red-500 focus:outline-none transition-colors duration-150'; + removeBtn.innerHTML = ''; + removeBtn.title = '删除此设置'; + removeBtn.addEventListener('click', function() { + const currentItem = this.closest('.safety-setting-item'); + currentItem.remove(); + // 检查容器是否为空,如果是,则添加回占位符 + if (container.children.length === 0) { + container.innerHTML = '
定义模型的安全过滤阈值。
'; + } + }); + + settingItem.appendChild(categorySelect); + settingItem.appendChild(thresholdSelect); + settingItem.appendChild(removeBtn); + + container.appendChild(settingItem); +} +// --- 结束:添加安全设置项的函数 --- + diff --git a/app/static/service-worker.js b/app/static/service-worker.js index 937b4cd..be2cd9a 100644 --- a/app/static/service-worker.js +++ b/app/static/service-worker.js @@ -17,13 +17,27 @@ self.addEventListener('install', event => { self.addEventListener('fetch', event => { event.respondWith( - caches.match(event.request) - .then(response => { - if (response) { - return response; - } - return fetch(event.request); - }) + caches.open(CACHE_NAME).then(cache => { + // 1. 尝试从缓存获取 + return cache.match(event.request).then(responseFromCache => { + // 2. 同时从网络获取 (后台进行) + const fetchPromise = fetch(event.request).then(responseFromNetwork => { + // 3. 网络请求成功,更新缓存 + cache.put(event.request, responseFromNetwork.clone()); + return responseFromNetwork; + }).catch(err => { + // 网络请求失败时,可以选择记录错误或不执行任何操作 + console.error('Network fetch failed:', err); + // 确保即使网络失败,如果缓存存在,我们仍然返回缓存 + // 如果缓存也不存在,则此 Promise 会 reject + throw err; + }); + + // 4. 如果缓存存在,立即返回缓存;否则等待网络响应 + // 后台的网络请求仍在进行,用于更新缓存 + return responseFromCache || fetchPromise; + }); + }) ); }); diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index 1d9f808..d55f1c2 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -311,6 +311,20 @@ --> 为每个思考模型设置预算(整数,最大值 24576),此项与上方模型列表自动关联。 + +
+ +
+ +
定义模型的安全过滤阈值。
+
+
+ +
+ 配置模型的安全过滤级别,例如 HARM_CATEGORY_HARASSMENT: BLOCK_NONE。 +