feat: 增加 Gemini 安全设置支持

- 新增 `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
This commit is contained in:
snaily
2025-05-02 22:49:36 +08:00
parent 3480fa3b0f
commit 2225a40bbe
11 changed files with 282 additions and 71 deletions
+4
View File
@@ -47,3 +47,7 @@ STREAM_CHUNK_SIZE=5
# 日志级别 (debug, info, warning, error, critical),默认为 info # 日志级别 (debug, info, warning, error, critical),默认为 info
LOG_LEVEL=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"}]'
+28 -1
View File
@@ -9,7 +9,7 @@ from pydantic import ValidationError
from pydantic_settings import BaseSettings from pydantic_settings import BaseSettings
from sqlalchemy import insert, update, select 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 from app.log.logger import Logger
@@ -69,6 +69,7 @@ class Settings(BaseSettings):
# 日志配置 # 日志配置
LOG_LEVEL: str = "INFO" # 默认日志级别 LOG_LEVEL: str = "INFO" # 默认日志级别
SAFETY_SETTINGS: List[Dict[str, str]] = DEFAULT_SAFETY_SETTINGS # 新增:安全设置
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**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 # 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.") 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 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 # 处理 bool
elif target_type == bool: elif target_type == bool:
return db_value.lower() in ('true', '1', 'yes', 'on') return db_value.lower() in ('true', '1', 'yes', 'on')
+17 -1
View File
@@ -60,4 +60,20 @@ VIDEO_FORMAT_TO_MIMETYPE = {
"mov": "video/quicktime", "mov": "video/quicktime",
"avi": "video/x-msvideo", "avi": "video/x-msvideo",
"webm": "video/webm", "webm": "video/webm",
} }
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"},
]
+4 -9
View File
@@ -46,10 +46,6 @@ async def list_models(
): ):
"""获取可用的 Gemini 模型列表,并根据配置添加衍生模型(搜索、图像、非思考)。""" """获取可用的 Gemini 模型列表,并根据配置添加衍生模型(搜索、图像、非思考)。"""
operation_name = "list_gemini_models" operation_name = "list_gemini_models"
# 注意:此路由的错误处理相对复杂,涉及模型查找和修改,
# 使用通用错误处理可能隐藏部分逻辑错误。暂时保留原有结构,
# 但如果需要更统一的处理,可以将内部逻辑封装并应用 handle_route_errors。
# 这里仅添加日志分隔符。
logger.info("-" * 50 + operation_name + "-" * 50) logger.info("-" * 50 + operation_name + "-" * 50)
logger.info("Handling Gemini models list request") 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.") raise HTTPException(status_code=503, detail="No valid API keys available to fetch models.")
logger.info(f"Using API key: {api_key}") logger.info(f"Using API key: {api_key}")
# 假设 get_gemini_models 是同步的,如果不是需要 await models_data =await model_service.get_gemini_models(api_key)
models_data = model_service.get_gemini_models(api_key)
if not models_data or "models" not in models_data: if not models_data or "models" not in models_data:
raise HTTPException(status_code=500, detail="Failed to fetch base models list.") 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}" item["name"] = f"models/{base_name}{suffix}"
display_name = f'{item.get("displayName", base_name)}{display_suffix}' display_name = f'{item.get("displayName", base_name)}{display_suffix}'
item["displayName"] = display_name item["displayName"] = display_name
item["description"] = display_name # 使用 display_name 作为描述 item["description"] = display_name
models_json["models"].append(item) 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.debug(f"Request: \n{request.model_dump_json(indent=2)}")
logger.info(f"Using API key: {api_key}") 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") raise HTTPException(status_code=400, detail=f"Model {model_name} is not supported")
response = await chat_service.generate_content( 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.debug(f"Request: \n{request.model_dump_json(indent=2)}")
logger.info(f"Using API key: {api_key}") 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") raise HTTPException(status_code=400, detail=f"Model {model_name} is not supported")
response_stream = chat_service.stream_generate_content( response_stream = chat_service.stream_generate_content(
+2 -4
View File
@@ -54,9 +54,7 @@ async def list_models(
logger.info("Handling models list request") logger.info("Handling models list request")
api_key = await key_manager.get_first_valid_key() api_key = await key_manager.get_first_valid_key()
logger.info(f"Using API key: {api_key}") logger.info(f"Using API key: {api_key}")
# 注意:这里假设 model_service.get_gemini_openai_models 是同步函数 return await model_service.get_gemini_openai_models(api_key)
# 如果它是异步的,需要 await
return model_service.get_gemini_openai_models(api_key)
@router.post("/v1/chat/completions") @router.post("/v1/chat/completions")
@@ -83,7 +81,7 @@ async def chat_completion(
logger.info(f"Using API key: {current_api_key}") 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 捕获并记录 # 使用 HTTPException,会被 handle_route_errors 捕获并记录
raise HTTPException( raise HTTPException(
status_code=400, detail=f"Model {request.model} is not supported" status_code=400, detail=f"Model {request.model} is not supported"
+2 -14
View File
@@ -102,20 +102,8 @@ def _get_safety_settings(model: str) -> List[Dict[str, str]]:
# and "gemini-2.0-pro-exp" not in model # and "gemini-2.0-pro-exp" not in model
# ): # ):
if model == "gemini-2.0-flash-exp": if model == "gemini-2.0-flash-exp":
return [ return settings.GEMINI_2_FLASH_EXP_SAFETY_SETTINGS
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "OFF"}, return settings.SAFETY_SETTINGS
{"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"},
]
def _build_payload( def _build_payload(
+26 -1
View File
@@ -1,6 +1,6 @@
# app/services/chat/api_client.py # app/services/chat/api_client.py
from typing import Dict, Any, AsyncGenerator from typing import Dict, Any, AsyncGenerator, Optional
import httpx import httpx
import random import random
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
@@ -40,6 +40,31 @@ class GeminiApiClient(ApiClient):
model = model[:-20] model = model[:-20]
return model 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]: 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) timeout = httpx.Timeout(self.timeout, read=self.timeout)
model = self._get_real_model(model) model = self._get_real_model(model)
+29 -32
View File
@@ -1,50 +1,47 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import requests
from app.config.config import settings from app.config.config import settings
from app.log.logger import get_model_logger from app.log.logger import get_model_logger
from app.service.client.api_client import GeminiApiClient
logger = get_model_logger() logger = get_model_logger()
class ModelService: class ModelService:
def get_gemini_models(self, api_key: str) -> Optional[Dict[str, Any]]: async def get_gemini_models(self, api_key: str) -> Optional[Dict[str, Any]]:
url = f"{settings.BASE_URL}/models?key={api_key}" """使用 GeminiApiClient 获取并过滤模型列表"""
api_client = GeminiApiClient(base_url=settings.BASE_URL) # 实例化客户端
gemini_models = await api_client.get_models(api_key)
try: if gemini_models is None:
response = requests.get(url) logger.error("从 API 客户端获取模型列表失败。")
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}")
return None return None
def get_gemini_openai_models(self, api_key: str) -> Optional[Dict[str, Any]]:
try: try:
gemini_models = self.get_gemini_models(api_key) filtered_models_list = []
return self.convert_to_openai_models_format(gemini_models) for model in gemini_models.get("models", []):
except requests.RequestException as e: model_id = model["name"].split("/")[-1]
logger.error(f"Request failed: {e}") 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 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] self, gemini_models: Dict[str, Any]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
openai_format = {"object": "list", "data": [], "success": True} openai_format = {"object": "list", "data": [], "success": True}
@@ -81,7 +78,7 @@ class ModelService:
openai_format["data"].append(image_model) openai_format["data"].append(image_model)
return openai_format 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): if not model or not isinstance(model, str):
return False return False
+135 -2
View File
@@ -1,3 +1,7 @@
// 将需要在外部函数访问的 DOM 元素移到外部
const safetySettingsContainer = document.getElementById('SAFETY_SETTINGS_container');
const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container');
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 初始化配置 // 初始化配置
initConfig(); initConfig();
@@ -85,6 +89,7 @@ document.addEventListener('DOMContentLoaded', function() {
const cancelResetBtn = document.getElementById('cancelResetBtn'); const cancelResetBtn = document.getElementById('cancelResetBtn');
const confirmResetBtn = document.getElementById('confirmResetBtn'); 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 事件 // 添加事件委托,处理动态添加的 THINKING_MODELS 输入框的 input 事件
const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); // const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); // Moved outside
if (thinkingModelsContainer) { if (thinkingModelsContainer) {
thinkingModelsContainer.addEventListener('input', function(event) { thinkingModelsContainer.addEventListener('input', function(event) {
if (event.target && event.target.classList.contains('array-input') && event.target.closest('.array-item[data-model-id]')) { 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 结束括号 }); // <-- DOMContentLoaded 结束括号
@@ -367,7 +378,11 @@ async function initConfig() {
if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) { if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) {
config.THINKING_BUDGET_MAP = {}; // 默认为空对象 config.THINKING_BUDGET_MAP = {}; // 默认为空对象
} }
// --- 结束:处理新字段的默认值 --- // --- 新增:处理 SAFETY_SETTINGS 默认值 ---
if (!config.SAFETY_SETTINGS || !Array.isArray(config.SAFETY_SETTINGS)) {
config.SAFETY_SETTINGS = []; // 默认为空数组
}
// --- 结束:处理 SAFETY_SETTINGS 默认值 ---
populateForm(config); populateForm(config);
@@ -506,6 +521,24 @@ function populateForm(config) {
if (uploadProvider) { if (uploadProvider) {
toggleProviderConfig(uploadProvider.value); 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 = '<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
}
// --- 结束:填充 SAFETY_SETTINGS ---
} }
// --- 新增:处理批量添加 API Key 的逻辑 --- // --- 新增:处理批量添加 API Key 的逻辑 ---
@@ -963,6 +996,23 @@ function collectFormData() {
} }
// --- 结束:处理 THINKING_BUDGET_MAP --- // --- 结束:处理 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; return formData;
} }
@@ -1166,3 +1216,86 @@ function addBudgetMapItemWithValue(mapKey, mapValue, modelId) {
createAndAppendBudgetMapItem(mapKey, mapValue, modelId); createAndAppendBudgetMapItem(mapKey, mapValue, modelId);
} }
/* --- 结束:(addBudgetMapItemWithValue 已弃用) --- */ /* --- 结束:(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 = '<i class="fas fa-trash-alt"></i>';
removeBtn.title = '删除此设置';
removeBtn.addEventListener('click', function() {
const currentItem = this.closest('.safety-setting-item');
currentItem.remove();
// 检查容器是否为空,如果是,则添加回占位符
if (container.children.length === 0) {
container.innerHTML = '<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>';
}
});
settingItem.appendChild(categorySelect);
settingItem.appendChild(thresholdSelect);
settingItem.appendChild(removeBtn);
container.appendChild(settingItem);
}
// --- 结束:添加安全设置项的函数 ---
+21 -7
View File
@@ -17,13 +17,27 @@ self.addEventListener('install', event => {
self.addEventListener('fetch', event => { self.addEventListener('fetch', event => {
event.respondWith( event.respondWith(
caches.match(event.request) caches.open(CACHE_NAME).then(cache => {
.then(response => { // 1. 尝试从缓存获取
if (response) { return cache.match(event.request).then(responseFromCache => {
return response; // 2. 同时从网络获取 (后台进行)
} const fetchPromise = fetch(event.request).then(responseFromNetwork => {
return fetch(event.request); // 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;
});
})
); );
}); });
+14
View File
@@ -311,6 +311,20 @@
</div> --> </div> -->
<small class="text-gray-500 mt-1 block">为每个思考模型设置预算(整数,最大值 24576),此项与上方模型列表自动关联。</small> <small class="text-gray-500 mt-1 block">为每个思考模型设置预算(整数,最大值 24576),此项与上方模型列表自动关联。</small>
</div> </div>
<!-- 安全设置 -->
<div class="mb-6">
<label for="SAFETY_SETTINGS" class="block font-semibold mb-2 text-gray-700">安全设置 (Safety Settings)</label>
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-2 space-y-3" id="SAFETY_SETTINGS_container">
<!-- 安全设置项将在这里动态添加 -->
<div class="text-gray-500 text-sm italic">定义模型的安全过滤阈值。</div>
</div>
<div class="flex justify-end">
<button type="button" class="bg-primary-600 hover:bg-primary-700 text-white px-4 py-2 rounded-lg font-medium transition-all duration-200 flex items-center gap-2" id="addSafetySettingBtn">
<i class="fas fa-plus"></i> 添加安全设置
</button>
</div>
<small class="text-gray-500 mt-1 block">配置模型的安全过滤级别,例如 HARM_CATEGORY_HARASSMENT: BLOCK_NONE。</small>
</div>
</div> </div>
<!-- 图像生成相关配置 --> <!-- 图像生成相关配置 -->