diff --git a/.env.example b/.env.example index 183215b..fc24275 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,8 @@ API_KEYS=["AIzaSyxxxxxxxxxxxxxxxxxxx","AIzaSyxxxxxxxxxxxxxxxxxxx"] ALLOWED_TOKENS=["sk-123456"] # AUTH_TOKEN=sk-123456 TEST_MODEL=gemini-1.5-flash +THINKING_MODELS=["gemini-2.5-flash-preview-04-17"] +THINKING_BUDGET_MAP={"gemini-2.5-flash-preview-04-17": 4000} IMAGE_MODELS=["gemini-2.0-flash-exp"] SEARCH_MODELS=["gemini-2.0-flash-exp","gemini-2.0-pro-exp"] FILTERED_MODELS=["gemini-1.0-pro-vision-latest", "gemini-pro-vision", "chat-bison-001", "text-bison-001", "embedding-gecko-001"] diff --git a/app/config/config.py b/app/config/config.py index c0f8bd8..a2099b6 100644 --- a/app/config/config.py +++ b/app/config/config.py @@ -46,6 +46,8 @@ class Settings(BaseSettings): TOOLS_CODE_EXECUTION_ENABLED: bool = False SHOW_SEARCH_LINK: bool = True SHOW_THINKING_PROCESS: bool = True + THINKING_MODELS: List[str] = [] # 新增:用于思考过程的模型列表 + THINKING_BUDGET_MAP: Dict[str, float] = {} # 新增:模型对应的预算映射 # 图像生成相关配置 PAID_KEY: str = "" @@ -85,25 +87,54 @@ def _parse_db_value(key: str, db_value: str, target_type: Type) -> Any: from app.log.logger import get_config_logger # 函数内导入 logger = get_config_logger() # 函数内初始化 try: + # 处理 List[str] if target_type == List[str]: - # 尝试解析 JSON 列表,如果失败则按逗号分割 try: parsed = json.loads(db_value) if isinstance(parsed, list): return [str(item) for item in parsed] except json.JSONDecodeError: - # 回退到逗号分割,去除空格 return [item.strip() for item in db_value.split(',') if item.strip()] - # 如果解析后不是列表或解析失败,返回空列表或进行其他处理 logger.warning(f"Could not parse '{db_value}' as List[str] for key '{key}', falling back to comma split or empty list.") - return [item.strip() for item in db_value.split(',') if item.strip()] # Fallback + return [item.strip() for item in db_value.split(',') if item.strip()] + # 处理 Dict[str, float] + elif target_type == Dict[str, float]: + parsed_dict = {} + try: + # First attempt: standard JSON parsing + parsed = json.loads(db_value) + if isinstance(parsed, dict): + parsed_dict = {str(k): float(v) for k, v in parsed.items()} + else: + logger.warning(f"Parsed DB value for key '{key}' is not a dictionary type. Value: {db_value}") + except (json.JSONDecodeError, ValueError, TypeError) as e1: + # Second attempt: try replacing single quotes if JSONDecodeError occurred + if isinstance(e1, json.JSONDecodeError) and "'" in db_value: + logger.warning(f"Failed initial JSON parse for key '{key}'. Attempting to replace single quotes. Error: {e1}") + try: + corrected_db_value = db_value.replace("'", '"') + parsed = json.loads(corrected_db_value) + if isinstance(parsed, dict): + parsed_dict = {str(k): float(v) for k, v in parsed.items()} + else: + logger.warning(f"Parsed DB value (after quote replacement) for key '{key}' is not a dictionary type. Value: {corrected_db_value}") + except (json.JSONDecodeError, ValueError, TypeError) as e2: + logger.error(f"Could not parse '{db_value}' as Dict[str, float] for key '{key}' even after replacing quotes: {e2}. Returning empty dict.") + else: + # 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 + # 处理 bool elif target_type == bool: return db_value.lower() in ('true', '1', 'yes', 'on') + # 处理 int elif target_type == int: return int(db_value) + # 处理 float elif target_type == float: return float(db_value) - else: # 默认为 str 或其他 pydantic 能处理的类型 + # 默认为 str 或其他 pydantic 能直接处理的类型 + else: return db_value except (ValueError, TypeError, json.JSONDecodeError) as e: logger.warning(f"Failed to parse db_value '{db_value}' for key '{key}' as type {target_type}: {e}. Using original string value.") @@ -161,20 +192,18 @@ async def sync_initial_settings(): # 比较解析后的值和内存中的值 # 注意:对于列表等复杂类型,直接比较可能不够健壮,但这里简化处理 if parsed_db_value != memory_value: - # 检查类型是否匹配,以防解析函数返回了不兼容的类型 - # 优先处理 List[str] 类型,避免直接对泛型使用 isinstance - if target_type == List[str]: - if isinstance(parsed_db_value, list): - # 可以选择性地添加对列表元素的检查,但这里保持简化 - setattr(settings, key, parsed_db_value) - logger.info(f"Updated setting '{key}' in memory from database value (List[str]).") - updated_in_memory = True - else: - logger.warning(f"Parsed DB value type mismatch for key '{key}'. Expected List[str], got {type(parsed_db_value)}. Skipping update.") - # 对于其他非泛型类型,使用常规的 isinstance 检查 - elif isinstance(parsed_db_value, target_type): + # 检查类型是否匹配,以防解析函数返回了不兼容的类型 + type_match = False + if target_type == List[str] and isinstance(parsed_db_value, list): + type_match = True + elif target_type == Dict[str, float] and isinstance(parsed_db_value, dict): + type_match = True + elif target_type not in (List[str], Dict[str, float]) and isinstance(parsed_db_value, target_type): + type_match = True + + if type_match: setattr(settings, key, parsed_db_value) - logger.info(f"Updated setting '{key}' in memory from database value.") + logger.info(f"Updated setting '{key}' in memory from database value ({target_type}).") updated_in_memory = True else: logger.warning(f"Parsed DB value type mismatch for key '{key}'. Expected {target_type}, got {type(parsed_db_value)}. Skipping update.") @@ -205,10 +234,12 @@ async def sync_initial_settings(): for key, value in final_memory_settings.items(): # 序列化值为字符串或 JSON 字符串 - if isinstance(value, list): - db_value = json.dumps(value) + if isinstance(value, (list, dict)): # 处理列表和字典 + db_value = json.dumps(value, ensure_ascii=False) # 使用 ensure_ascii=False 以支持非 ASCII 字符 elif isinstance(value, bool): db_value = str(value).lower() + elif value is None: # 处理 None 值 + db_value = "" # 或者根据需要设为 NULL 或其他标记 else: db_value = str(value) diff --git a/app/middleware/request_logging_middleware.py b/app/middleware/request_logging_middleware.py index c3d516b..be1ec4e 100644 --- a/app/middleware/request_logging_middleware.py +++ b/app/middleware/request_logging_middleware.py @@ -26,7 +26,7 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware): f"Formatted request body:\n{json.dumps(formatted_body, indent=2, ensure_ascii=False)}" ) except json.JSONDecodeError: - logger.info("Request body is not valid JSON.") + logger.error("Request body is not valid JSON.") except Exception as e: logger.error(f"Error reading request body: {str(e)}") diff --git a/app/router/gemini_routes.py b/app/router/gemini_routes.py index c57aa8f..3140111 100644 --- a/app/router/gemini_routes.py +++ b/app/router/gemini_routes.py @@ -83,6 +83,21 @@ async def list_models( models_json["models"].append(item) + # 添加思考模型的非思考版本 + if settings.THINKING_MODELS: + for name in settings.THINKING_MODELS: + model = model_mapping.get(name) + if not model: + continue + + item = deepcopy(model) + item["name"] = f"models/{name}-non-thinking" + display_name = f'{item.get("displayName")} Non Thinking' + item["displayName"] = display_name + item["description"] = display_name + + models_json["models"].append(item) + return models_json diff --git a/app/service/chat/gemini_chat_service.py b/app/service/chat/gemini_chat_service.py index 5cc883c..5d1b42d 100644 --- a/app/service/chat/gemini_chat_service.py +++ b/app/service/chat/gemini_chat_service.py @@ -108,6 +108,12 @@ def _build_payload(model: str, request: GeminiRequest) -> Dict[str, Any]: if model.endswith("-image") or model.endswith("-image-generation"): payload.pop("systemInstruction") payload["generationConfig"]["responseModalities"] = ["Text", "Image"] + + if model.endswith("-non-thinking"): + payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0} + if model in settings.THINKING_BUDGET_MAP: + payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": settings.THINKING_BUDGET_MAP.get(request.model,1000)} + return payload diff --git a/app/service/chat/openai_chat_service.py b/app/service/chat/openai_chat_service.py index 7d5c455..97f713a 100644 --- a/app/service/chat/openai_chat_service.py +++ b/app/service/chat/openai_chat_service.py @@ -130,6 +130,10 @@ def _build_payload( payload["generationConfig"]["maxOutputTokens"] = request.max_tokens if request.model.endswith("-image") or request.model.endswith("-image-generation"): payload["generationConfig"]["responseModalities"] = ["Text", "Image"] + if request.model.endswith("-non-thinking"): + payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": 0} + if request.model in settings.THINKING_BUDGET_MAP: + payload["generationConfig"]["thinkingConfig"] = {"thinkingBudget": settings.THINKING_BUDGET_MAP.get(request.model,1000)} if ( instruction @@ -258,7 +262,7 @@ class OpenAIChatService: async for line in self.api_client.stream_generate_content( payload, model, current_attempt_key ): - # print(line) + print(line) if line.startswith("data:"): chunk = json.loads(line[6:]) openai_chunk = self.response_handler.handle_response( diff --git a/app/service/client/api_client.py b/app/service/client/api_client.py index f540535..1747756 100644 --- a/app/service/client/api_client.py +++ b/app/service/client/api_client.py @@ -31,7 +31,10 @@ class GeminiApiClient(ApiClient): model = model[:-7] if model.endswith("-image"): model = model[:-6] - + if model.endswith("-non-thinking"): + model = model[:-13] + if "-search" in model and "-non-thinking" in model: + model = model[:-20] return model async def generate_content(self, payload: Dict[str, Any], model: str, api_key: str) -> Dict[str, Any]: diff --git a/app/service/config/config_service.py b/app/service/config/config_service.py index 1bf5618..f5df6b0 100644 --- a/app/service/config/config_service.py +++ b/app/service/config/config_service.py @@ -47,6 +47,8 @@ class ConfigService: # 处理不同类型的值 if isinstance(value, list): db_value = json.dumps(value) + elif isinstance(value, dict): # 新增对 dict 类型的处理 + db_value = json.dumps(value) elif isinstance(value, bool): db_value = str(value).lower() else: diff --git a/app/service/model/model_service.py b/app/service/model/model_service.py index d220d50..873a4da 100644 --- a/app/service/model/model_service.py +++ b/app/service/model/model_service.py @@ -24,7 +24,7 @@ class ModelService: if model_id not in settings.FILTERED_MODELS: filtered_models_list.append(model) else: - logger.info(f"Filtered out model: {model_id}") + logger.debug(f"Filtered out model: {model_id}") gemini_models["models"] = filtered_models_list return gemini_models @@ -70,6 +70,10 @@ class ModelService: image_model = openai_model.copy() image_model["id"] = f"{model_id}-image" openai_format["data"].append(image_model) + if model_id in settings.THINKING_MODELS: + non_thinking_model = openai_model.copy() + non_thinking_model["id"] = f"{model_id}-non-thinking" + openai_format["data"].append(non_thinking_model) if settings.CREATE_IMAGE_MODEL: image_model = openai_model.copy() diff --git a/app/static/js/config_editor.js b/app/static/js/config_editor.js index 427eebf..91bcd7c 100644 --- a/app/static/js/config_editor.js +++ b/app/static/js/config_editor.js @@ -1,7 +1,7 @@ document.addEventListener('DOMContentLoaded', function() { // 初始化配置 initConfig(); - + // 标签切换 const tabButtons = document.querySelectorAll('.tab-btn'); tabButtons.forEach(button => { @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', function() { switchTab(tabId); }); }); - + // 上传提供商切换 const uploadProviderSelect = document.getElementById('UPLOAD_PROVIDER'); if (uploadProviderSelect) { @@ -20,7 +20,7 @@ document.addEventListener('DOMContentLoaded', function() { toggleProviderConfig(this.value); }); } - + // 切换按钮事件 const toggleSwitches = document.querySelectorAll('.toggle-switch'); toggleSwitches.forEach(toggleSwitch => { @@ -33,19 +33,19 @@ document.addEventListener('DOMContentLoaded', function() { } }); }); - + // 保存按钮 const saveBtn = document.getElementById('saveBtn'); if (saveBtn) { saveBtn.addEventListener('click', saveConfig); } - + // 重置按钮 const resetBtn = document.getElementById('resetBtn'); if (resetBtn) { resetBtn.addEventListener('click', resetConfig); } - + // 滚动按钮 window.addEventListener('scroll', toggleScrollButtons); @@ -197,55 +197,97 @@ document.addEventListener('DOMContentLoaded', function() { showNotification('已生成新认证令牌', 'success'); }); } + + // --- 修改:思考模型预算映射不再需要手动添加按钮 --- + // const addBudgetMapItemBtn = document.getElementById('addBudgetMapItemBtn'); + // if (addBudgetMapItemBtn) { + // addBudgetMapItemBtn.addEventListener('click', addBudgetMapItem); + // } + // --- 结束:思考模型预算映射相关 --- + + // 添加事件委托,处理动态添加的 THINKING_MODELS 输入框的 input 事件 + const thinkingModelsContainer = document.getElementById('THINKING_MODELS_container'); + if (thinkingModelsContainer) { + thinkingModelsContainer.addEventListener('input', function(event) { + if (event.target && event.target.classList.contains('array-input') && event.target.closest('.array-item[data-model-id]')) { + const modelInput = event.target; + const modelId = modelInput.closest('.array-item').getAttribute('data-model-id'); + const budgetKeyInput = document.querySelector(`.map-key-input[data-model-id="${modelId}"]`); + if (budgetKeyInput) { + budgetKeyInput.value = modelInput.value; + } + } + }); + } + + }); // <-- DOMContentLoaded 结束括号 +// --- 新增:生成唯一ID --- +function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); +} +// --- 结束:生成唯一ID --- + + // 初始化配置 async function initConfig() { try { showNotification('正在加载配置...', 'info'); const response = await fetch('/api/config'); - + if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } - + const config = await response.json(); - + // 确保数组字段有默认值 if (!config.API_KEYS || !Array.isArray(config.API_KEYS) || config.API_KEYS.length === 0) { config.API_KEYS = ['请在此处输入 API 密钥']; } - + if (!config.ALLOWED_TOKENS || !Array.isArray(config.ALLOWED_TOKENS) || config.ALLOWED_TOKENS.length === 0) { config.ALLOWED_TOKENS = ['']; } - + if (!config.IMAGE_MODELS || !Array.isArray(config.IMAGE_MODELS) || config.IMAGE_MODELS.length === 0) { config.IMAGE_MODELS = ['gemini-1.5-pro-latest']; } - + if (!config.SEARCH_MODELS || !Array.isArray(config.SEARCH_MODELS) || config.SEARCH_MODELS.length === 0) { config.SEARCH_MODELS = ['gemini-1.5-flash-latest']; } - + if (!config.FILTERED_MODELS || !Array.isArray(config.FILTERED_MODELS) || config.FILTERED_MODELS.length === 0) { config.FILTERED_MODELS = ['gemini-1.0-pro-latest']; } - + // --- 新增:处理新字段的默认值 --- + if (!config.THINKING_MODELS || !Array.isArray(config.THINKING_MODELS)) { + config.THINKING_MODELS = []; // 默认为空数组 + } + if (!config.THINKING_BUDGET_MAP || typeof config.THINKING_BUDGET_MAP !== 'object' || config.THINKING_BUDGET_MAP === null) { + config.THINKING_BUDGET_MAP = {}; // 默认为空对象 + } + // --- 结束:处理新字段的默认值 --- + populateForm(config); - + // 确保上传提供商有默认值 const uploadProvider = document.getElementById('UPLOAD_PROVIDER'); if (uploadProvider && !uploadProvider.value) { uploadProvider.value = 'smms'; // 设置默认值为 smms toggleProviderConfig('smms'); } - + showNotification('配置加载成功', 'success'); } catch (error) { console.error('加载配置失败:', error); showNotification('加载配置失败: ' + error.message, 'error'); - + // 加载失败时,使用默认配置 const defaultConfig = { API_KEYS: [''], @@ -253,9 +295,11 @@ async function initConfig() { IMAGE_MODELS: ['gemini-1.5-pro-latest'], SEARCH_MODELS: ['gemini-1.5-flash-latest'], FILTERED_MODELS: ['gemini-1.0-pro-latest'], - UPLOAD_PROVIDER: 'smms' + UPLOAD_PROVIDER: 'smms', + THINKING_MODELS: [], + THINKING_BUDGET_MAP: {} }; - + populateForm(defaultConfig); toggleProviderConfig('smms'); } @@ -263,45 +307,105 @@ async function initConfig() { // 填充表单 function populateForm(config) { + const modelIdMap = {}; // modelName -> modelId + + // 1. Clear existing dynamic content first + const arrayContainers = document.querySelectorAll('.array-container'); + arrayContainers.forEach(container => { + container.innerHTML = ''; // Clear all array containers + }); + const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container'); + if (budgetMapContainer) { + budgetMapContainer.innerHTML = ''; // Clear budget map container + } else { + console.error("Critical: THINKING_BUDGET_MAP_container not found!"); + return; // Cannot proceed + } + + // 2. Populate THINKING_MODELS and build the map + if (Array.isArray(config.THINKING_MODELS)) { + const container = document.getElementById('THINKING_MODELS_container'); + if (container) { + config.THINKING_MODELS.forEach(modelName => { + if (modelName && typeof modelName === 'string' && modelName.trim()) { + const trimmedModelName = modelName.trim(); + // Call addArrayItemWithValue to add the model DOM element and get its ID + const modelId = addArrayItemWithValue('THINKING_MODELS', trimmedModelName); + if (modelId) { + modelIdMap[trimmedModelName] = modelId; + } else { + console.warn(`Failed to get modelId for THINKING_MODEL: '${trimmedModelName}'`); + } + } else { + console.warn(`Invalid THINKING_MODEL entry found:`, modelName); + } + }); + } else { + console.error("Critical: THINKING_MODELS_container not found!"); + } + } + + // 3. Populate THINKING_BUDGET_MAP using the map + let budgetItemsAdded = false; + if (config.THINKING_BUDGET_MAP && typeof config.THINKING_BUDGET_MAP === 'object') { + for (const [modelName, budgetValue] of Object.entries(config.THINKING_BUDGET_MAP)) { + if (modelName && typeof modelName === 'string') { + const trimmedModelName = modelName.trim(); + const modelId = modelIdMap[trimmedModelName]; // Look up the ID + if (modelId) { + // Call the function specifically designed to add ONLY the budget map DOM element + createAndAppendBudgetMapItem(trimmedModelName, budgetValue, modelId); + budgetItemsAdded = true; + } else { + // Log if a budget entry exists but its corresponding model wasn't found/added + console.warn(`Budget map: Could not find model ID for '${trimmedModelName}'. Skipping budget item.`); + } + } else { + console.warn(`Invalid key found in THINKING_BUDGET_MAP:`, modelName); + } + } + } + // Add placeholder only if no budget items were successfully added + if (!budgetItemsAdded && budgetMapContainer) { + budgetMapContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
'; + } + + // 4. Populate other array fields (excluding THINKING_MODELS) for (const [key, value] of Object.entries(config)) { - // 首先检查是否是数组类型 - if (Array.isArray(value)) { + if (Array.isArray(value) && key !== 'THINKING_MODELS') { const container = document.getElementById(`${key}_container`); if (container) { - // 清除现有项 - const existingItems = container.querySelectorAll('.array-item'); - existingItems.forEach(item => item.remove()); - // 添加数组项 - value.forEach(item => { - // 确保只添加非空字符串项(如果需要) - // if (item && typeof item === 'string' && item.trim() !== '') { - addArrayItemWithValue(key, item); - // } + // Container already cleared, just add items + value.forEach(itemValue => { + if (typeof itemValue === 'string') { + addArrayItemWithValue(key, itemValue); // This adds non-thinking model array items + } else { + console.warn(`Invalid item found in array '${key}':`, itemValue); + } }); } - // 处理完数组后,跳过本次循环的剩余部分 - continue; } + } - // 如果不是数组,再尝试查找对应的单个元素 - const element = document.getElementById(key); - if (element) { - if (typeof value === 'boolean') { - element.checked = value; - } else { - // 处理其他类型 (确保 value 不是 null 或 undefined) - // 特别处理 LOG_LEVEL,确保大小写匹配 option 的 value - if (key === 'LOG_LEVEL' && typeof value === 'string') { - element.value = value.toUpperCase(); - } else { - element.value = value ?? ''; // 使用空字符串作为默认值 + // 5. Populate non-array/non-budget fields + for (const [key, value] of Object.entries(config)) { + if (!Array.isArray(value) && !(typeof value === 'object' && value !== null && key === 'THINKING_BUDGET_MAP')) { + const element = document.getElementById(key); + if (element) { + if (element.type === 'checkbox' && typeof value === 'boolean') { + element.checked = value; + } else if (element.type !== 'checkbox') { + if (key === 'LOG_LEVEL' && typeof value === 'string') { + element.value = value.toUpperCase(); + } else { + element.value = (value !== null && value !== undefined) ? value : ''; + } } } } - // 如果既不是数组,也找不到对应 ID 的元素,则忽略该配置项 } - // 初始化上传提供商配置 (保持不变) + // 6. Initialize upload provider const uploadProvider = document.getElementById('UPLOAD_PROVIDER'); if (uploadProvider) { toggleProviderConfig(uploadProvider.value); @@ -432,7 +536,7 @@ function switchTab(tabId) { button.classList.add('bg-white', 'bg-opacity-50', 'text-gray-700', 'hover:bg-opacity-70'); } }); - + // 更新内容区域 const sections = document.querySelectorAll('.config-section'); sections.forEach(section => { @@ -461,18 +565,31 @@ function toggleProviderConfig(provider) { function addArrayItem(key) { const container = document.getElementById(`${key}_container`); if (!container) return; - - addArrayItemWithValue(key, ''); + + const newItemValue = ''; // Start with an empty value for new items + const modelId = addArrayItemWithValue(key, newItemValue); // Add the DOM element + + // If it's a thinking model, also add the corresponding budget map item + if (key === 'THINKING_MODELS' && modelId) { + createAndAppendBudgetMapItem(newItemValue, 0, modelId); // Default budget 0 + } } -// 添加带值的数组项 +// 添加带值的数组项 (Adds array item DOM, returns modelId if it's a thinking model) function addArrayItemWithValue(key, value) { const container = document.getElementById(`${key}_container`); - if (!container) return; - + if (!container) return null; + + const isThinkingModel = key === 'THINKING_MODELS'; + const modelId = isThinkingModel ? generateUUID() : null; + const arrayItem = document.createElement('div'); // 主容器使用 Flexbox arrayItem.className = 'array-item flex items-center mb-2 gap-2'; // 添加 gap-2 来分隔元素 + if (isThinkingModel) { + arrayItem.setAttribute('data-model-id', modelId); // 添加ID属性 + } + // 创建一个包装器 div 来包含输入框和生成按钮 const inputWrapper = document.createElement('div'); @@ -485,6 +602,11 @@ function addArrayItemWithValue(key, value) { input.value = value; // 输入框占据包装器内的主要空间,移除边框和圆角,因为包装器已有 input.className = 'array-input flex-grow px-3 py-2 border-none rounded-l-md focus:outline-none'; // 移除右侧圆角 + if (isThinkingModel) { + input.setAttribute('data-model-id', modelId); // 添加ID属性 + input.placeholder = '思考模型名称'; // 添加占位符 + } + inputWrapper.appendChild(input); // 将输入框添加到包装器 @@ -514,7 +636,21 @@ function addArrayItemWithValue(key, value) { removeBtn.innerHTML = ''; removeBtn.title = '删除'; removeBtn.addEventListener('click', function() { - arrayItem.remove(); + const currentArrayItem = this.closest('.array-item'); + if (isThinkingModel) { + const currentModelId = currentArrayItem.getAttribute('data-model-id'); + // 查找并删除对应的预算映射项 + const budgetMapItem = document.querySelector(`.map-item[data-model-id="${currentModelId}"]`); + if (budgetMapItem) { + budgetMapItem.remove(); + // 检查预算映射容器是否为空,如果是,则添加回占位符 + const budgetContainer = document.getElementById('THINKING_BUDGET_MAP_container'); + if (budgetContainer && budgetContainer.children.length === 0) { + budgetContainer.innerHTML = '
请在上方添加思考模型,预算将自动关联。
'; + } + } + } + currentArrayItem.remove(); // 删除模型项本身 }); // 将包装器(包含输入框和可能的生成按钮)和删除按钮添加到主容器 @@ -523,12 +659,74 @@ function addArrayItemWithValue(key, value) { // 插入到容器末尾 container.appendChild(arrayItem); + + // 返回生成的 ID (如果是思考模型) 或 null + return isThinkingModel ? modelId : null; + // Note: This function no longer automatically calls createAndAppendBudgetMapItem } + +// --- 新增:专门用于创建和添加预算映射 DOM 元素 --- +function createAndAppendBudgetMapItem(mapKey, mapValue, modelId) { + const container = document.getElementById('THINKING_BUDGET_MAP_container'); + if (!container) { + console.error("Cannot add budget item: THINKING_BUDGET_MAP_container not found!"); + return; + } + + // If container currently only has the placeholder, clear it + const placeholder = container.querySelector('.text-gray-500.italic'); + // Check if the only child is the placeholder before clearing + if (placeholder && container.children.length === 1 && container.firstChild === placeholder) { + container.innerHTML = ''; + } + + const mapItem = document.createElement('div'); + mapItem.className = 'map-item flex items-center mb-2 gap-2'; + mapItem.setAttribute('data-model-id', modelId); // Add ID attribute + + // Key Input (Model Name) - Read-only + const keyInput = document.createElement('input'); + keyInput.type = 'text'; + keyInput.value = mapKey; + keyInput.placeholder = '模型名称 (自动关联)'; + keyInput.readOnly = true; + keyInput.className = 'map-key-input flex-grow px-3 py-2 border border-gray-300 rounded-md focus:outline-none bg-gray-100 text-gray-500'; + keyInput.setAttribute('data-model-id', modelId); + + // Value Input (Budget) - Integer + const valueInput = document.createElement('input'); + valueInput.type = 'number'; + // Ensure mapValue is treated as integer, default to 0 if invalid + const intValue = parseInt(mapValue, 10); + valueInput.value = isNaN(intValue) ? 0 : intValue; + valueInput.placeholder = '预算 (整数)'; + valueInput.className = 'map-value-input w-24 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'; + valueInput.addEventListener('input', function() { + this.value = this.value.replace(/[^0-9]/g, ''); // Remove non-digit characters + }); + + // Remove Button - Disabled + const removeBtn = document.createElement('button'); + removeBtn.type = 'button'; + removeBtn.className = 'remove-btn text-gray-300 cursor-not-allowed focus:outline-none'; + removeBtn.innerHTML = ''; + removeBtn.title = '请从上方模型列表删除'; + removeBtn.disabled = true; + + mapItem.appendChild(keyInput); + mapItem.appendChild(valueInput); + mapItem.appendChild(removeBtn); + + container.appendChild(mapItem); +} +// --- 结束:专门的预算映射项创建函数 --- + + // 收集表单数据 function collectFormData() { const formData = {}; - + // 处理普通输入 const inputs = document.querySelectorAll('input[type="text"], input[type="number"], select'); inputs.forEach(input => { @@ -541,13 +739,13 @@ function collectFormData() { } } }); - + // 处理复选框 const checkboxes = document.querySelectorAll('input[type="checkbox"]'); checkboxes.forEach(checkbox => { formData[checkbox.name] = checkbox.checked; }); - + // 处理数组 const arrayContainers = document.querySelectorAll('.array-container'); arrayContainers.forEach(container => { @@ -555,7 +753,25 @@ function collectFormData() { const arrayInputs = container.querySelectorAll('.array-input'); formData[key] = Array.from(arrayInputs).map(input => input.value).filter(value => value.trim() !== ''); }); - + + // --- 新增:处理 THINKING_BUDGET_MAP --- + const budgetMapContainer = document.getElementById('THINKING_BUDGET_MAP_container'); + if (budgetMapContainer) { + formData['THINKING_BUDGET_MAP'] = {}; + const mapItems = budgetMapContainer.querySelectorAll('.map-item'); + mapItems.forEach(item => { + const keyInput = item.querySelector('.map-key-input'); + const valueInput = item.querySelector('.map-value-input'); + if (keyInput && valueInput && keyInput.value.trim() !== '') { + // 将预算值解析为整数 + const budgetValue = parseInt(valueInput.value, 10); // 使用基数10 + // 检查是否为有效数字,如果不是则默认为 0 + formData['THINKING_BUDGET_MAP'][keyInput.value.trim()] = isNaN(budgetValue) ? 0 : budgetValue; + } + }); + } + // --- 结束:处理 THINKING_BUDGET_MAP --- + return formData; } @@ -596,7 +812,7 @@ async function saveConfig() { // 1. 停止定时任务 await stopScheduler(); - + const response = await fetch('/api/config', { method: 'PUT', headers: { @@ -604,16 +820,16 @@ async function saveConfig() { }, body: JSON.stringify(formData) }); - + if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); } - + const result = await response.json(); - + // 移除居中的 saveStatus 提示 - + showNotification('配置保存成功', 'success'); // 3. 启动新的定时任务 @@ -624,21 +840,21 @@ async function saveConfig() { // 保存失败时,也尝试重启定时任务,以防万一 await startScheduler(); // 移除居中的 saveStatus 提示 - + showNotification('保存配置失败: ' + error.message, 'error'); } } // 重置配置 (现在只负责打开模态框) -function resetConfig(event) { +function resetConfig(event) { // 阻止事件冒泡和默认行为 if (event) { event.preventDefault(); event.stopPropagation(); } - + console.log('resetConfig called. Event target:', event ? event.target.id : 'No event'); - + // 确保只有当事件来自重置按钮时才显示模态框 if (!event || event.target.id === 'resetBtn' || event.currentTarget.id === 'resetBtn') { const resetConfirmModal = document.getElementById('resetConfirmModal'); @@ -729,7 +945,7 @@ function scrollToBottom() { // 切换滚动按钮显示 function toggleScrollButtons() { const scrollButtons = document.querySelector('.scroll-buttons'); - + if (window.scrollY > 200) { scrollButtons.style.display = 'flex'; } else { @@ -749,3 +965,17 @@ function generateRandomToken() { return result; } // --- 结束:生成随机令牌函数 --- + +// --- 修改:添加思考模型预算映射项 (现在由添加思考模型触发) --- +// function addBudgetMapItem() { +// // 不再需要手动添加 +// } + +// Deprecated: This function is now effectively replaced by createAndAppendBudgetMapItem +// for the initial population logic. It delegates to the new function if called. +function addBudgetMapItemWithValue(mapKey, mapValue, modelId) { + // console.warn("Deprecated call to addBudgetMapItemWithValue, use createAndAppendBudgetMapItem instead for population."); + // Delegate to the new function which handles DOM creation + createAndAppendBudgetMapItem(mapKey, mapValue, modelId); +} +/* --- 结束:(addBudgetMapItemWithValue 已弃用) --- */ diff --git a/app/templates/config_editor.html b/app/templates/config_editor.html index e1b1a82..e34e9cd 100644 --- a/app/templates/config_editor.html +++ b/app/templates/config_editor.html @@ -265,8 +265,38 @@ + + +
+ +
+ +
+
+ +
+ 用于“思考过程”的模型列表 +
+ + +
+ +
+ +
请先在上方添加思考模型,然后在此处配置预算。
+
+ + + 为每个思考模型设置预算(整数),此项与上方模型列表自动关联。 +
- +