mirror of
https://github.com/snailyp/gemini-balance.git
synced 2026-07-20 03:51:53 +08:00
feat(config, chat, ui): 添加思考模型及预算管理功能
引入了思考模型 (THINKING_MODELS) 和相应的预算映射 (THINKING_BUDGET_MAP) 的概念,允许在配置中指定用于特定内部处理流程(如“思考过程”)的模型及其 token 预算。 主要变更包括: 后端 (Python): - 在 `Settings` 中添加了 `THINKING_MODELS` (List[str]) 和 `THINKING_BUDGET_MAP` (Dict[str, float]) 配置项。 - 增强了 `config._parse_db_value` 函数,以正确解析来自数据库或环境变量的列表和字典字符串(包括处理单引号和提供更详细的日志)。 - 更新了相关服务(如 `GeminiChatService`, `ModelService`, `ConfigService`)以识别和利用这些新配置。 - 调整了中间件和路由以适应可能的逻辑变更。 前端 (HTML/JavaScript): - 在配置编辑器 (`config_editor.html`, `config_editor.js`) 中添加了新的 UI 部分来管理思考模型列表和预算映射。 - 实现了动态添加/删除思考模型的功能,并自动关联/解除关联对应的预算映射条目。 - 预算映射中的模型名称(键)是只读的,自动从思考模型列表同步;预算值(值)是可编辑的数字输入。 - 更新了表单数据的加载 (`populateForm`) 和收集 (`collectFormData`) 逻辑,以正确处理新的列表和映射类型。 - 移除了手动添加预算映射的按钮,改为自动关联。 - 改进了数组和映射项的 DOM 操作逻辑,包括使用 UUID 来关联模型和预算项。
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 = '<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
|
||||
}
|
||||
|
||||
// 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 = '<i class="fas fa-trash-alt"></i>';
|
||||
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 = '<div class="text-gray-500 text-sm italic">请在上方添加思考模型,预算将自动关联。</div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
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 = '<i class="fas fa-trash-alt"></i>';
|
||||
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 已弃用) --- */
|
||||
|
||||
@@ -265,8 +265,38 @@
|
||||
<label for="SHOW_THINKING_PROCESS" class="toggle-label block overflow-hidden h-6 rounded-full bg-gray-300 cursor-pointer"></label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 思考模型列表 -->
|
||||
<div class="mb-6">
|
||||
<label for="THINKING_MODELS" class="block font-semibold mb-2 text-gray-700">思考模型列表</label>
|
||||
<div class="array-container bg-white rounded-lg border border-gray-200 p-4 mb-2" id="THINKING_MODELS_container">
|
||||
<!-- 数组项将在这里动态添加 -->
|
||||
</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" onclick="addArrayItem('THINKING_MODELS')">
|
||||
<i class="fas fa-plus"></i> 添加模型
|
||||
</button>
|
||||
</div>
|
||||
<small class="text-gray-500 mt-1 block">用于“思考过程”的模型列表</small>
|
||||
</div>
|
||||
|
||||
<!-- 思考模型预算映射 -->
|
||||
<div class="mb-6">
|
||||
<label for="THINKING_BUDGET_MAP" class="block font-semibold mb-2 text-gray-700">思考模型预算映射</label>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4 mb-2 space-y-3" id="THINKING_BUDGET_MAP_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="addBudgetMapItemBtn">
|
||||
<i class="fas fa-plus"></i> 添加预算映射
|
||||
</button>
|
||||
</div> -->
|
||||
<small class="text-gray-500 mt-1 block">为每个思考模型设置预算(整数),此项与上方模型列表自动关联。</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 图像生成相关配置 -->
|
||||
<div class="config-section bg-white bg-opacity-70 rounded-xl p-6 mb-6 shadow-lg" id="image-section">
|
||||
<h2 class="text-xl font-bold mb-6 pb-3 border-b border-gray-200 flex items-center gap-2">
|
||||
|
||||
Reference in New Issue
Block a user